OzWrapper.OzSamdV2.OzSamdV2.OzSamdV2¶
- class OzWrapper.OzSamdV2.OzSamdV2.OzSamdV2¶
Bases:
SensorBase.GenericSensorSAMD 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
bossacand thread-safe serial access with locking. Inherits fromSensorBase.SensorBase.GenericSensorand supersedes the v1 wrapperOzWrapper.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, orNonewhen 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 duringinitialize()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 runningrun().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
OzSamdV2instance state.Sets up the MCP230XX GPIO expander (with device number taken from the
MCP_IDenvironment variable), the USB-ACM device detector, default run delay, threading primitives, and the reboot manager. CallsSensorBase.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
bossacand verify the result from its output.Resolves the host CPU architecture (
x86_64orarm), launches the bundledupload.shshell helper that drivesbossac, and scans the captured stdout for two success markers: a100%progress indicator and aVerify successfulline. Only when both markers are present is the flash treated as successful.- Parameters:
- Returns:
Trueif both progress and verify markers were found in the upload output,Falseotherwise.- Return type:
- 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();armv7lis normalized toarmto match the bundledbossacbinaries.
- getSensorReading() dict¶
Stub for the
SensorBase.SensorBase.GenericSensorinterface.SAMD V2 readings are produced asynchronously by the
run()daemon thread and aggregated synchronously byputSensorValue(), 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:
- 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) fromconfig, re-detects the SAMD USB-ACM port, validates liveness viais_alive(), and triggers an OTA firmware flash viaupdateSamdFirmware()when the reported firmware does not matchSAMD_VERSIONor when forced viaff. After firmware checks, iterates each configured sensor category (wind,visible,surface,rain,noise,uva,pyro) and delegates toinitializeSensor()for each enabled sensor. Finally starts the backgroundrun()daemon thread (only once) and populatesinit_valueviaputInitvalues().- Parameters:
config (
dict) – Configuration dict containing sensor categories and runtime flags such asdebug,rdata,ff, anddelay.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:
Trueif at least one sensor initialized successfully,Falseif the SAMD port could not be opened or no sensor initialized.- Return type:
- 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 ofsensorto instantiate the appropriate driver (Wind, Visible, Surface, Rain, Noise, ApogeeUva, ApogeePyro) and invokes itsinitializemethod with the sharedsamd_serialconnection. On success, the driver instance is appended tosensor_list.- Parameters:
sensor (
dict) – Sensor configuration dict. Must contain apnkey 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:
1on successful initialization,0on failure or when the part number is not recognized.- Return type:
- Raises:
Exception – Propagates any exception raised by the underlying sensor driver’s
initializemethod.
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
0return value.
- is_alive() str | None¶
Send an AT poll command to check SAMD responsiveness.
Reopens
samd_serialif needed, flushes the input/output buffers, writesPOLL_CMDfollowed byEOL, and attempts to read a single line. If the response contains theFW:token, the substring starting at that token (e.g."FW:2.1") is returned. Non-firmware responses are ignored (and printed whendebug_modeis true).- Parameters:
None.
- Returns:
Firmware version string (such as
"FW:2.1") if the SAMD responded with a recognizable banner, otherwiseNone.- Return type:
str | None
- Raises:
serial.SerialException – Caught internally and logged; returns
Nonein that case.Exception – Caught internally and logged; returns
None.
Example
>>> wrapper = OzSamdV2() >>> wrapper.is_alive() 'FW:2.1'
Note
The caller is expected to hold
_samd_thread_lockwhen sharing the port with therun()thread; otherwise the response may be corrupted by concurrent sensor reads.
- putInitvalues(value: dict) dict¶
Normalize
configentries and collectinitflags per sensor group.Iterates every key in
configand, for each entry that is a dict or a list of dicts, gathers theinitvalue from each member into a list. Non-dict members or unexpected types contribute0to the result so that downstream consumers always observe a list of integers. The aggregated mapping is written intovaluekeyed by the original config key, and a top-levelSamdV2entry 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
valuedict, now populated with per-category init lists and a"SamdV2"summary entry.- Return type:
- 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 threeis_alive()polls, callsreinitialize()if the SAMD remains silent, then iterates each sensor insensor_listand merges itsputSensorValue(value)result into the output payload. The lock prevents read/write conflicts with the streamingrun()thread.- Parameters:
value (
dict) – Shared output dict to populate with SAMD-connected sensor data. Mutated in place and also returned.- Returns:
The updated
valuedict with merged peripheral sensor data, or the unchangedvalueif the lock could not be acquired or the SAMD is unresponsive.- Return type:
- 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 withrun(); 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()onMCP, 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
MCPisNone(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 viaupdate_port(), and re-openssamd_serialat 115200 baud with read/write timeouts. If port detection fails, emits aneventsMQTT payload describing the failure and triggers a Raspberry Pi reboot viareboot_manager.- Parameters:
None.
- Returns:
Trueif both port re-detection and serial open succeeded,Falseif either step failed.- Return type:
- 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 insensor_list, attaches a timestamp andtypefield to the merged payload, and, whenrdata_flagis enabled andmqtt_send_queueis set, pushes the payload onto the MQTT queue with"rdata": True. SleepsRUN_DELAYseconds 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()andis_alive(); locking through_samd_thread_lockensures 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(sobossacandsttycan take exclusive ownership of the device), invokesflashSamdFirmware()with the bundledfirmware_file, waits for the SAMD to reboot, and triggersreinitialize()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,
bossaccannot 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 theLasanentry. If the port is missing, reboots the SAMD viareboot_samd()and retries up to three times with a 5-second pause between attempts. The detected port path is stored inlasan_port.- Parameters:
None.
- Returns:
Trueif the SAMD V2 port was found within three attempts,Falseafter exhausting retries.- Return type:
- 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¶
- config¶
- data_type = 'samd'¶
- 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¶
- reboot_manager¶
- samd_serial = None¶
- samd_thread: threading.Thread | None = None¶
- sensor_list = []¶
- surface_sensor = None¶
- visible_sensor = None¶
- wind_sensor = None¶