Skip to content

Commit 5e67e5e

Browse files
authored
Convert to reading config from an INI file (n1kdo#68)
* Added images to prevent new logos from showing as a changed file Added ini file for new config support Added variations of .db names * Initial deploy of config module. Note thius does yet work as I need to move to my Pi. I cannot run this on my Mac. * This allows the code to work. I added a Singleton to the config object so init just runs once to load the files. I removed the parameter of config.ini until I figure out how to add the parameter to the invocation of the class with the Singleton I updated a few items that did not have config. prior to the field like EVENT_TIME and QTH_LONGITUDE I updated code in headless.py to not try and save the images if no data is returned. This happens when there are no QSOS. I updated many logging statementas to put the module in the name (I Have not looked to see if there is a way for force logging to do that on its own) * Updated the rest of the config.py Config class to read all the values from the ini file Changed the format of the log statement to add module::function Removed the modules labels I added to dataaccess and headless and graphics before I realized I could add it to the log formatter. Made the config object check the format of the date passed. It raises an exceptiona nd exits if this is not set properly. Note the Trackback makes it really hard to see the error Last thing is to add the ini file name back as a parameter * This file is ignored but I am trying to see if I can do an add, then not have changes tracked. * Added a comment to the logformatter in constants.py toindicate this is called first and therefore sets the format for all subsequent logging * Create config.ini * Update config.ini * Update .gitignore * Delete config.ini * Create config-sample.ini * Update INSTALL_RASPI.md Add comment about config-sample.ini * Update rpi_install.sh Copied the sample config file to its target directory. * Updated config.py to add back Headless parameter as I omitted it. * Update INSTALL_RASPI.md * Update INSTALL_RASPI.md Removed the reference to MyClubCall. I could not find an occurrence of that any more. * Added updates to handle logging dynamically to set the log level from ini file Added code to check for a valid ini file ane report where it was found Added code to list th epossible places the ini file can be if not found or multiples found * Updated init script output to go to the syslog file * Final update ready for Pull Request to move config info to n1mm_view.ini Note the ini file can be in ONLY one of the following directories: ~/.config/n1mm_view.ini ~/n1mm_view.ini ~/n1mm_view/n1mm_view.ini There is error handling if it is in more than one place If one is a developer and wants to use git to pull the latest, I would recommend NOT putting the config file in the script directory. I put it in the pi user's ~ dir I also put the logo in the homedir (I added a new config that defaults to logo.png) and I set it to my logo in the config directory I also fixed an issue I saw in testing with N1MM where it sent a PSK QSO as a mode of PSK rather than PSK31 so I added that as a new digital mode Let me know how you like this one and let me know if anything needs to be reworked. * MAkes headless executable
1 parent dff1e4b commit 5e67e5e

13 files changed

+265
-127
lines changed

.gitignore

+5
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,8 @@
22
.idea/
33
*.s3db*
44
n1mm_view.db
5+
config.ini
6+
*.png
7+
*.jpg
8+
*.db.*
9+
*.db

INSTALL_RASPI.md

+9-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,15 @@ 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-
Edit the config.py file with your operating settings. "MyClubCall" is a placeholder. For testing, make the event start time before and the event end time after your current clock time. Other options in the configuration file are helpfully commented.
21+
Copy the file config-sample.ini to ~/.config/n1mm_view.ini.
22+
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.
24+
Use the following command if so desired:
25+
```
26+
sudo ln -s /home/pi/.config/n1mm_view.ini /root/.config/n1mm_view.ini
27+
```
28+
29+
Edit the config file with your operating settings. For testing, make the event start time before and the event end time after your current clock time. Other options in the configuration file are helpfully commented.
2230

2331
# Running N1MM_view
2432
There are two programs of interest. You will want to open a separate terminal window for each.

collector.py

+2-6
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,18 @@
1313
import time
1414
import xml.parsers.expat
1515

16-
import config
16+
from config import Config
1717
import dataaccess
1818

1919
__author__ = 'Jeffrey B. Otterson, N1KDO'
2020
__copyright__ = 'Copyright 2016, 2017, 2019, 2024 Jeffrey B. Otterson'
2121
__license__ = 'Simplified BSD'
2222

23+
config = Config()
2324
BROADCAST_BUF_SIZE = 2048
2425

2526
run = True
2627

27-
logging.basicConfig(format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s', datefmt='%Y-%m-%d %H:%M:%S',
28-
level=config.LOG_LEVEL)
29-
logging.Formatter.converter = time.gmtime
30-
31-
3228
class Operators:
3329
operators = {}
3430
db = None

config-sample.ini

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
[GLOBAL]
2+
DATABASE_FILENAME = n1mm_view.db
3+
DISPLAY_DWELL_TIME = 6
4+
DATA_DWELL_TIME = 60
5+
LOG_LEVEL = INFO
6+
7+
[EVENT INFO]
8+
NAME = Field Day
9+
START_TIME = 2025-01-25 15:00:00
10+
END_TIME = 2025-01-26 20:59:59
11+
QTH_LATITUDE = 27.9837941202094249
12+
QTH_LONGITUDE = -82.74670114956339
13+
14+
[N1MM INFO]
15+
BROADCAST_PORT = 12060
16+
BROADCAST_ADDRESS = 192.168.1.255
17+
LOG_FILE_NAME = FD2024-N4N.s3db
18+
19+
[HEADLESS INFO]
20+
; 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.
21+
; A sample value could be /mnt/ramdisk/n1mm_view/html
22+
IMAGE_DIR = None
23+
24+
; The POST_FILE_COMMAND is used is to execute this command. You can use it to call rsync or a script.
25+
#POST_FILE_COMMAND = rsync -avz /mnt/ramdisk/n1mm_view/html/* user@sshserver:www/n1mm_view/html
26+
27+
[FONT INFO]
28+
# If font seems too big, try 60 for VIEW_FONT and 100 for BIGGER_FONT
29+
VIEW_FONT = 64
30+
BIGGER_FONT = 180

config.py

+122-49
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,126 @@
1+
#!/usr/bin/python3
2+
13
"""
2-
this file contains configuration values for n1mm_view.
3-
This should be the only thing you would need to customize.
4+
this file is a rewrite of config.py to implement a configParser to keep the config in n1mm_view.ini
45
"""
5-
import datetime
6+
7+
__author__ = 'Tom Schaefer NY4I'
8+
__copyright__ = 'Copyright 2024 Thomas M. Schaefer'
9+
__license__ = 'Simplified BSD'
10+
11+
12+
import configparser
613
import logging
14+
import datetime
15+
import os
16+
import time
717

8-
""" name of database file """
9-
DATABASE_FILENAME = 'n1mm_view.db'
10-
""" Name of the event/contest """
11-
EVENT_NAME = 'N4N Field Day'
12-
""" start time of the event/contest in YYYY-MM-DD hh:mm:ss format """
13-
14-
EVENT_START_TIME = datetime.datetime.strptime('2024-06-22 18:00:00', '%Y-%m-%d %H:%M:%S')
15-
""" end time of the event/contest """
16-
EVENT_END_TIME = datetime.datetime.strptime('2024-06-23 17:59:59', '%Y-%m-%d %H:%M:%S')
17-
""" port number used by N1MM+ for UDP broadcasts This matches the port you set in N1MM Configurator UDP logging """
18-
N1MM_BROADCAST_PORT = 12060
19-
"""
20-
broadcast IP address, used by log re-player.
21-
This could be the IP of the N1MM master, or just the last address in the network segment
22-
"""
23-
N1MM_BROADCAST_ADDRESS = '192.168.1.255'
24-
""" n1mm+ log file name used by replayer """
25-
N1MM_LOG_FILE_NAME = 'FD2024-N4N.s3db'
26-
""" QTH here is the location of your event. We mark this location with a red dot when we generate the map views."""
27-
""" QTH Latitude """
28-
QTH_LATITUDE = 34.0109629
29-
""" QTH Longitude """
30-
QTH_LONGITUDE = -84.4616047
31-
""" number of seconds before automatic display change to the next screen """
32-
DISPLAY_DWELL_TIME = 6
33-
"""
34-
number of seconds before automatic info recalculation from database. Too low makes the Pi work harder.
35-
Too high makes a lag in viewing your results.
36-
"""
37-
DATA_DWELL_TIME = 60
38-
""" log level for apps -- one of logging.WARN, logging.INFO, logging.DEBUG """
39-
LOG_LEVEL = logging.DEBUG
40-
#
41-
"""images directory, or None if not writing image files"""
42-
IMAGE_DIR = None # '/mnt/ramdisk/n1mm_view/html'
43-
44-
""" set HEADLESS True to not open graphics window. This is for using only the Apache option."""
45-
HEADLESS = False
46-
47-
# This should match the directory above we want to send from the directory to which we write.
48-
POST_FILE_COMMAND = 'rsync -avz /mnt/ramdisk/n1mm_view/html/* sparc:www/n1mm_view/html'
49-
50-
""" Font Sizes """
51-
# If font seems too big, try 60 for VIEW_FONT and 100 for BIGGER_FONT
52-
VIEW_FONT = 64
53-
BIGGER_FONT = 180
18+
# Note LOG_FORMATS is here rather than constants.py to avoid a circular import
19+
LOG_FORMAT = '%(asctime)s.%(msecs)03d %(levelname)-8s [%(module)s::%(funcName)s] %(message)s'
20+
21+
22+
23+
BASE_CONFIG_NAME = 'n1mm_view.ini'
24+
CONFIG_NAMES = [ os.path.dirname(__file__) + '/' + BASE_CONFIG_NAME
25+
,os.path.expanduser('~/' + BASE_CONFIG_NAME)
26+
,os.path.expanduser('~/.config/' + BASE_CONFIG_NAME)
27+
]
28+
29+
# Setup logging. This is the first occurrence so this is the only place basicConfig is called
30+
logging.basicConfig( format=LOG_FORMAT, datefmt='%Y-%m-%d %H:%M:%S'
31+
,level=logging.DEBUG # Set to DEBUG so we get all until we grab the value from the config file. THis allows config.py to log before we read the log level
32+
)
33+
logging.Formatter.converter = time.gmtime
34+
class Singleton(type):
35+
def __init__(self, name, bases, mmbs):
36+
super(Singleton, self).__init__(name, bases, mmbs)
37+
self._instance = super(Singleton, self).__call__()
38+
39+
def __call__(self, *args, **kw):
40+
return self._instance
41+
42+
class Config(metaclass = Singleton):
43+
44+
def __init__(self, *args, **kw):
45+
46+
cfg = configparser.ConfigParser()
47+
# Find and read ini file
48+
readCFGName = cfg.read(CONFIG_NAMES)
49+
# Check if there was just one config file found or none at all - Error in both cases so exit
50+
n = len(readCFGName) # Number of config files found
51+
if n > 1:
52+
print ('ConfigParser found more than one config file named %s' % (BASE_CONFIG_NAME))
53+
for s in readCFGName:
54+
print (' Found %s' % (s))
55+
print ('Please use ONLY ONE file named %s in one of the following locations:' % (BASE_CONFIG_NAME))
56+
for s in CONFIG_NAMES:
57+
print (' %s' % (s))
58+
exit ()
59+
elif n == 0:
60+
print ('ConfigParser cannot find a config file named %s' % (BASE_CONFIG_NAME))
61+
print ('Please create ONLY ONE config file named %s in one of the following locations:' % (BASE_CONFIG_NAME))
62+
for s in CONFIG_NAMES:
63+
print (' %s' % (s))
64+
exit ()
65+
66+
67+
68+
# Get logging level set first for subsequent logging...
69+
self.LOG_LEVEL = cfg.get('GLOBAL','LOG_LEVEL',fallback='ERROR')
70+
logging.info('Setting log level to %s' % (self.LOG_LEVEL))
71+
72+
# Note that basicConfig is called again since n1mm_view uses the class methods in logging.
73+
# While there is a setLevel to dynamically set the level, it is not a class function.
74+
# So you have to call basicConfig again with the force parameter True to override the existing one.
75+
# If rather than call the class function, logging was instantiated as logger (accessible to all) then it could just use setLevel, but that is a bigger refactor.
76+
77+
logging.basicConfig( format=LOG_FORMAT, datefmt='%Y-%m-%d %H:%M:%S'
78+
,level=self.LOG_LEVEL
79+
,force = True
80+
)
81+
82+
logging.info ('Reading config file @ %s' % (readCFGName))
83+
84+
self.DATABASE_FILENAME = cfg.get('GLOBAL','DATABASE_FILENAME',fallback='n1mm_view.db')
85+
logging.info ('Using database file %s' % (self.DATABASE_FILENAME))
86+
87+
self.LOGO_FILENAME = cfg.get('GLOBAL','LOGO_FILENAME',fallback='logo.png')
88+
if not os.path.exists(self.LOGO_FILENAME):
89+
logging.error('Logo file %s does not exist' % (self.LOGO_FILENAME))
90+
else:
91+
logging.info ('Using logo file %s' % (self.LOGO_FILENAME))
92+
93+
self.EVENT_NAME = cfg.get('EVENT INFO','NAME')
94+
95+
dt = cfg.get('EVENT INFO','START_TIME')
96+
try:
97+
self.EVENT_START_TIME = datetime.datetime.strptime(dt, '%Y-%m-%d %H:%M:%S')
98+
except (ValueError, TypeError):
99+
logging.exception('*** INVALID START_TIME *** Value for START_TIME (%s) is not valid' % (dt))
100+
exit()
101+
102+
dt = cfg.get('EVENT INFO','END_TIME')
103+
try:
104+
self.EVENT_END_TIME = datetime.datetime.strptime(dt, '%Y-%m-%d %H:%M:%S')
105+
except (ValueError, TypeError):
106+
logging.exception('*** INVALID END_TIME *** Value for END_TIME (%s) is not valid' % (dt))
107+
exit()
108+
109+
self.N1MM_BROADCAST_PORT = cfg.getint('N1MM INFO','BROADCAST_PORT',fallback=12060)
110+
logging.info ('LIstening on UDP port %d' % (self.N1MM_BROADCAST_PORT))
111+
self.N1MM_BROADCAST_ADDRESS = cfg.get('N1MM INFO','BROADCAST_ADDRESS')
112+
self.N1MM_LOG_FILE_NAME = cfg.get('N1MM INFO','LOG_FILE_NAME')
113+
114+
self.QTH_LATITUDE = cfg.getfloat('EVENT INFO','QTH_LATITUDE')
115+
self.QTH_LONGITUDE = cfg.getfloat('EVENT INFO','QTH_LONGITUDE')
116+
self.DISPLAY_DWELL_TIME = cfg.getint('GLOBAL','DISPLAY_DWELL_TIME',fallback=6)
117+
self.DATA_DWELL_TIME = cfg.getint('GLOBAL','DATA_DWELL_TIME',fallback=60)
118+
self.HEADLESS_DWELL_TIME = cfg.getint('GLOBAL','HEADLESS_DWELL_TIME',fallback=180)
119+
120+
121+
122+
self.IMAGE_DIR = cfg.get('HEADLESS INFO','IMAGE_DIR',fallback='/mnt/ramdisk/n1mm_view/html')
123+
self.HEADLESS = cfg.getboolean('HEADLESS INFO','HEADLESS',fallback = False) #False
124+
self.POST_FILE_COMMAND = cfg.get('HEADLESS INFO','POST_FILE_COMMAND', fallback=None)
125+
self.VIEW_FONT = cfg.getint('FONT INFO','VIEW_FONT',fallback=64)
126+
self.BIGGER_FONT = cfg.getint('FONT INFO','BIGGER_FONT',fallback=180)

constants.py

+7-7
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@
88

99
import time
1010
import logging
11-
import config
1211

13-
logging.basicConfig(format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s', datefmt='%Y-%m-%d %H:%M:%S',
14-
level=config.LOG_LEVEL)
15-
logging.Formatter.converter = time.gmtime
12+
from config import Config
1613

14+
config = Config()
15+
16+
logging.info('Starting constants.py')
1717

1818
class Bands:
1919
"""
@@ -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', 'PSK31', 'PSK63', 'FT8', 'FT4']
41+
MODES_LIST = ['N/A', 'CW', 'AM', 'FM', 'LSB', 'USB', 'RTTY', 'PSK', 'PSK31', 'PSK63', 'FT8', 'FT4']
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]
48+
MODE_TO_SIMPLE_MODE = [0, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3]
4949
SIMPLE_MODE_POINTS = [0, 2, 1, 2] # n/a, CW, phone, digital
5050
SIMPLE_MODES = {'N/A': 0, 'CW': 1,
5151
'AM': 2, 'FM': 2, 'LSB': 2, 'USB': 2,
52-
'RTTY': 3, 'PSK31': 3, 'PSK63': 3, 'FT8': 3, 'FT4': 3, 'MFSK': 3,
52+
'RTTY': 3, 'PSK': 3, 'PSK31': 3, 'PSK63': 3, 'FT8': 3, 'FT4': 3, 'MFSK': 3,
5353
}
5454

5555
@classmethod

dashboard.py

+4-7
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,16 @@
1414
import sys
1515
import time
1616

17-
import config
17+
from config import Config
1818
import dataaccess
1919
import graphics
2020

2121
__author__ = 'Jeffrey B. Otterson, N1KDO'
2222
__copyright__ = 'Copyright 2016, 2017, 2019 Jeffrey B. Otterson'
2323
__license__ = 'Simplified BSD'
2424

25+
config = Config()
26+
2527
LOGO_IMAGE_INDEX = 0
2628
QSO_COUNTS_TABLE_INDEX = 1
2729
QSO_RATES_TABLE_INDEX = 2
@@ -41,11 +43,6 @@
4143
IMAGE_FORMAT = 'RGB'
4244
SAVE_PNG = False
4345

44-
logging.basicConfig(format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s', datefmt='%Y-%m-%d %H:%M:%S',
45-
level=config.LOG_LEVEL)
46-
logging.Formatter.converter = time.gmtime
47-
48-
4946
def load_data(size, q, last_qso_timestamp):
5047
"""
5148
load data from the database tables
@@ -335,7 +332,7 @@ def main():
335332

336333
logging.debug('display setup')
337334

338-
images[LOGO_IMAGE_INDEX] = pygame.image.load('logo.png')
335+
images[LOGO_IMAGE_INDEX] = pygame.image.load(config.LOGO_FILENAME)
339336
crawl_messages = CrawlMessages(screen, size)
340337
update_crawl_message(crawl_messages)
341338

0 commit comments

Comments
 (0)