diff --git a/collector.py b/collector.py index 4f1bbaf..b2c7d41 100755 --- a/collector.py +++ b/collector.py @@ -17,7 +17,7 @@ import dataaccess __author__ = 'Jeffrey B. Otterson, N1KDO' -__copyright__ = 'Copyright 2016, 2017, 2019 Jeffrey B. Otterson' +__copyright__ = 'Copyright 2016, 2017, 2019, 2024 Jeffrey B. Otterson' __license__ = 'Simplified BSD' BROADCAST_BUF_SIZE = 2048 @@ -82,7 +82,7 @@ class N1mmMessageParser: """ this is a cheap and dirty class to parse N1MM+ broadcast messages. It accepts the message and returns a dict, keyed by the element name. - This is unsuitable for any for any other purpose, since it throws away the + This is unsuitable for any other purpose, since it throws away the outer _contactinfo_ (or whatever) element -- instead it returns the name of the outer element as the value of the __messagetype__ key. OTOH, hopefully it is faster than using the DOM-based minidom.parse @@ -162,14 +162,12 @@ def process_message(parser, db, cursor, operators, stations, message, seen): """ Process a N1MM+ contactinfo message """ - bInsert = False message = compress_message(message) - #logging.debug(message) data = parser.parse(message) - message_type = data.get('__messagetype__') or '' - logging.debug('Received UDP message %s' % (message_type)) - if message_type == 'contactinfo' or message_type == 'contactreplace': - qso_id = data.get('ID') or ''; + message_type = data.get('__messagetype__', '') + logging.debug(f'Received UDP message {message_type}') + if message_type in ['contactinfo', 'contactreplace']: + qso_id = data.get('ID', '') # If no ID tag from N1MM, generate a hash for uniqueness if len(qso_id) == 0: @@ -178,24 +176,23 @@ def process_message(parser, db, cursor, operators, stations, message, seen): qso_id = qso_id.replace('-','') qso_timestamp = data.get('timestamp') - mycall = data.get('mycall') + mycall = data.get('mycall', '').upper() band = data.get('band') - mode = data.get('mode') - operator = data.get('operator') - station_name = data.get('StationName') + mode = data.get('mode', '').upper() + operator = data.get('operator', '').upper() + station_name = data.get('StationName', '').upper() if station_name is None or station_name == '': - station_name = data.get('NetBiosName') - station = station_name + station_name = data.get('NetBiosName', '') + station = station_name.upper() rx_freq = int(data.get('rxfreq')) * 10 # convert to Hz tx_freq = int(data.get('txfreq')) * 10 - callsign = data.get('call') + callsign = data.get('call', '').upper() rst_sent = data.get('snt') rst_recv = data.get('rcv') - exchange = data.get('exchange1') - section = data.get('section') - comment = data.get('comment') or '' + exchange = data.get('exchange1', '').upper() + section = data.get('section', '').upper() + comment = data.get('comment', '') - # convert qso_timestamp to datetime object timestamp = convert_timestamp(qso_timestamp) @@ -204,29 +201,31 @@ def process_message(parser, db, cursor, operators, stations, message, seen): rx_freq, tx_freq, callsign, rst_sent, rst_recv, exchange, section, comment, qso_id) elif message_type == 'RadioInfo': - logging.debug('Received radioInfo message') + logging.debug('Received RadioInfo message') elif message_type == 'contactdelete': - qso_id = data.get('ID') or ''; + qso_id = data.get('ID') or '' # If no ID tag from N1MM, generate a hash for uniqueness if len(qso_id) == 0: - qso_id = checksum(data) + qso_id = checksum(data) else: - qso_id = qso_id.replace('-','') + qso_id = qso_id.replace('-', '') - logging.info('Delete QSO Request with ID %s' % (qso_id)) + logging.info(f'Delete QSO Request with ID {qso_id}') dataaccess.delete_contact_by_qso_id(db, cursor, qso_id) elif message_type == 'dynamicresults': logging.debug('Received Score message') else: - logging.warning('unknown message type {} received, ignoring.'.format(message_type)) + logging.warning(f'unknown message type "{message_type}" received, ignoring.') logging.debug(message) def message_processor(q, event): global run logging.info('collector message_processor starting.') + message_count = 0 + seen = set() db = sqlite3.connect(config.DATABASE_FILENAME) try: cursor = db.cursor() @@ -235,8 +234,6 @@ def message_processor(q, event): operators = Operators(db, cursor) stations = Stations(db, cursor) parser = N1mmMessageParser() - message_count = 0 - seen = set() thread_run = True while not event.is_set() and thread_run: @@ -251,7 +248,7 @@ def message_processor(q, event): db.close() logging.info('db closed') run = False - logging.info('collector message_processor exited, {} messages collected.'.format(message_count)) + logging.info(f'collector message_processor exited, {message_count} messages collected.') def main(): diff --git a/config.py b/config.py index 8af9cbf..4ea74eb 100644 --- a/config.py +++ b/config.py @@ -8,16 +8,11 @@ """ name of database file """ DATABASE_FILENAME = 'n1mm_view.db' """ Name of the event/contest """ -EVENT_NAME = 'Field Day' +EVENT_NAME = 'N4N Field Day' """ start time of the event/contest in YYYY-MM-DD hh:mm:ss format """ -# EVENT_START_TIME = datetime.datetime.strptime('2019-06-25 18:00:00', '%Y-%m-%d %H:%M:%S') -# EVENT_START_TIME = datetime.datetime.strptime('2021-06-26 18:00:00', '%Y-%m-%d %H:%M:%S') -# EVENT_START_TIME = datetime.datetime.strptime('2022-06-25 18:00:00', '%Y-%m-%d %H:%M:%S') + EVENT_START_TIME = datetime.datetime.strptime('2024-06-22 18:00:00', '%Y-%m-%d %H:%M:%S') """ end time of the event/contest """ -# EVENT_END_TIME = datetime.datetime.strptime('2019-06-26 17:59:59', '%Y-%m-%d %H:%M:%S') -# EVENT_END_TIME = datetime.datetime.strptime('2021-06-27 17:59:59', '%Y-%m-%d %H:%M:%S') -# EVENT_END_TIME = datetime.datetime.strptime('2022-06-26 17:59:59', '%Y-%m-%d %H:%M:%S') EVENT_END_TIME = datetime.datetime.strptime('2024-06-23 17:59:59', '%Y-%m-%d %H:%M:%S') """ port number used by N1MM+ for UDP broadcasts This matches the port you set in N1MM Configurator UDP logging """ N1MM_BROADCAST_PORT = 12060 @@ -27,8 +22,7 @@ """ N1MM_BROADCAST_ADDRESS = '192.168.1.255' """ n1mm+ log file name used by replayer """ -# N1MM_LOG_FILE_NAME = 'MyClubCall-2019.s3db' -N1MM_LOG_FILE_NAME = 'fd2024.s3db' +N1MM_LOG_FILE_NAME = 'FD2024-N4N.s3db' """ QTH here is the location of your event. We mark this location with a red dot when we generate the map views.""" """ QTH Latitude """ QTH_LATITUDE = 34.0109629 @@ -42,12 +36,11 @@ """ DATA_DWELL_TIME = 60 """ log level for apps -- one of logging.WARN, logging.INFO, logging.DEBUG """ -LOG_LEVEL = logging.INFO +LOG_LEVEL = logging.DEBUG # """images directory, or None if not writing image files""" -IMAGE_DIR = '/mnt/ramdisk/n1mm_view/html' -IMAGE_WIDTH = 1920 -IMAGE_HEIGHT = 1080 +IMAGE_DIR = None # '/mnt/ramdisk/n1mm_view/html' + """ set HEADLESS True to not open graphics window. This is for using only the Apache option.""" HEADLESS = False diff --git a/dashboard.py b/dashboard.py index 733d67b..4510658 100755 --- a/dashboard.py +++ b/dashboard.py @@ -30,9 +30,10 @@ QSO_STATIONS_PIE_INDEX = 5 QSO_BANDS_PIE_INDEX = 6 QSO_MODES_PIE_INDEX = 7 -QSO_RATE_CHART_IMAGE_INDEX = 8 -SECTIONS_WORKED_MAP_INDEX = 9 -IMAGE_COUNT = 10 +QSO_CLASSES_PIE_INDEX = 8 +QSO_RATE_CHART_IMAGE_INDEX = 9 +SECTIONS_WORKED_MAP_INDEX = 10 +IMAGE_COUNT = 11 IMAGE_MESSAGE = 1 CRAWL_MESSAGE = 2 @@ -57,6 +58,7 @@ def load_data(size, q, last_qso_timestamp): operator_qso_rates = [] qsos_per_hour = [] qsos_by_section = {} + qso_classes = [] db = None data_updated = False @@ -85,6 +87,9 @@ def load_data(size, q, last_qso_timestamp): # get something else. qso_band_modes = dataaccess.get_qso_band_modes(cursor) + # load qso exchange data: what class are the other stations? + qso_classes = dataaccess.get_qso_classes(cursor) + # load QSOs per Hour by Operator operator_qso_rates = dataaccess.get_qsos_per_hour_per_operator(cursor, last_qso_time) @@ -150,7 +155,12 @@ def load_data(size, q, last_qso_timestamp): except Exception as e: logging.exception(e) try: - image_data, image_size = graphics.qso_rates_chart(size, qsos_per_hour) + image_data, image_size = graphics.qso_classes_graph(size, qso_classes) + enqueue_image(q, QSO_CLASSES_PIE_INDEX, image_data, image_size) + except Exception as e: + logging.exception(e) + try: + image_data, image_size = graphics.qso_rates_graph(size, qsos_per_hour) enqueue_image(q, QSO_RATE_CHART_IMAGE_INDEX, image_data, image_size) except Exception as e: logging.exception(e) diff --git a/dataaccess.py b/dataaccess.py index 75761b1..d5ceb53 100644 --- a/dataaccess.py +++ b/dataaccess.py @@ -47,7 +47,7 @@ def create_tables(db, cursor): ' exchange char(4),\n' ' section char(4),\n' ' comment TEXT,\n' - ' qso_id char(32) UNIQUE NOT NULL);') + ' qso_id char(32) PRIMARY KEY NOT NULL);') # this is primary key to speed up Update & Delete cursor.execute('CREATE INDEX IF NOT EXISTS qso_log_band_id ON qso_log(band_id);') cursor.execute('CREATE INDEX IF NOT EXISTS qso_log_mode_id ON qso_log(mode_id);') cursor.execute('CREATE INDEX IF NOT EXISTS qso_log_operator_id ON qso_log(operator_id);') @@ -56,6 +56,7 @@ def create_tables(db, cursor): cursor.execute('CREATE INDEX IF NOT EXISTS qso_log_qso_id ON qso_log(qso_id);') db.commit() + def record_contact_combined(db, cursor, operators, stations, timestamp, mycall, band, mode, operator, station, rx_freq, tx_freq, callsign, rst_sent, rst_recv, @@ -92,7 +93,6 @@ def record_contact_combined(db, cursor, operators, stations, logging.warning('[dataaccess] Insert Failed: %s\nError: %s' % (qso_id, str(err))) - def record_contact(db, cursor, operators, stations, timestamp, mycall, band, mode, operator, station, rx_freq, tx_freq, callsign, rst_sent, rst_recv, @@ -128,6 +128,7 @@ def record_contact(db, cursor, operators, stations, except Exception as err: logging.warning('[dataaccess] Insert Failed: %s\nError: %s' % (qso_id, str(err))) + def update_contact(db, cursor, operators, stations, timestamp, mycall, band, mode, operator, station, rx_freq, tx_freq, callsign, rst_sent, rst_recv, @@ -163,6 +164,7 @@ def update_contact(db, cursor, operators, stations, except Exception as err: logging.warning('[dataaccess] Update Failed: %s\nError: %s' % (qso_id, str(err))) + def delete_contact(db, cursor, timestamp, station, callsign): """ Delete the results of a delete in N1MM @@ -180,6 +182,7 @@ def delete_contact(db, cursor, timestamp, station, callsign): logging.exception('[dataaccess] Exception deleting contact from db.') return '' + def delete_contact_by_qso_id(db, cursor, qso_id): """ Delete the results of a delete in N1MM @@ -262,6 +265,13 @@ def get_qso_band_modes(cursor): return qso_band_modes +def get_qso_classes(cursor): + cursor.execute('SELECT COUNT(*), exchange FROM qso_log group by exchange;') + exchanges = [] + for row in cursor: + exchanges.append((row[0], row[1])) + return exchanges + def get_qsos_per_hour_per_band(cursor): qsos_per_hour = [] qsos_by_band = [0] * constants.Bands.count() diff --git a/graphics.py b/graphics.py index dc4b16d..161bd61 100644 --- a/graphics.py +++ b/graphics.py @@ -12,8 +12,8 @@ import matplotlib import matplotlib.backends.backend_agg as agg import matplotlib.cm +import matplotlib.colors as mcolors import matplotlib.pyplot as plt -import numpy as np import pygame from matplotlib.dates import HourLocator, DateFormatter @@ -64,11 +64,11 @@ def init_display(): try: pygame.display.init() except pygame.error as ex: - logging.warning(f'pygame error {ex}') - logging.warning('Driver: %s failed.' % driver) + logging.debug(f'pygame error {ex}') + logging.debug('Driver: %s failed.' % driver) continue found = True - logging.info('using %s driver', driver) + logging.info(f'Discovered compatible driver {driver}') break if not found or driver is None: @@ -85,9 +85,7 @@ def init_display(): # size = (size[0]-10, size[1] - 30) screen = pygame.display.set_mode(size, pygame.NOFRAME) - logging.debug('display size: %d x %d', size[0], size[1]) - # Clear the screen to start - screen.fill(BLACK) + logging.info('display size: %d x %d', size[0], size[1]) return screen, size @@ -105,9 +103,8 @@ def show_graph(screen, size, surf): def save_image(image_data, image_size, filename): surface = pygame.image.frombuffer(image_data, image_size, 'RGB') - logging.info('Saving file to %s', filename) + logging.debug('Saving file to %s', filename) pygame.image.save(surface, filename) - pass def make_pie(size, values, labels, title): @@ -117,15 +114,21 @@ def make_pie(size, values, labels, title): make the pie chart a square that is as tall as the display. """ logging.debug('make_pie(...,...,%s)', title) - inches = size[1] / 100.0 - fig = plt.figure(figsize=(inches, inches), dpi=100, tight_layout={'pad': 0.10}, facecolor='k') + new_labels = [] + for i in range(0, len(labels)): + new_labels.append(f'{labels[i]} ({values[i]})') + + width_inches = size[0] / 100.0 + height_inches = size[1] / 100.0 + fig = plt.figure(figsize=(width_inches, height_inches), dpi=100, tight_layout={'pad': 0.10, }, facecolor='k') ax = fig.add_subplot(111) - ax.pie(values, labels=labels, autopct='%1.1f%%', textprops={'color': 'w'}, wedgeprops={'linewidth': 0.25}, - colors=('b', 'g', 'r', 'c', 'm', 'y', '#ff9900', '#00ff00', '#663300')) + ax.pie(values, labels=new_labels, autopct='%1.1f%%', textprops={'color': 'w', 'fontsize': 14}, + wedgeprops={'linewidth': 0.25}, colors=mcolors.TABLEAU_COLORS) ax.set_title(title, color='white', size=48, weight='bold') handles, labels = ax.get_legend_handles_labels() - legend = ax.legend(handles[0:5], labels[0:5], title='Top %s' % title, loc='lower left') # best + # legend = ax.legend(handles[0:5], labels[0:5], title='Top %s' % title, loc='upper right', prop={'size': 14}) + legend = ax.legend(handles[0:5], labels[0:5], loc='upper right', prop={'size': 14}) # best frame = legend.get_frame() frame.set_color((0, 0, 0, 0.75)) frame.set_edgecolor('w') @@ -136,11 +139,11 @@ def make_pie(size, values, labels, title): canvas = agg.FigureCanvasAgg(fig) canvas.draw() renderer = canvas.get_renderer() + canvas_size = canvas.get_width_height() raw_data = renderer.tostring_rgb() plt.close(fig) - canvas_size = canvas.get_width_height() logging.debug('make_pie(...,...,%s) done', title) return raw_data, canvas_size @@ -160,6 +163,49 @@ def qso_operators_graph(size, qso_operators): return make_pie(size, values, labels, "QSOs by Operator") +def qso_classes_graph(size, qso_classes): + """ + create the QSOs by Operators pie chart + """ + # calculate QSO by class + if qso_classes is None or len(qso_classes) == 0: + return None, (0, 0) + qso_classes = sorted(qso_classes, key=lambda x: x[0]) + + total = 0 + for qso_class in qso_classes: + total += qso_class[0] + + summarize = 0 + threshold = 2.0 + for qso_class in qso_classes: + pct = qso_class[0] / total * 100.0 + if pct < threshold: + summarize = qso_class[0] + else: + break + + grouped_qso_classes = [] + summarized_names = [] + summarized_values = 0 + + for d in qso_classes: + if d[0] <= summarize: + summarized_names.append(d[1]) + summarized_values += d[0] + else: + grouped_qso_classes.append(d) + grouped_qso_classes = sorted(grouped_qso_classes, key=lambda x: x[0], reverse=True) + grouped_qso_classes.append((summarized_values, f'{len(summarized_names)} others')) + + labels = [] + values = [] + for d in grouped_qso_classes: + labels.append(d[1]) + values.append(d[0]) + return make_pie(size, values, labels, "QSOs by Class") + + def qso_operators_table(size, qso_operators): """ create the Top 5 QSOs by Operators table @@ -180,6 +226,7 @@ def qso_operators_table(size, qso_operators): else: return draw_table(size, cells, "Top 5 Operators", bigger_font) + def qso_operators_table_all(size, qso_operators): """ create the QSOs by All Operators table @@ -198,6 +245,7 @@ def qso_operators_table_all(size, qso_operators): else: return draw_table(size, cells, "QSOs by All Operators", bigger_font) + def qso_stations_graph(size, qso_stations): """ create the QSOs by Station pie chart @@ -319,7 +367,7 @@ def qso_rates_table(size, operator_qso_rates): return draw_table(size, operator_qso_rates, "QSO/Hour Rates") -def qso_rates_chart(size, qsos_per_hour): +def qso_rates_graph(size, qsos_per_hour): """ make the qsos per hour per band chart returns a pygame surface @@ -354,7 +402,6 @@ def qso_rates_chart(size, qsos_per_hour): lt = calendar.timegm(qsos_per_hour[-1][0].timetuple()) if data_valid: dates = matplotlib.dates.date2num(qso_counts[0]) - colors = ['r', 'g', 'b', 'c', 'm', 'y', '#ff9900', '#00ff00', '#663300'] labels = Bands.BANDS_TITLE[1:] if lt < st: start_date = dates[0] # matplotlib.dates.date2num(qsos_per_hour[0][0].timetuple()) @@ -365,7 +412,8 @@ def qso_rates_chart(size, qsos_per_hour): ax.set_xlim(start_date, end_date) ax.stackplot(dates, qso_counts[1], qso_counts[2], qso_counts[3], qso_counts[4], qso_counts[5], qso_counts[6], - qso_counts[7], qso_counts[8], qso_counts[9], labels=labels, colors=colors, linewidth=0.2) + qso_counts[7], qso_counts[8], qso_counts[9], labels=labels, colors=mcolors.TABLEAU_COLORS, + linewidth=0.2) ax.grid(True) legend = ax.legend(loc='best', ncol=Bands.count() - 1) legend.get_frame().set_color((0, 0, 0, 0)) @@ -520,7 +568,10 @@ def draw_map(size, qsos_by_section): transform=ax.transAxes, style='italic', size=14, color='white') ranges = [0, 1, 2, 10, 20, 50, 100] # , 500] # , 1000] num_colors = len(ranges) - color_palette = matplotlib.cm.viridis(np.linspace(0.33, 1, num_colors + 1)) + # color_palette = matplotlib.cm.viridis(np.linspace(0.33, 1, num_colors + 1)) + delta = 1 / (num_colors + 1) + colors = [delta * i for i in range(num_colors+1)] + color_palette = matplotlib.cm.viridis(colors) for section_name in CONTEST_SECTIONS.keys(): qsos = qsos_by_section.get(section_name) diff --git a/headless.py b/headless.py index 2a33398..f9ee946 100644 --- a/headless.py +++ b/headless.py @@ -22,12 +22,16 @@ __license__ = 'Simplified BSD' logging.basicConfig(format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s', datefmt='%Y-%m-%d %H:%M:%S', - level=config.LOG_LEVEL) + level=logging.DEBUG) #.LOG_LEVEL) logging.Formatter.converter = time.gmtime def makePNGTitle(image_dir, title): - return ''.join([image_dir, '/', re.sub('[^\w\-_]', '_', title), '.png']) + if image_dir is None: + image_dir = './images' + title = title.replace(' ', '_') + return f'{image_dir}/{title}.png' + # return ''.join([image_dir, '/', re.sub('[^\w\-_]', '_', title), '.png']) def create_images(size, image_dir, last_qso_timestamp): @@ -42,6 +46,7 @@ def create_images(size, image_dir, last_qso_timestamp): operator_qso_rates = [] qsos_per_hour = [] qsos_by_section = {} + qso_classes = [] db = None data_updated = False @@ -82,8 +87,11 @@ def create_images(size, image_dir, last_qso_timestamp): # load QSO rates per Hour by Band qsos_per_hour, qsos_per_band = dataaccess.get_qsos_per_hour_per_band(cursor) - # load QSOs by Section - qsos_by_section = dataaccess.get_qsos_by_section(cursor) + # load qso exchange data: what class are the other stations? + qso_classes = dataaccess.get_qso_classes(cursor) + + # load QSOs by Section + qsos_by_section = dataaccess.get_qsos_by_section(cursor) logging.debug('load data done') except sqlite3.OperationalError as error: @@ -146,8 +154,14 @@ def create_images(size, image_dir, last_qso_timestamp): except Exception as e: logging.exception(e) try: - image_data, image_size = graphics.qso_rates_chart(size, qsos_per_hour) - filename = makePNGTitle(image_dir, 'qso_rates_chart') + image_data, image_size = graphics.qso_classes_graph(size, qso_classes) + filename = makePNGTitle(image_dir, 'qso_classes_graph') + graphics.save_image(image_data, image_size, filename) + except Exception as e: + logging.exception(e) + try: + image_data, image_size = graphics.qso_rates_graph(size, qsos_per_hour) + filename = makePNGTitle(image_dir, 'qso_rates_graph') graphics.save_image(image_data, image_size, filename) except Exception as e: logging.exception(e) @@ -177,11 +191,12 @@ def main(): logging.debug("Checking for IMAGE_DIR") logging.info("IMAGE_DIR set to %s - checking if exists" % config.IMAGE_DIR) # Check if the dir given exists and create if necessary - if not os.path.exists(config.IMAGE_DIR): - logging.error("%s did not exist - creating..." % config.IMAGE_DIR) - os.makedirs(config.IMAGE_DIR) - if not os.path.exists(config.IMAGE_DIR): - sys.exit('Image %s directory could not be created' % config.IMAGE_DIR) + if config.IMAGE_DIR is not None: + if not os.path.exists(config.IMAGE_DIR): + logging.error("%s did not exist - creating..." % config.IMAGE_DIR) + os.makedirs(config.IMAGE_DIR) + if not os.path.exists(config.IMAGE_DIR): + sys.exit('Image %s directory could not be created' % config.IMAGE_DIR) logging.info('creating world...') # base_map = graphics.create_map() diff --git a/one_chart.py b/one_chart.py index 87e7267..622ecb2 100644 --- a/one_chart.py +++ b/one_chart.py @@ -3,7 +3,7 @@ """ This script allows you to view ONE of the graphs relatively quickly, mostly for debugging the graph generators. """ - +import gc import logging import pygame import sqlite3 @@ -69,6 +69,9 @@ def main(): # load QSO rates per Hour by Band qsos_per_hour, qsos_per_band = dataaccess.get_qsos_per_hour_per_band(cursor) + # load qso exchange data: what class are the other stations? + qso_classes = dataaccess.get_qso_classes(cursor) + # load QSOs by Section qsos_by_section = dataaccess.get_qsos_by_section(cursor) @@ -84,18 +87,29 @@ def main(): db = None try: - # image_data, image_size = graphics.qso_summary_table(size, qso_band_modes) - # image_data, image_size = graphics.qso_rates_table(size, operator_qso_rates) - # image_data, image_size = graphics.qso_operators_graph(size, qso_operators) - # image_data, image_size = graphics.qso_operators_table(size, qso_operators) - # image_data, image_size = graphics.qso_stations_graph(size, qso_stations) - # image_data, image_size = graphics.qso_bands_graph(size, qso_band_modes) - # image_data, image_size = graphics.qso_modes_graph(size, qso_band_modes) - # image_data, image_size = graphics.qso_rates_chart(size, qsos_per_hour) + image_data, image_size = graphics.qso_summary_table(size, qso_band_modes) + graphics.save_image(image_data, image_size, 'images/qso_summary_table.png') + image_data, image_size = graphics.qso_rates_table(size, operator_qso_rates) + graphics.save_image(image_data, image_size, 'images/qso_rates_table.png') + image_data, image_size = graphics.qso_operators_graph(size, qso_operators) + graphics.save_image(image_data, image_size, 'images/qso_operators_graph.png') + image_data, image_size = graphics.qso_operators_table(size, qso_operators) + graphics.save_image(image_data, image_size, 'images/qso_operators_table.png') + image_data, image_size = graphics.qso_stations_graph(size, qso_stations) + graphics.save_image(image_data, image_size, 'images/qso_stations_graph.png') + image_data, image_size = graphics.qso_bands_graph(size, qso_band_modes) + graphics.save_image(image_data, image_size, 'images/qso_bands_graph.png') + image_data, image_size = graphics.qso_classes_graph(size, qso_classes) + image = pygame.image.frombuffer(image_data, image_size, 'RGB') # this is the image to SHOW on the screen + graphics.save_image(image_data, image_size, 'images/qso_classes_graph.png') + image_data, image_size = graphics.qso_modes_graph(size, qso_band_modes) + graphics.save_image(image_data, image_size, 'images/qso_modes_graph.png') + image_data, image_size = graphics.qso_rates_graph(size, qsos_per_hour) + graphics.save_image(image_data, image_size, 'images/qso_rates_graph.png') image_data, image_size = graphics.draw_map(size, qsos_by_section) - # gc.collect() + graphics.save_image(image_data, image_size, 'images/qsos_map.png') + gc.collect() - image = pygame.image.frombuffer(image_data, image_size, 'RGB') graphics.show_graph(screen, size, image) pygame.display.flip() diff --git a/requirements.txt b/requirements.txt index baa06c0..e95ca00 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ -numpy~=1.26 matplotlib~=3.9 Cartopy~=0.23 -pygame~=2.5 +pygame~=2.6