Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion smibhid/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Press the space_open or space_closed buttons to call the smib server endpoint ap
- Regularly polls for space state (polling period configurable in config.py) and updates the SMIBHID status appropriately to sync with other space state controls
- Flashes both space state LEDs at 2Hz if space state cannot be determined
- 2x16 character LCD display support
- Error information shown on connected displays where configured in modules using ErrorHandler class

## Circuit diagram
### Pico W Connections
Expand Down Expand Up @@ -64,7 +65,7 @@ Set the LOG_LEVEL value in config.py for global log level output configuration w

Example: `LOG_LEVEL = 2`

#### Handlers
#### Log Handlers
Populate the LOG_HANDLERS list in config.py with zero or more of the following log output handlers (case sensitive): "Console", "File"

Example: `LOG_HANDLERS = ["Console", "File"]`
Expand All @@ -74,6 +75,9 @@ Set the LOG_FILE_MAX_SIZE value in config.py to set the maximum size of the log

Example: `LOG_FILE_MAX_SIZE = 10240`

### Error handling
Create a new instance of the ErrorHandling class in a module to register a list of possible errors for that module and enabled or disable them for display on connected screens using class methods. See the space state module for an example of implementation.

### Adding functionality
Refer to the [S.M.I.B. contribution guidelines](https://github.com/somakeit/S.M.I.B./contribute) for more info on contributing.

Expand Down
31 changes: 26 additions & 5 deletions smibhid/lib/LCD1602.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from ulogging import uLogger
from display import driver_registry
from config import SDA_PIN, SCL_PIN, I2C_ID
from asyncio import sleep as async_sleep, create_task

#Device I2C address
LCD_ADDRESS = (0x7c>>1)
Expand Down Expand Up @@ -55,6 +56,7 @@ def __init__(self) -> None:
self.log.info("Init LCD1602 display driver")
self._row = 16
self._col = 2
self.error_loop_task = None

try:
self.LCD1602_I2C = I2C(I2C_ID, sda = SDA_PIN, scl = SCL_PIN, freq = 400000)
Expand Down Expand Up @@ -113,11 +115,30 @@ def _display(self) -> None:
self._showcontrol |= LCD_DISPLAYON
self._command(LCD_DISPLAYCONTROL | self._showcontrol)

def print_space_state(self, state: str) -> None:
"""Abstraction for space state formatting and placement."""
self.print_on_line(0, "S.M.I.B.H.I.D.")
self.print_on_line(1, f"Space: {state}")

def update_status(self, status: dict) -> None:
"""Render state and error information on LCD display."""
self.log.info("Updating display status on LCD1602")
self.errors = status["errors"]
state_line = 0
self.log.info(f"Length of errors dict: {len(self.errors)}")
if len(self.errors) == 0:
self.log.info("No errors in status update")
self.print_on_line(0, "S.M.I.B.H.I.D.")
state_line = 1

self.print_on_line(state_line, f"State: {status["state"]}")

if self.error_loop_task == None or self.error_loop_task.done():
self.error_loop_task = create_task(self.async_error_printing_loop())

async def async_error_printing_loop(self) -> None:
while True:
for error in self.errors:
self.log.info(f"Printing error: {error}")
self.print_on_line(1, f"Err: {error}")
await async_sleep(2)
await async_sleep(0.1)

def _begin(self, lines: int) -> None:
"""Configure and set initial display output."""
if (lines > 1):
Expand Down
18 changes: 15 additions & 3 deletions smibhid/lib/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ def __init__(self) -> None:
self.enabled = False
self.screens = []
self._load_configured_drivers()
self.state = "Unknown"
self.errors = {}

def _load_configured_drivers(self) -> None:
for driver in self.drivers:
Expand All @@ -40,6 +42,7 @@ def _load_configured_drivers(self) -> None:
self.enabled = False

def _execute_command(self, command: str, *args) -> None:
self.log.info(f"Executing command on screen drivers: {command}, with arguments: {args}")
for screen in self.screens:
if hasattr(screen, command):
method = getattr(screen, command)
Expand All @@ -54,6 +57,15 @@ def print_startup(self, version: str) -> None:
"""Display startup information on all screens."""
self._execute_command("print_startup", version)

def print_space_state(self, state: str) -> None:
"""Update space state information on all screens."""
self._execute_command("print_space_state", state)
def _update_status(self) -> None:
"""Update state and error information on all screens."""
self.log.info("Updating status on all screens")
self._execute_command("update_status", {"state": self.state, "errors": self.errors})

def update_state(self, state: str) -> None:
self.state = state
self._update_status()

def update_errors(self, errors: list) -> None:
self.errors = errors
self._update_status()
85 changes: 85 additions & 0 deletions smibhid/lib/error_handling.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
from display import Display
from ulogging import uLogger

class ErrorHandler: # TODO add pytests for this class
"""
Register a module for error handling and provide methods for registering, enabling, disabling, and getting error messages.
If a display is available, ensure your display handling module registers the display instance with the error handler using configure_display().
The error handler will then ensure the display update status method is called when errors are enabled or disabled, passing in all enabled errors.
"""

error_handler_registry = {}

@classmethod
def register_error_handler(cls, error_handler_name: str, error_handler_instance) -> None:
cls.error_handler_registry[error_handler_name] = error_handler_instance

@classmethod
def get_error_handler_class(cls, error_handler_name: str) -> None:
return cls.error_handler_registry.get(error_handler_name)

@classmethod
def configure_display(cls, display: Display) -> None:
cls.display = display

@classmethod
def update_errors_on_display(cls) -> None:
errors = []
for error_handler in cls.error_handler_registry:
errors.extend(cls.error_handler_registry[error_handler].get_all_errors())
cls.display.update_errors(errors)

def __init__(self, module_name: str) -> None:
"""Creates a new error handler instance for a module and registers it with the error handler registry."""
self.log = uLogger(f"ErrorHandling - {module_name}")
self.errors = {}
self.register_error_handler(module_name, self)

def register_error(self, key: str, message: str):
"""Register a new error with its key, message, and enabled status."""
if key not in self.errors:
self.errors[key] = {'message': message, 'enabled': False}
self.log.info(f"Registered error '{key}' with message '{message}'")
else:
raise ValueError(f"Error key '{key}' already registered.")

def enable_error(self, key: str):
"""Enable an error."""
if key in self.errors:
self.errors[key]['enabled'] = True
self.log.info(f"Enabled error '{key}'")
self.update_errors_on_display()
else:
raise ValueError(f"Error key '{key}' not registered.")

def disable_error(self, key: str):
"""Disable an error."""
if key in self.errors:
self.errors[key]['enabled'] = False
self.log.info(f"Disabled error '{key}'")
self.update_errors_on_display()
else:
raise ValueError(f"Error key '{key}' not registered.")

def get_error_message(self, key: str) -> str:
"""Get the error message for a given key."""
if key in self.errors:
return self.errors[key]['message']
else:
raise ValueError(f"Error key '{key}' not registered.")

def is_error_enabled(self, key: str) -> bool:
"""Check if an error is enabled."""
if key in self.errors:
return self.errors[key]['enabled']
else:
raise ValueError(f"Error key '{key}' not registered.")

def get_all_errors(self) -> list:
"""Return a list of all enabled errors."""
errors = []
for error in self.errors:
if self.errors[error]['enabled']:
errors.append(self.errors[error]['message'])
return errors

10 changes: 6 additions & 4 deletions smibhid/lib/hid.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from ulogging import uLogger
from asyncio import get_event_loop
from asyncio import get_event_loop, Event, create_task
from slack_api import Wrapper
from display import Display
from space_state import SpaceState
from error_handling import ErrorHandler

class HID:

Expand All @@ -16,7 +17,9 @@ def __init__(self) -> None:
self.slack_api = Wrapper()
self.loop_running = False
self.display = Display()
self.spaceState = SpaceState()
self.spaceState = SpaceState(self.display)
self.errorHandler = ErrorHandler("HID")
self.errorHandler.configure_display(self.display)

def startup(self) -> None:
"""
Expand All @@ -26,10 +29,9 @@ def startup(self) -> None:
self.log.info(f"SMIBHID firmware version: {self.version}")
self.display.clear()
self.display.print_startup(self.version)
self.log.info("Starting network monitor")
self.spaceState.startup()

self.log.info("Entering main loop")
self.loop_running = True
loop = get_event_loop()
loop.run_forever()
loop.run_forever()
6 changes: 3 additions & 3 deletions smibhid/lib/networking.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import config
from lib.ulogging import uLogger
from lib.utils import StatusLED
import uasyncio
from asyncio import sleep, Event

class WirelessNetwork:

Expand Down Expand Up @@ -58,7 +58,7 @@ def dump_status(self):

async def wait_status(self, expected_status, *, timeout=config.WIFI_CONNECT_TIMEOUT_SECONDS, tick_sleep=0.5) -> bool:
for unused in range(ceil(timeout / tick_sleep)):
await uasyncio.sleep(tick_sleep)
await sleep(tick_sleep)
status = self.dump_status()
if status == expected_status:
return True
Expand Down Expand Up @@ -143,7 +143,7 @@ async def check_network_access(self) -> bool:
async def network_monitor(self) -> None:
while True:
await self.check_network_access()
await uasyncio.sleep(5)
await sleep(5)

def get_mac(self) -> str:
return self.mac
Expand Down
49 changes: 38 additions & 11 deletions smibhid/lib/space_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,17 @@
from constants import OPEN, CLOSED
from display import Display
from slack_api import Wrapper
from error_handling import ErrorHandler

class SpaceState:
def __init__(self) -> None:
def __init__(self, display: Display) -> None:
"""
Pass an asyncio event object to error_event and use a coroutine to
monitor for event triggers to handle errors in space state checking
by querying the is_in_error_state attribute.
"""
self.log = uLogger("SpaceState")
self.display = Display()
self.display = display
self.slack_api = Wrapper()
self.space_open_button_event = Event()
self.space_closed_button_event = Event()
Expand All @@ -21,13 +27,25 @@ def __init__(self) -> None:
self.space_open_led.off()
self.space_closed_led.off()
self.space_state = None
self.space_state_check_in_error_state = False
self.checking_space_state = False
self.checking_space_state_timeout_s = 30
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.configure_error_handling()

def configure_error_handling(self) -> None:
self.error_handler = ErrorHandler("SpaceState")
self.errors = {
"API": "Slow API",
"CHK": "State check"
# "API": "The space state API is taking too long to respond.", needs scrolling feature on lcd1602
# "CHK": "An error occurred while checking the space state."
}

for error_key, error_message in self.errors.items():
self.error_handler.register_error(error_key, error_message)

def startup(self) -> None:
self.log.info(f"Starting {self.open_button.get_name()} button watcher")
create_task(self.open_button.wait_for_press())
Expand All @@ -49,39 +67,38 @@ def set_output_space_open(self) -> None:
self.space_state = True
self.space_open_led.on()
self.space_closed_led.off()
self.display.print_space_state("Open")
self.display.update_state("Open")
self.log.info("Space state is open.")

def set_output_space_closed(self) -> None:
"""Set LED's display etc to show the space as closed"""
self.space_state = False
self.space_open_led.off()
self.space_closed_led.on()
self.display.print_space_state("Closed")
self.display.update_state("Closed")
self.log.info("Space state is closed.")

def set_output_space_none(self) -> None:
"""Set LED's display etc to show the space as none"""
self.space_state = None
self.space_open_led.off()
self.space_closed_led.off()
self.display.print_space_state("None")
self.display.update_state("None")
self.log.info("Space state is none.")

def _set_space_state_check_to_error(self) -> None:
"""Activities relating to space_state check moving to error state"""
self.log.info("Space state check has errored.")
if not self.space_state_check_in_error_state:
self.space_state_check_in_error_state = True
if not self.error_handler.is_error_enabled("CHK"):
self.error_handler.enable_error("CHK")
self.state_check_error_open_led_flash_task = create_task(self.space_open_led.async_constant_flash(2))
self.state_check_error_closed_led_flash_task = create_task(self.space_closed_led.async_constant_flash(2))
self.display.print_space_state("Error")

def _set_space_state_check_to_ok(self) -> None:
"""Activities relating to space_state check moving to ok state"""
self.log.info("Space state check status error has cleared")
if self.space_state_check_in_error_state:
self.space_state_check_in_error_state = False
if self.error_handler.is_error_enabled("CHK"):
self.error_handler.disable_error("CHK")
self.state_check_error_open_led_flash_task.cancel()
self.state_check_error_closed_led_flash_task.cancel()
self.space_open_led.off()
Expand All @@ -93,10 +110,14 @@ def _free_to_check_space_state(self) -> bool:
self.log.info("Checking space state check state")
if self.checking_space_state:
self.log.warn("Already checking space state")
if not self.error_handler.is_error_enabled("API"):
self.error_handler.enable_error("API")
return False
else:
self.log.info("Free to check space state")
self.checking_space_state = True
if self.error_handler.is_error_enabled("API"):
self.error_handler.disable_error("API")
return True

def _set_space_output(self, new_space_state: bool | None) -> None:
Expand Down Expand Up @@ -189,3 +210,9 @@ async def task_wrapper_for_error_handling():
self.log.error(f"State poller encountered an error creating task: {e}")
finally:
await sleep(self.space_state_poll_frequency)

def set_error_id(self, error_id: int) -> None:
self.error_id = error_id

def get_error_id(self) -> int:
return self.error_id