Skip to content

Commit ed13c38

Browse files
authored
Added new table of recent QSOs and more cleanup of the INI file plus a few others. (n1kdo#69)
* Added a chart that shows the last 10 worked contacts. This is in headless only. A future change is to set which charts are created in the config n1mm_view.ini file. I also updated the init files to output StandardOutput and StandareError to journal as syslog is out of favor nowadays (I just learned that...) Constants also have some mode changes to add potential bad modes if the source programhas an issue. Specifically, I made a mode of NoMode set to DATA as in some rare cases, TR4W could not have a sub-mode That usually happens on older radios without explicity sub-modes like PSK or USB-D. I updated INSTALL_RASPI to mention the config file goes in just one place and also to suggest adding a symbolic link to /root/.config as I run dashboard with sudo I updated rpi_install.sh to copy the ini file to the ./config directory. * Fixed a typo in headless (was missing exception handler). Removed config.sample file as the new file is now named n1mm_view.ini.sample * Added the HEADLESS DWELL parameter * Add a CREATE INDEX to index qso_log by timestamp for the last 10 qso table. This should be useful for the last_qso_logic too. * Fixed a typo, an unused module and changed the qso loop. Notes int he original PR * Reformatted the time column in the last 10 QSOs report. Also removed the Station column
1 parent 5e67e5e commit ed13c38

12 files changed

+164
-55
lines changed

INSTALL_RASPI.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ This last command will run for quite some time.
1818
The latest test took about 90 minutes with a good internet connection for a Raspberry Pi 3 B+ (18 minutes on a Pi 4B). We gave it a lot of things to do so you won't need to do them.
1919
While you are waiting, you could create a splash screen for your event: a 1000x1000 portable network graphics (.png) image in RGB format works great.
2020

21-
Copy the file config-sample.ini to ~/.config/n1mm_view.ini.
21+
Copy the file `n1mm_view.ini.sample` to `~/.config/n1mm_view.ini`. Note you can also put the file in your home directory (`~/`) or the script directory. But the file can only be in one of those places--not two or three.
2222

23-
Note to pame it easier if you need to run dashboard.py as sudo, it is useful to create a symbolic link named /root/.config/n1mm_view.ini to point to /home/pi/.config/n1mm_view.ini.
23+
Note to make it easier if you need to run dashboard.py as sudo, it is useful to create a symbolic link named /root/.config/n1mm_view.ini to point to /home/pi/.config/n1mm_view.ini.
2424
Use the following command if so desired:
2525
```
2626
sudo ln -s /home/pi/.config/n1mm_view.ini /root/.config/n1mm_view.ini

config-sample.ini

-30
This file was deleted.

config.py

+1
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ def __init__(self, *args, **kw):
116116
self.DISPLAY_DWELL_TIME = cfg.getint('GLOBAL','DISPLAY_DWELL_TIME',fallback=6)
117117
self.DATA_DWELL_TIME = cfg.getint('GLOBAL','DATA_DWELL_TIME',fallback=60)
118118
self.HEADLESS_DWELL_TIME = cfg.getint('GLOBAL','HEADLESS_DWELL_TIME',fallback=180)
119+
self.SKIP_TIMESTAMP_CHECK = cfg.getboolean('DEBUG','SKIP_TIMESTAMP_CHECK',fallback=False)
119120

120121

121122

constants.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -38,18 +38,18 @@ class Modes:
3838
"""
3939
all the modes that are supported.
4040
"""
41-
MODES_LIST = ['N/A', 'CW', 'AM', 'FM', 'LSB', 'USB', 'RTTY', 'PSK', 'PSK31', 'PSK63', 'FT8', 'FT4']
41+
MODES_LIST = ['N/A', 'CW', 'AM', 'FM', 'LSB', 'USB', 'SSB', 'RTTY', 'PSK', 'PSK31', 'PSK63', 'FT8', 'FT4', 'MFSK', 'NoMode', 'None']
4242
MODES = {elem: index for index, elem in enumerate(MODES_LIST)}
4343

4444
"""
4545
simplified modes for score reporting: CW, PHONE, DATA
4646
"""
4747
SIMPLE_MODES_LIST = ['N/A', 'CW', 'PHONE', 'DATA']
48-
MODE_TO_SIMPLE_MODE = [0, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3]
48+
MODE_TO_SIMPLE_MODE = [0, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 2]
4949
SIMPLE_MODE_POINTS = [0, 2, 1, 2] # n/a, CW, phone, digital
5050
SIMPLE_MODES = {'N/A': 0, 'CW': 1,
51-
'AM': 2, 'FM': 2, 'LSB': 2, 'USB': 2,
52-
'RTTY': 3, 'PSK': 3, 'PSK31': 3, 'PSK63': 3, 'FT8': 3, 'FT4': 3, 'MFSK': 3,
51+
'AM': 2, 'FM': 2, 'LSB': 2, 'USB': 2, 'SSB': 2, 'None': 2,
52+
'RTTY': 3, 'PSK': 3, 'PSK31': 3, 'PSK63': 3, 'FT8': 3, 'FT4': 3, 'MFSK': 3, 'NoMode': 3,
5353
}
5454

5555
@classmethod

dataaccess.py

+34-1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ def create_tables(db, cursor):
5454
cursor.execute('CREATE INDEX IF NOT EXISTS qso_log_station_id ON qso_log(station_id);')
5555
cursor.execute('CREATE INDEX IF NOT EXISTS qso_log_section ON qso_log(section);')
5656
cursor.execute('CREATE INDEX IF NOT EXISTS qso_log_qso_id ON qso_log(qso_id);')
57+
cursor.execute('CREATE INDEX IF NOT EXISTS qso_log_qso_timestamp ON qso_log(timestamp);')
5758
db.commit()
5859

5960

@@ -211,7 +212,8 @@ def get_last_qso(cursor) :
211212
message = 'Last QSO: %s %s %s on %s by %s at %s' % (
212213
row[1], row[2], row[3], constants.Bands.BANDS_TITLE[row[5]], row[4],
213214
datetime.utcfromtimestamp(row[0]).strftime('%H:%M:%S'))
214-
logging.debug('%s' % (message))
215+
logging.debug('%s' % (message))
216+
215217
return last_qso_time, message
216218

217219

@@ -308,3 +310,34 @@ def get_qsos_by_section(cursor):
308310
qsos_by_section[row[0]] = row[1]
309311
logging.debug(f'Section {row[0]} {row[1]}')
310312
return qsos_by_section
313+
314+
def get_last_N_qsos(cursor, nQSOCount):
315+
logging.info('get_last_N_qsos for last %d QSOs' % (nQSOCount))
316+
qsos = []
317+
cursor.execute('SELECT qso_id, timestamp, callsign, band_id, mode_id, operator.name, rx_freq, tx_freq, exchange, section, station.name \n'
318+
'FROM qso_log '
319+
'JOIN operator ON operator.id = operator_id\n'
320+
'JOIN station ON station.id = station_id\n'
321+
'ORDER BY timestamp DESC LIMIT %d;' % (nQSOCount))
322+
for row in cursor:
323+
qsos.append(( row[1] # raw timestamp 0
324+
,row[2] # call 1
325+
,constants.Bands.BANDS_TITLE[row[3]] # band 2
326+
,constants.Modes.SIMPLE_MODES_LIST[constants.Modes.MODE_TO_SIMPLE_MODE[row[4]]] # mode 3
327+
,row[5] # operator callsign 4
328+
,row[8] # exchange 5
329+
,row[9] # section 6
330+
,row[10] # station name 7
331+
))
332+
message = 'QSO: time=%sZ call=%s exchange=%s %s mode=%s band=%s operator=%s station=%s' % (
333+
datetime.utcfromtimestamp(row[1]).strftime('%Y %b %d %H:%M:%S')
334+
,row[2] # callsign
335+
,row[8] # exchange
336+
,row[9] # section
337+
,constants.Modes.SIMPLE_MODES_LIST[constants.Modes.MODE_TO_SIMPLE_MODE[row[4]]]
338+
,constants.Bands.BANDS_TITLE[row[3]]
339+
,row[5] #operator
340+
,row[10] #station
341+
)
342+
logging.info('%s' % (message))
343+
return qsos

graphics.py

+25
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,32 @@ def qso_classes_graph(size, qso_classes):
210210
values.append(d[0])
211211
return make_pie(size, values, labels, "QSOs by Class")
212212

213+
def qso_table(size, qsos):
214+
"""
215+
create the a table of the qso log
216+
"""
217+
if len(qsos) == 0:
218+
return None, (0, 0)
219+
220+
count = 0
221+
cells = [['Time', 'Call', 'Band', 'Mode', 'Operator', 'Section']] #, 'Station']]
222+
223+
for d in qsos[:10]:
224+
cells.append( ['%s' % datetime.datetime.utcfromtimestamp(d[0]).strftime('%m-%d-%y %Tz') # Time
225+
,'%s' % d[1] # Call
226+
,'%s' % d[2] # Band
227+
,'%s' % d[3] # Mode
228+
,'%s' % d[4] # Operator
229+
,'%s' % d[6] # Section
230+
# ,'%s' % d[7] # Station
231+
])
232+
count += 1
213233

234+
if count == 0:
235+
return None, (0, 0)
236+
else:
237+
return draw_table(size, cells, "Last 10 QSOs")
238+
214239
def qso_operators_table(size, qso_operators):
215240
"""
216241
create the Top 5 QSOs by Operators table

headless.py

+31-8
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ def create_images(size, image_dir, last_qso_timestamp):
5050
qsos_per_hour = []
5151
qsos_by_section = {}
5252
qso_classes = []
53+
qsos = []
5354

5455
db = None
5556
data_updated = False
@@ -70,7 +71,9 @@ def create_images(size, image_dir, last_qso_timestamp):
7071
last_qso_time, message = dataaccess.get_last_qso(cursor)
7172

7273
logging.debug('old_timestamp = %s, timestamp = %s' % (last_qso_timestamp, last_qso_time))
73-
if last_qso_time != last_qso_timestamp:
74+
if config.SKIP_TIMESTAMP_CHECK:
75+
logging.warn('Skipping check for a recent QSO - Please just use this for debug - Review SKIP_TIMESTAMP_CHECK in ini file')
76+
if last_qso_time != last_qso_timestamp or config.SKIP_TIMESTAMP_CHECK:
7477
# last_qso_time is passed as the result and updated in call to this function.
7578
logging.debug('data updated!')
7679
data_updated = True
@@ -95,8 +98,11 @@ def create_images(size, image_dir, last_qso_timestamp):
9598

9699
# load QSOs by Section
97100
qsos_by_section = dataaccess.get_qsos_by_section(cursor)
101+
102+
# load last 10 qsos
103+
qsos = dataaccess.get_last_N_qsos(cursor, 10) # Note this returns last 10 qsos in reverse order so oldest is first
98104

99-
logging.debug('load data done')
105+
logging.info('load data done')
100106
except sqlite3.OperationalError as error:
101107
logging.exception(error)
102108
return
@@ -115,78 +121,95 @@ def create_images(size, image_dir, last_qso_timestamp):
115121
graphics.save_image(image_data, image_size, filename)
116122
except Exception as e:
117123
logging.exception(e)
124+
118125
try:
119126
image_data, image_size = graphics.qso_rates_table(size, operator_qso_rates)
120127
if image_data is not None:
121128
filename = makePNGTitle(image_dir, 'qso_rates_table')
122129
graphics.save_image(image_data, image_size, filename)
123130
except Exception as e:
124131
logging.exception(e)
132+
125133
try:
126134
image_data, image_size = graphics.qso_operators_graph(size, qso_operators)
127135
if image_data is not None:
128136
filename = makePNGTitle(image_dir, 'qso_operators_graph')
129137
graphics.save_image(image_data, image_size, filename)
130138
except Exception as e:
131139
logging.exception(e)
140+
132141
try:
133142
image_data, image_size = graphics.qso_operators_table(size, qso_operators)
134143
if image_data is not None:
135144
filename = makePNGTitle(image_dir, 'qso_operators_table')
136145
graphics.save_image(image_data, image_size, filename)
137146
except Exception as e:
138147
logging.exception(e)
148+
139149
try:
140150
image_data, image_size = graphics.qso_operators_table_all(size, qso_operators)
141151
if image_data is not None:
142152
filename = makePNGTitle(image_dir, 'qso_operators_table_all')
143153
graphics.save_image(image_data, image_size, filename)
144154
except Exception as e:
145155
logging.exception(e)
156+
146157
try:
147158
image_data, image_size = graphics.qso_stations_graph(size, qso_stations)
148159
if image_data is not None:
149160
filename = makePNGTitle(image_dir, 'qso_stations_graph')
150161
graphics.save_image(image_data, image_size, filename)
151162
except Exception as e:
152163
logging.exception(e)
164+
153165
try:
154166
image_data, image_size = graphics.qso_bands_graph(size, qso_band_modes)
155167
if image_data is not None:
156168
filename = makePNGTitle(image_dir, 'qso_bands_graph')
157169
graphics.save_image(image_data, image_size, filename)
158170
except Exception as e:
159171
logging.exception(e)
172+
160173
try:
161174
image_data, image_size = graphics.qso_modes_graph(size, qso_band_modes)
162175
if image_data is not None:
163176
filename = makePNGTitle(image_dir, 'qso_modes_graph')
164177
graphics.save_image(image_data, image_size, filename)
165178
except Exception as e:
166179
logging.exception(e)
180+
167181
try:
168182
image_data, image_size = graphics.qso_classes_graph(size, qso_classes)
169183
if image_data is not None:
170184
filename = makePNGTitle(image_dir, 'qso_classes_graph')
171185
graphics.save_image(image_data, image_size, filename)
172186
except Exception as e:
173187
logging.exception(e)
188+
174189
try:
175190
image_data, image_size = graphics.qso_rates_graph(size, qsos_per_hour)
176191
if image_data is not None:
177192
filename = makePNGTitle(image_dir, 'qso_rates_graph')
178193
graphics.save_image(image_data, image_size, filename)
179194
except Exception as e:
180195
logging.exception(e)
196+
197+
try:
198+
image_data, image_size = graphics.qso_table(size, qsos)
199+
if image_data is not None:
200+
filename = makePNGTitle(image_dir, 'last_qso_table')
201+
graphics.save_image(image_data, image_size, filename)
202+
except Exception as e:
203+
logging.exception(e)
181204

182205
# map gets updated every time so grey line moves
183206
try:
184-
# There is a memory leak in the next code -- is there?
185-
image_data, image_size = graphics.draw_map(size, qsos_by_section)
186-
if image_data is not None:
187-
filename = makePNGTitle(image_dir, 'sections_worked_map')
188-
graphics.save_image(image_data, image_size, filename)
189-
gc.collect()
207+
# There is a memory leak in the next code -- is there?
208+
image_data, image_size = graphics.draw_map(size, qsos_by_section)
209+
if image_data is not None:
210+
filename = makePNGTitle(image_dir, 'sections_worked_map')
211+
graphics.save_image(image_data, image_size, filename)
212+
gc.collect()
190213

191214
except Exception as e:
192215
logging.exception(e)

init/n1mm_view_collector.service

+2-2
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ ExecStart=/home/pi/n1mm_view/collector.py
1717
Restart=always
1818
RestartSec=30
1919
StandardInput=tty
20-
StandardOutput=syslog
21-
StandardError=syslog
20+
StandardOutput=journal
21+
StandardError=journal
2222
TTYPath=/dev/tty2
2323
TTYReset=yes
2424
TTYVHangup=yes

init/n1mm_view_dashboard.service

+7-4
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,21 @@
66

77
[Unit]
88
Description=N1MM View Dashboard process
9-
After=gettty@tty3.service
9+
After=gettty@tty5.service
1010

1111
[Service]
1212
User=pi
1313
Group=pi
1414
Type=simple
1515
WorkingDirectory=/home/pi/n1mm_view
16+
17+
# Wait until we startup dashboard
18+
#ExecStartPre=/bin/sleep 30
1619
ExecStart=/home/pi/n1mm_view/dashboard.py
1720
StandardInput=tty
18-
StandardOutput=syslog
19-
StandardError=syslog
20-
TTYPath=/dev/tty3
21+
StandardOutput=journal
22+
StandardError=journal
23+
TTYPath=/dev/tty5
2124
TTYReset=yes
2225
TTYVHangup=yes
2326

init/n1mm_view_headless.service

+2-2
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ ExecStart=/home/pi/n1mm_view/headless.py
1717
Restart=always
1818
RestartSec=30
1919
StandardInput=tty
20-
StandardOutput=syslog
21-
StandardError=syslog
20+
StandardOutput=journal
21+
StandardError=journal
2222
TTYPath=/dev/tty3
2323
TTYReset=yes
2424
TTYVHangup=yes

n1mm_view.ini.sample

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Change this file name to n1mm_view.ini and put it in ONLY one of the following three locations:
2+
# ~/.config/n1mm_view.ini
3+
# ~/n1mm_view.ini
4+
# <script directory>/n1mm_view.ini
5+
# If you have the file in more than one of those locations, the script will abort with an error message.
6+
# This is to avoid multiple ini files making it harder to know where the settings are being read.
7+
8+
# This is where you should set you local options for your site. At a minimum, change the following:
9+
# Under EVENT_INFO,
10+
# set Name to something like <Your CLub Name> Field Day or WInter Field Day
11+
# set the START_TIME and END_TIME
12+
# set you QTH_ coordinates if you want to see a pin on the map
13+
# Optionally, set the LOGO_FILENAME to the proper logo
14+
# For Winter Field Day, wget https://winterfieldday.org/img/wfda_logo.png
15+
# For ARRL Field Day, grab the new logo each year
16+
# If you are using the headless.py script to generate graphics to load to your website,
17+
# modify the POST_FILE_COMMAND to rsync the files or run a script if you want to do multiple steps.
18+
# If you use a script like postCommands.sh, make it executable with chmod +x postCommands.sh
19+
20+
21+
22+
[GLOBAL]
23+
DATABASE_FILENAME = n1mm_view.db
24+
DISPLAY_DWELL_TIME = 6
25+
DATA_DWELL_TIME = 60
26+
HEADLESS_DWELL_TIME = 120
27+
LOG_LEVEL = INFO
28+
LOGO_FILENAME = /home/pi/wfda_logo.png
29+
30+
[EVENT INFO]
31+
NAME = Field Day
32+
START_TIME = 2025-01-25 15:00:00
33+
END_TIME = 2025-01-26 20:59:59
34+
QTH_LATITUDE = 27.9837941202094249
35+
QTH_LONGITUDE = -82.74670114956339
36+
37+
[N1MM INFO]
38+
BROADCAST_PORT = 12060
39+
BROADCAST_ADDRESS = 192.168.1.255
40+
LOG_FILE_NAME = FD2024-N4N.s3db
41+
42+
[HEADLESS INFO]
43+
; Set IMAGE_DIR to None or the name of a directory on the system to write files. Note if using a Pi with an SD card only, use the ramdisk setup in the install process.
44+
; A sample value could be /mnt/ramdisk/n1mm_view/html
45+
IMAGE_DIR = None
46+
47+
; The POST_FILE_COMMAND is used is to execute this command. You can use it to call rsync or a script.
48+
#POST_FILE_COMMAND = rsync -avz /mnt/ramdisk/n1mm_view/html/* user@sshserver:www/n1mm_view/html
49+
50+
[FONT INFO]
51+
# If font seems too big, try 60 for VIEW_FONT and 100 for BIGGER_FONT
52+
VIEW_FONT = 64
53+
BIGGER_FONT = 180

0 commit comments

Comments
 (0)