OzWrapper.OzSamdV2.OzSamdV2.OzSamdV2

class OzWrapper.OzSamdV2.OzSamdV2.OzSamdV2

Bases: SensorBase.GenericSensor

SAMD V2 microcontroller wrapper with firmware update and real-time streaming.

Communicates with the SAMD V2 co-processor over USB-ACM serial to read wind, noise, rain, visible light, surface, UVA, and pyranometer sensors. Supports OTA firmware flashing via bossac and thread-safe serial access with locking. Inherits from SensorBase.SensorBase.GenericSensor and supersedes the v1 wrapper OzWrapper.OzSamd.OzSamd.OzSamd.

Variables:
  • MCP (drivers.MCP230XX.MCP230XX) – MCP230XX GPIO expander used to issue hardware resets to the SAMD board.

  • POLL_CMD (bytes) – AT command bytes used for SAMD liveness check (b"AT").

  • EOL (bytes) – Line terminator for the SAMD serial protocol (b"\r\n").

  • SAMD_VERSION (str) – Expected firmware version string used for compatibility checks ("FW:2.1").

  • firmware_file (str) – Absolute path to the SAMD firmware binary used for OTA flashing.

  • samd_serial (serial.Serial | None) – Open serial connection to the SAMD board, or None when closed.

  • data_type (str) – Identifier string for MQTT payload type tagging ("samd").

  • lasan_port (str) – Detected USB-ACM port for the Lasan/SAMD board.

  • sensor_list (list) – List of successfully initialized sensor driver instances.

  • RUN_DELAY (int | float) – Seconds between continuous sensor read cycles in the background thread.

  • config (dict) – Full SAMD configuration dict captured during initialize() for later re-use.

  • debug_mode (bool) – Whether verbose debug logging is enabled.

  • rdata_flag (bool) – Whether real-time data streaming over MQTT is enabled.

  • mqtt_send_queue (queue.Queue | None) – Queue used to publish real-time data via MQTT.

  • samd_thread (threading.Thread | None) – Daemon thread running run().

  • reboot_manager (utils.reboot_RPi.RebootManager) – Helper used to reboot the Raspberry Pi when the SAMD cannot be recovered.

Example

>>> from queue import Queue
>>> from OzWrapper.OzSamdV2.OzSamdV2 import OzSamdV2
>>> wrapper = OzSamdV2()
>>> wrapper.initialize({"wind": [{"en": True, "pn": 211}]}, {}, Queue())

Initialize OzSamdV2 instance state.

Sets up the MCP230XX GPIO expander (with device number taken from the MCP_ID environment variable), the USB-ACM device detector, default run delay, threading primitives, and the reboot manager. Calls SensorBase.SensorBase.GenericSensor.__init__() first.

Parameters:

None.

Returns:

None.

Raises:

Exception – Propagates any error raised by the MCP230XX driver or the device detector during construction.

Example

>>> from OzWrapper.OzSamdV2.OzSamdV2 import OzSamdV2
>>> wrapper = OzSamdV2()

Note

The constructor does not open the serial port nor start the background thread. Call initialize() to perform those steps.

flashSamdFirmware(firmware_file: str, port: str = 'ttyACM0') bool

Flash SAMD firmware via bossac and verify the result from its output.

Resolves the host CPU architecture (x86_64 or arm), launches the bundled upload.sh shell helper that drives bossac, and scans the captured stdout for two success markers: a 100% progress indicator and a Verify successful line. Only when both markers are present is the flash treated as successful.

Parameters:
  • firmware_file (str) – Absolute path to the .bin firmware image to flash.

  • port (str) – USB-ACM port name (without /dev/) to which the SAMD V2 is attached. Defaults to "ttyACM0".

Returns:

True if both progress and verify markers were found in the upload output, False otherwise.

Return type:

bool

Raises:

Exception – Caught internally; errors are logged via the context logger and the method returns False.

Example

>>> wrapper = OzSamdV2()
>>> wrapper.flashSamdFirmware(
...     "/usr/src/app/OzWrapper/OzSamdV2/firmware.bin",
...     "ttyACM0",
... )
True

Note

The architecture is detected via platform.machine(); armv7l is normalized to arm to match the bundled bossac binaries.

getSensorReading() dict

Stub for the SensorBase.SensorBase.GenericSensor interface.

SAMD V2 readings are produced asynchronously by the run() daemon thread and aggregated synchronously by putSensorValue(), so this method is intentionally a no-op placeholder kept only to satisfy the parent class contract.

Parameters:

None.

Returns:

Always None (implicit), since no synchronous reading is performed here.

Return type:

dict

Raises:

None.

Example

>>> wrapper = OzSamdV2()
>>> wrapper.getSensorReading()

Note

Use putSensorValue() to retrieve aggregated sensor data on demand.

initialize(config: dict, init_value: dict, mqtt_send_queue: queue.Queue) bool

Initialize the SAMD V2 board, flash firmware if needed, and set up sensors.

Captures runtime flags (debug, rdata, ff) from config, re-detects the SAMD USB-ACM port, validates liveness via is_alive(), and triggers an OTA firmware flash via updateSamdFirmware() when the reported firmware does not match SAMD_VERSION or when forced via ff. After firmware checks, iterates each configured sensor category (wind, visible, surface, rain, noise, uva, pyro) and delegates to initializeSensor() for each enabled sensor. Finally starts the background run() daemon thread (only once) and populates init_value via putInitvalues().

Parameters:
  • config (dict) – Configuration dict containing sensor categories and runtime flags such as debug, rdata, ff, and delay.

  • init_value (dict) – Mutable dict that will be populated with per-sensor initialization flags for downstream reporting.

  • mqtt_send_queue (queue.Queue) – Queue used to publish real-time sensor data and SAMD-down event payloads over MQTT.

Returns:

True if at least one sensor initialized successfully, False if the SAMD port could not be opened or no sensor initialized.

Return type:

bool

Raises:

Exception – Caught internally; failures are logged via the context logger and reflected in config["init"].

Example

>>> from queue import Queue
>>> wrapper = OzSamdV2()
>>> wrapper.initialize(
...     {"wind": [{"en": True, "pn": 211}], "rdata": True},
...     {},
...     Queue(),
... )
True

Note

The serial port is closed after the firmware/liveness check and re-opened later by sensor drivers as needed. The background thread is only started if one is not already alive.

initializeSensor(sensor: dict) int

Initialize a single sensor by part number and add it to the sensor list.

Dispatches on the pn (part number) field of sensor to instantiate the appropriate driver (Wind, Visible, Surface, Rain, Noise, ApogeeUva, ApogeePyro) and invokes its initialize method with the shared samd_serial connection. On success, the driver instance is appended to sensor_list.

Parameters:

sensor (dict) – Sensor configuration dict. Must contain a pn key with the supported part number (211/212/213 for wind, 311 for visible, 411 for surface, 61 for rain, 41 for noise, 611 for Apogee UVA, 612 for Apogee pyranometer).

Returns:

1 on successful initialization, 0 on failure or when the part number is not recognized.

Return type:

int

Raises:

Exception – Propagates any exception raised by the underlying sensor driver’s initialize method.

Example

>>> wrapper = OzSamdV2()
>>> wrapper.initializeSensor({"pn": 211, "en": True})
1

Note

Unknown part numbers are logged as warnings via the context logger and result in a 0 return value.

is_alive() str | None

Send an AT poll command to check SAMD responsiveness.

Reopens samd_serial if needed, flushes the input/output buffers, writes POLL_CMD followed by EOL, and attempts to read a single line. If the response contains the FW: token, the substring starting at that token (e.g. "FW:2.1") is returned. Non-firmware responses are ignored (and printed when debug_mode is true).

Parameters:

None.

Returns:

Firmware version string (such as "FW:2.1") if the SAMD responded with a recognizable banner, otherwise None.

Return type:

str | None

Raises:

Example

>>> wrapper = OzSamdV2()
>>> wrapper.is_alive()
'FW:2.1'

Note

The caller is expected to hold _samd_thread_lock when sharing the port with the run() thread; otherwise the response may be corrupted by concurrent sensor reads.

putInitvalues(value: dict) dict

Normalize config entries and collect init flags per sensor group.

Iterates every key in config and, for each entry that is a dict or a list of dicts, gathers the init value from each member into a list. Non-dict members or unexpected types contribute 0 to the result so that downstream consumers always observe a list of integers. The aggregated mapping is written into value keyed by the original config key, and a top-level SamdV2 entry is added with the wrapper-level init status.

Parameters:

value (dict) – Mutable output dict that will receive normalized init flags. Mutated in place and also returned.

Returns:

The same value dict, now populated with per-category init lists and a "SamdV2" summary entry.

Return type:

dict

Raises:

None – Defensive type checks make the method tolerant of unexpected config shapes.

Example

>>> wrapper = OzSamdV2()
>>> wrapper.config = {"wind": [{"init": 1}], "init": "FW:2.1"}
>>> wrapper.putInitvalues({})
{'wind': [1], 'SamdV2': 'FW:2.1'}

Note

Intended for use at the end of initialize() so that the caller can report per-sensor initialization status without re-walking the configuration tree.

putSensorValue(value: dict) dict

Collect sensor values with thread-safe SAMD access and merge into payload.

Acquires _samd_thread_lock (10-second timeout), validates SAMD responsiveness via up to three is_alive() polls, calls reinitialize() if the SAMD remains silent, then iterates each sensor in sensor_list and merges its putSensorValue(value) result into the output payload. The lock prevents read/write conflicts with the streaming run() thread.

Parameters:

value (dict) – Shared output dict to populate with SAMD-connected sensor data. Mutated in place and also returned.

Returns:

The updated value dict with merged peripheral sensor data, or the unchanged value if the lock could not be acquired or the SAMD is unresponsive.

Return type:

dict

Raises:

Exception – Caught internally; errors are logged via the context logger.

Example

>>> wrapper = OzSamdV2()
>>> payload = {}
>>> wrapper.putSensorValue(payload)

Note

is_alive() must be invoked while holding the lock because it shares the serial port with run(); calling it outside the lock can cause garbled responses.

reboot_samd() None

Reset the SAMD V2 co-processor via the MCP GPIO expander.

Issues a hardware reset by calling drivers.MCP230XX.MCP230XX.samd_rst() on MCP, then pauses for 5 seconds to allow the SAMD to finish booting.

Parameters:

None.

Returns:

None.

Raises:

Exception – Propagates any error raised by the MCP230XX driver.

Example

>>> wrapper = OzSamdV2()
>>> wrapper.reboot_samd()

Note

No-op when MCP is None (for example when the expander failed to initialize).

reinitialize() bool

Reboot the SAMD, re-detect its port, and re-open the serial connection.

Performs a full recovery cycle: hardware-resets the SAMD via reboot_samd(), re-detects its USB-ACM port via update_port(), and re-opens samd_serial at 115200 baud with read/write timeouts. If port detection fails, emits an events MQTT payload describing the failure and triggers a Raspberry Pi reboot via reboot_manager.

Parameters:

None.

Returns:

True if both port re-detection and serial open succeeded, False if either step failed.

Return type:

bool

Raises:

Exception – Caught internally; failures during port detection or serial open are logged via the context logger.

Example

>>> wrapper = OzSamdV2()
>>> wrapper.reinitialize()
True

Note

A failed port detection will reboot the entire Raspberry Pi via utils.reboot_RPi.RebootManager.reboot_raspberry_pi() as a last-resort recovery action.

run() None

Continuously read all sensors and publish real-time data via MQTT.

Background daemon thread loop that acquires _samd_thread_lock (with a 15-second timeout), reads every initialized sensor in sensor_list, attaches a timestamp and type field to the merged payload, and, when rdata_flag is enabled and mqtt_send_queue is set, pushes the payload onto the MQTT queue with "rdata": True. Sleeps RUN_DELAY seconds between cycles.

Parameters:

None.

Returns:

Runs indefinitely until the process exits.

Return type:

None

Raises:

Exception – Per-sensor read errors are caught and logged; loop iteration continues even on errors.

Example

>>> wrapper = OzSamdV2()
>>> import threading
>>> threading.Thread(target=wrapper.run, daemon=True).start()

Note

The serial port is shared with putSensorValue() and is_alive(); locking through _samd_thread_lock ensures that only one operation accesses the SAMD at a time.

updateSamdFirmware() None

Close the serial port and flash new firmware to the SAMD via bossac.

Closes samd_serial (so bossac and stty can take exclusive ownership of the device), invokes flashSamdFirmware() with the bundled firmware_file, waits for the SAMD to reboot, and triggers reinitialize() when flashing fails.

Parameters:

None.

Returns:

None.

Raises:

Exception – Caught internally and logged via the context logger.

Example

>>> wrapper = OzSamdV2()
>>> wrapper.updateSamdFirmware()

Note

Closing the serial port before flashing is critical; if the port remains open in Python, bossac cannot reset the board into bootloader mode and the upload will fail.

update_port() bool

Scan ACM devices for the Lasan/SAMD V2 port, retrying with reboots.

Calls drivers.DeviceDetector.DeviceDetector.DeviceDetector.scan_acm_devices() to enumerate USB-ACM devices and locate the Lasan entry. If the port is missing, reboots the SAMD via reboot_samd() and retries up to three times with a 5-second pause between attempts. The detected port path is stored in lasan_port.

Parameters:

None.

Returns:

True if the SAMD V2 port was found within three attempts, False after exhausting retries.

Return type:

bool

Raises:

Exception – Propagates any error raised by the device detector or by reboot_samd().

Example

>>> wrapper = OzSamdV2()
>>> wrapper.update_port()
True

Note

Each failed attempt issues a hardware reset of the SAMD via the MCP230XX expander; this can interrupt any in-flight serial traffic.

EOL = b'\r\n'
MCP = None
POLL_CMD = b'AT'
RUN_DELAY = None
SAMD_VERSION = 'FW:2.1'
_samd_thread_lock
baseConfig: ClassVar[dict]
config
data_type = 'samd'
debug_mode: bool = False
detector
empty_config = False
firmware_file = '/usr/src/app/OzWrapper/OzSamdV2/firmware.bin'
lasan_port = '/dev/ttyACM0'
mqtt_send_queue: queue.Queue | None = None
parameter = 'pm'
partNumber = 'pn'
rain_sensor = None
rdata_flag: bool = False
reboot_manager
samd_serial = None
samd_thread: threading.Thread | None = None
sensor_list = []
surface_sensor = None
visible_sensor = None
wind_sensor = None