From e3a90e50e78f493422360e3c823fcdacdd3d0946 Mon Sep 17 00:00:00 2001 From: Stephen Jefferson Date: Thu, 10 Oct 2024 19:59:33 +0100 Subject: [PATCH 1/4] Fix naming to convention on constant definition --- smibhid/config.py | 2 +- smibhid/lib/space_state.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/smibhid/config.py b/smibhid/config.py index e23542c..d617d26 100644 --- a/smibhid/config.py +++ b/smibhid/config.py @@ -33,7 +33,7 @@ ## Space state # Set the space state poll frequency in seconds (>= 5), set to 0 to disable the state poll -space_state_poll_frequency_s = 5 +SPACE_STATE_POLL_FREQUENCY_S = 5 # How long to wait for button press to accept extra hours when opening space ADD_HOURS_INPUT_TIMEOUT = 3 diff --git a/smibhid/lib/space_state.py b/smibhid/lib/space_state.py index ddcc85e..6dbc560 100644 --- a/smibhid/lib/space_state.py +++ b/smibhid/lib/space_state.py @@ -57,7 +57,7 @@ def __init__(self, module_config: ModuleConfig, hid: object) -> None: self.space_state = None self.checking_space_state = False self.checking_space_state_timeout_s = 30 - self.space_state_poll_frequency = config.space_state_poll_frequency_s + self.space_state_poll_frequency = config.SPACE_STATE_POLL_FREQUENCY_S if self.space_state_poll_frequency != 0 and self.space_state_poll_frequency < 5: self.space_state_poll_frequency = 5 self.state_check_error_open_led_flash_task = None From 24c829cd2716cd0e61cfcf575f05afa545ede617 Mon Sep 17 00:00:00 2001 From: Stephen Jefferson Date: Thu, 10 Oct 2024 20:00:05 +0100 Subject: [PATCH 2/4] Don't crash error handler enable error if no display present --- smibhid/lib/error_handling.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/smibhid/lib/error_handling.py b/smibhid/lib/error_handling.py index bbcfeba..fe0a09c 100644 --- a/smibhid/lib/error_handling.py +++ b/smibhid/lib/error_handling.py @@ -48,7 +48,8 @@ def enable_error(self, key: str): if key in self.errors: self.errors[key]['enabled'] = True self.log.info(f"Enabled error '{key}'") - self.update_errors_on_display() + if hasattr(self, 'display'): + self.update_errors_on_display() else: raise ValueError(f"Error key '{key}' not registered.") From 224bfc5365fe07ae859a293d8bc2d4a2b73541d1 Mon Sep 17 00:00:00 2001 From: Stephen Jefferson Date: Thu, 10 Oct 2024 22:19:19 +0100 Subject: [PATCH 3/4] Add config management to identify, remediate and throw errors for incorrect configuration. This closes #239 --- smibhid/README.md | 6 +++ smibhid/lib/config/config_management.py | 54 +++++++++++++++++++++++ smibhid/lib/config/config_template.py | 58 +++++++++++++++++++++++++ smibhid/main.py | 7 +++ 4 files changed, 125 insertions(+) create mode 100644 smibhid/lib/config/config_management.py create mode 100644 smibhid/lib/config/config_template.py diff --git a/smibhid/README.md b/smibhid/README.md index bb2d3a4..5f5f845 100644 --- a/smibhid/README.md +++ b/smibhid/README.md @@ -19,6 +19,7 @@ Press the space_open or space_closed buttons to call the smib server endpoint ap - Error information shown on connected displays where configured in modules using ErrorHandler class - UI Logger captures timestamps of button presses and uploads to SMIB for logging and review of usage patterns - Space open relay pin optionally sets a GPIO to high or low when the space is open +- Config file checker against config template - useful for upgrades missing config of new features ## Circuit diagram ### Pico W Connections @@ -53,6 +54,8 @@ Copy the files from the smibhib folder into the root of a Pico W running Micropy - Set the space state poll frequency in seconds (>= 5), set to 0 to disable the state poll - Configure the space open relay pin if required or else set to None, also choose if space open sets pin high or low +If you miss any configuration options, a default will be applied, an error output in the log detailing the configuration item missed including the default value configured and if connected, an error displayed on displays. + ## Onboard status LED The LED on the Pico W board is used to give feedback around network connectivity if you are not able to connect to the terminal output for logs. * 1 flash at 2 Hz: successful connection @@ -101,6 +104,9 @@ Use existing space state buttons, lights, slack API wrapper and watchers as an e - Enter a new UI state by calling the transition_to() method on a UIstate instance and pass any arguments needed by that state - You will need to pass any core objects needed by the base UIState class and apply using super() as normal. These are currently HID (for managing the current state instance) and SpaceState so that the open and close buttons are available in all UIs with default space open/closed behaviour. +#### Config template update +If you add a new feature that has configuration options, ensure you set the constant and default value in the config.py file as well as in the config.config_template.py file to allow automated checking of config files to catch upgrade error and misconfigurations. + ### UI State diagram The space state UI state machine is described in this diagram: diff --git a/smibhid/lib/config/config_management.py b/smibhid/lib/config/config_management.py new file mode 100644 index 0000000..7459284 --- /dev/null +++ b/smibhid/lib/config/config_management.py @@ -0,0 +1,54 @@ +from ulogging import uLogger +import lib.config.config_template as config_template +import config + +class ConfigManagement: + """ + Ensure config file contains all items in config_template and that the values are of the correct type. + Apply defaults from config_template where values are missing in config. + """ + def __init__(self) -> None: + self.log = uLogger("ConfigManagement") + self.error_count = 0 + + def configure_error_handling(self) -> None: + """ + Register errors with the error handler for the configuration management module. + """ + from lib.error_handling import ErrorHandler + self.error_handler = ErrorHandler("ConfigManagement") + self.errors = { + "CFG": "Config errors found, check logs" + } + + for error_key, error_message in self.errors.items(): + self.error_handler.register_error(error_key, error_message) + + return + + def check_config(self) -> int: + self.log.info("Checking config file") + + for key in dir(config_template): + if not hasattr(config, key): + self.log.error(f"Missing config item in config.py: {key}, setting default value: {getattr(config_template, key)}") + setattr(config, key, getattr(config_template, key)) + self.error_count += 1 + else: + if type(getattr(config_template, key)) != type(getattr(config, key)) and type(getattr(config_template, key)) != type(None): + self.log.error(f"Config item in config.py has incorrect type: {key}") + setattr(config, key, getattr(config_template, key)) + self.error_count += 1 + + self.log.info(f"Config file check complete. Errors encountered: {self.error_count}") + + if self.error_count > 0: + self.log.error("Please correct the above errors in config.py and restart SMIBHID") + return -1 + + return 0 + + def enable_error(self) -> None: + self.error_handler.enable_error("CFG") + + return diff --git a/smibhid/lib/config/config_template.py b/smibhid/lib/config/config_template.py new file mode 100644 index 0000000..d617d26 --- /dev/null +++ b/smibhid/lib/config/config_template.py @@ -0,0 +1,58 @@ +## Logging +# Level 0-4: 0 = Disabled, 1 = Critical, 2 = Error, 3 = Warning, 4 = Info +LOG_LEVEL = 2 +# Handlers: Populate list with zero or more of the following log output handlers (case sensitive): "Console", "File" +LOG_HANDLERS = ["Console", "File"] +# Max log file size in bytes, there will be a maximum of 2 files at this size created +LOG_FILE_MAX_SIZE = 10240 + +## IO +SPACE_OPEN_BUTTON = 12 +SPACE_CLOSED_BUTTON = 13 +SPACE_OPEN_LED = 15 +SPACE_CLOSED_LED = 16 +# Set to None if no relay/transistor is connected +SPACE_OPEN_RELAY = None +SPACE_OPEN_RELAY_ACTIVE_HIGH = True + +## WIFI +WIFI_SSID = "" +WIFI_PASSWORD = "" +WIFI_COUNTRY = "GB" +WIFI_CONNECT_TIMEOUT_SECONDS = 10 +WIFI_CONNECT_RETRIES = 1 +WIFI_RETRY_BACKOFF_SECONDS = 5 +# Leave as none for MAC based unique hostname or specify a custom hostname string +CUSTOM_HOSTNAME = None + +NTP_SYNC_INTERVAL_SECONDS = 86400 + +## Web host +WEBSERVER_HOST = "" +WEBSERVER_PORT = "80" + +## Space state +# Set the space state poll frequency in seconds (>= 5), set to 0 to disable the state poll +SPACE_STATE_POLL_FREQUENCY_S = 5 +# How long to wait for button press to accept extra hours when opening space +ADD_HOURS_INPUT_TIMEOUT = 3 + +## I2C +SDA_PIN = 8 +SCL_PIN = 9 +I2C_ID = 0 + +## Displays - Populate driver list with connected displays from this supported list: ["LCD1602"] +DISPLAY_DRIVERS = ["LCD1602"] +# Scroll speed for text on displays in characters per second +SCROLL_SPEED = 4 + +## RFID reader +RFID_ENABLED = False +RFID_SCK = 18 +RFID_MOSI = 19 +RFID_MISO = 16 +RFID_RST = 21 +RFID_CS = 17 + +ENABLE_UI_LOGGING_UPLOAD = False diff --git a/smibhid/main.py b/smibhid/main.py index 83f1484..a85bb86 100644 --- a/smibhid/main.py +++ b/smibhid/main.py @@ -4,7 +4,14 @@ """ # Built against Pico W Micropython firmware v1.22.2 https://micropython.org/download/RPI_PICO_W/ +from lib.config.config_management import ConfigManagement +config_management = ConfigManagement() +config_errors = config_management.check_config() + from lib.hid import HID hid = HID() +if config_errors < 0: + config_management.configure_error_handling() + config_management.enable_error() hid.startup() From e62600bdb2cdb9f9caefa40881baee7ef2867181 Mon Sep 17 00:00:00 2001 From: Stephen Jefferson Date: Wed, 23 Oct 2024 20:52:49 +0100 Subject: [PATCH 4/4] Made it more pythonic --- smibhid/lib/config/config_management.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smibhid/lib/config/config_management.py b/smibhid/lib/config/config_management.py index 7459284..750fe05 100644 --- a/smibhid/lib/config/config_management.py +++ b/smibhid/lib/config/config_management.py @@ -35,7 +35,7 @@ def check_config(self) -> int: setattr(config, key, getattr(config_template, key)) self.error_count += 1 else: - if type(getattr(config_template, key)) != type(getattr(config, key)) and type(getattr(config_template, key)) != type(None): + if not isinstance(getattr(config, key), type(getattr(config_template, key))) and getattr(config_template, key) is not None: self.log.error(f"Config item in config.py has incorrect type: {key}") setattr(config, key, getattr(config_template, key)) self.error_count += 1