diff --git a/.gitignore b/.gitignore index 1f6e00c8..6ade2c73 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ node_modules/ CLAUDE.md .build/ .venv/ +.claude/ diff --git a/.vscode/settings.json b/.vscode/settings.json index d6817251..70758b93 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,6 +6,7 @@ "lib/apds9960", "lib/bme280", "lib/bq27441", + "lib/daplink_bridge", "lib/daplink_flash", "lib/gc9a01", "lib/hts221", diff --git a/README.md b/README.md index c4ca0705..6113ceb2 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,8 @@ This repository contains all the drivers for the main components of the [STeaMi] | Component | Driver | I2C Address | Description | | ------------- | ---------------------------------------------- | ----------- | ------------------------------------- | | BQ27441-G1 | [`bq27441`](lib/bq27441/README.md) | `0x55` | Battery fuel gauge | -| DAPLink Flash | [`daplink_flash`](lib/daplink_flash/README.md) | `0x3B` | I2C-to-SPI flash bridge + config zone | +| DAPLink Bridge | [`daplink_bridge`](lib/daplink_bridge/) | `0x3B` | I2C bridge to STM32F103 + config zone | +| DAPLink Flash | [`daplink_flash`](lib/daplink_flash/README.md) | — | Flash file operations via bridge | | SSD1327 | [`ssd1327`](lib/ssd1327/README.md) | — (SPI) | 128x128 greyscale OLED display | | MCP23009E | [`mcp23009e`](lib/mcp23009e/README.md) | `0x20` | 8-bit I/O expander (D-PAD) | | VL53L1X | [`vl53l1x`](lib/vl53l1x/README.md) | `0x29` | Time-of-Flight distance sensor | diff --git a/lib/daplink_bridge/daplink_bridge/__init__.py b/lib/daplink_bridge/daplink_bridge/__init__.py new file mode 100644 index 00000000..e39e9409 --- /dev/null +++ b/lib/daplink_bridge/daplink_bridge/__init__.py @@ -0,0 +1,5 @@ +from daplink_bridge.device import DaplinkBridge + +__all__ = [ + "DaplinkBridge", +] diff --git a/lib/daplink_bridge/daplink_bridge/const.py b/lib/daplink_bridge/daplink_bridge/const.py new file mode 100644 index 00000000..29aa2614 --- /dev/null +++ b/lib/daplink_bridge/daplink_bridge/const.py @@ -0,0 +1,29 @@ +from micropython import const + +# I2C address (7-bit) — 0x76 in 8-bit (CODAL convention) +DAPLINK_BRIDGE_DEFAULT_ADDR = const(0x3B) + +# WHO_AM_I expected value +DAPLINK_BRIDGE_WHO_AM_I_VAL = const(0x4C) + +# Commands — bridge level +CMD_WHO_AM_I = const(0x01) +CMD_WRITE_CONFIG = const(0x30) +CMD_READ_CONFIG = const(0x31) +CMD_CLEAR_CONFIG = const(0x32) + +# Registers +REG_STATUS = const(0x80) +REG_ERROR = const(0x81) + +# Status register bits +STATUS_BUSY = const(0x80) + +# Error register bits +ERROR_BAD_PARAM = const(0x01) +ERROR_CMD_FAILED = const(0x80) + +# Protocol limits +MAX_WRITE_CHUNK = const(30) +SECTOR_SIZE = const(256) +CONFIG_SIZE = const(1024) diff --git a/lib/daplink_bridge/daplink_bridge/device.py b/lib/daplink_bridge/daplink_bridge/device.py new file mode 100644 index 00000000..bb116488 --- /dev/null +++ b/lib/daplink_bridge/daplink_bridge/device.py @@ -0,0 +1,143 @@ +from time import sleep_ms + +from daplink_bridge.const import * + + +class DaplinkBridge(object): + """Low-level I2C bridge to the STM32F103 DAPLink interface.""" + + def __init__(self, i2c, address=DAPLINK_BRIDGE_DEFAULT_ADDR): + self.i2c = i2c + self.address = address + self._buffer_1 = bytearray(1) + + # -------------------------------------------------- + # Low level I2C + # -------------------------------------------------- + + def _read_reg(self, reg, n=1): + """Read n bytes from register.""" + if n == 1: + self.i2c.readfrom_mem_into(self.address, reg, self._buffer_1) + return self._buffer_1[0] + return self.i2c.readfrom_mem(self.address, reg, n) + + def _write_reg(self, reg, data): + """Write data bytes to register.""" + self.i2c.writeto_mem(self.address, reg, data) + + def _write_cmd(self, cmd): + """Write a single command byte (no payload).""" + self._buffer_1[0] = cmd + self.i2c.writeto(self.address, self._buffer_1) + + def _writeto(self, data): + """Write raw data frame to the bridge.""" + self.i2c.writeto(self.address, data) + + def _readfrom(self, n): + """Read n raw bytes from the bridge.""" + return self.i2c.readfrom(self.address, n) + + # -------------------------------------------------- + # Device identification + # -------------------------------------------------- + + def device_id(self): + """Read WHO_AM_I register. Expected: 0x4C.""" + return self._read_reg(CMD_WHO_AM_I) + + # -------------------------------------------------- + # Status and error registers + # -------------------------------------------------- + + def _status(self): + """Read raw status register.""" + return self._read_reg(REG_STATUS) + + def _error(self): + """Read raw error register.""" + return self._read_reg(REG_ERROR) + + def busy(self): + """Return True if bridge is busy.""" + return bool(self._status() & STATUS_BUSY) + + def _wait_busy(self, timeout_ms=30000): + """Poll busy bit until clear. Raises OSError on timeout.""" + elapsed = 0 + while self.busy(): + sleep_ms(5) + elapsed += 5 + if elapsed >= timeout_ms: + raise OSError("DAPLink bridge busy timeout") + + # -------------------------------------------------- + # Config zone (1 KB persistent storage in F103 internal flash) + # -------------------------------------------------- + + def clear_config(self): + """Erase the 1 KB config zone.""" + self._wait_busy() + self._write_cmd(CMD_CLEAR_CONFIG) + sleep_ms(100) + self._wait_busy() + err = self._error() + if err: + raise OSError("DAPLink config clear error: 0x{:02X}".format(err)) + + def write_config(self, data, offset=0): + """Write data to the config zone at the given offset. + + Args: + data: bytes or str to store. + offset: byte offset within the config zone (0-1023). + """ + if isinstance(data, str): + data = data.encode() + length = len(data) + if offset < 0 or offset >= CONFIG_SIZE: + raise ValueError("offset out of range: {}".format(offset)) + if offset + length > CONFIG_SIZE: + raise ValueError("data exceeds config zone boundary") + buf = bytearray(MAX_WRITE_CHUNK + 2) + max_payload = len(buf) - 4 + buf[0] = CMD_WRITE_CONFIG + pos = 0 + while pos < length: + self._wait_busy() + chunk_len = min(max_payload, length - pos) + cur_offset = offset + pos + buf[1] = (cur_offset >> 8) & 0xFF + buf[2] = cur_offset & 0xFF + buf[3] = chunk_len + buf[4 : 4 + chunk_len] = data[pos : pos + chunk_len] + for i in range(4 + chunk_len, len(buf)): + buf[i] = 0 + self.i2c.writeto(self.address, buf) + sleep_ms(50) + pos += chunk_len + self._wait_busy() + err = self._error() + if err: + raise OSError("DAPLink config write error: 0x{:02X}".format(err)) + + def read_config(self): + """Read config zone content. + + Returns: + bytes: config data up to first 0xFF, or b'' if empty. + """ + result = bytearray() + for page_offset in range(0, CONFIG_SIZE, SECTOR_SIZE): + self._wait_busy() + hi = (page_offset >> 8) & 0xFF + lo = page_offset & 0xFF + self._write_reg(CMD_READ_CONFIG, bytes([hi, lo])) + sleep_ms(100) + data = self.i2c.readfrom(self.address, SECTOR_SIZE) + for i in range(SECTOR_SIZE): + if data[i] == 0xFF: + return bytes(result) + result.append(data[i]) + return bytes(result) diff --git a/lib/daplink_bridge/manifest.py b/lib/daplink_bridge/manifest.py new file mode 100644 index 00000000..4cf99886 --- /dev/null +++ b/lib/daplink_bridge/manifest.py @@ -0,0 +1,6 @@ +metadata( + description="Low-level I2C bridge driver for the STM32F103 DAPLink interface.", + version="0.0.1", +) + +package("daplink_bridge") diff --git a/lib/daplink_flash/daplink_flash/const.py b/lib/daplink_flash/daplink_flash/const.py index 27178b20..b6ebf342 100644 --- a/lib/daplink_flash/daplink_flash/const.py +++ b/lib/daplink_flash/daplink_flash/const.py @@ -1,39 +1,15 @@ from micropython import const -# I2C address (7-bit) — 0x76 in 8-bit (CODAL convention) -DAPLINK_FLASH_DEFAULT_ADDR = const(0x3B) - -# WHO_AM_I expected value -DAPLINK_FLASH_WHO_AM_I_VAL = const(0x4C) - -# Commands -CMD_WHO_AM_I = const(0x01) +# Commands — flash level CMD_SET_FILENAME = const(0x03) CMD_GET_FILENAME = const(0x04) CMD_CLEAR_FLASH = const(0x10) CMD_WRITE_DATA = const(0x11) CMD_READ_SECTOR = const(0x20) -CMD_WRITE_CONFIG = const(0x30) -CMD_READ_CONFIG = const(0x31) -CMD_CLEAR_CONFIG = const(0x32) - -# Registers -REG_STATUS = const(0x80) -REG_ERROR = const(0x81) - -# Status register bits -STATUS_BUSY = const(0x80) - -# Error register bits -ERROR_BAD_PARAM = const(0x01) -ERROR_FILE_FULL = const(0x20) -ERROR_BAD_FILENAME = const(0x40) -ERROR_CMD_FAILED = const(0x80) # Protocol limits MAX_WRITE_CHUNK = const(30) SECTOR_SIZE = const(256) MAX_SECTORS = const(32768) -CONFIG_SIZE = const(1024) FILENAME_LEN = const(8) EXT_LEN = const(3) diff --git a/lib/daplink_flash/daplink_flash/device.py b/lib/daplink_flash/daplink_flash/device.py index aa5d3559..ed3f1362 100644 --- a/lib/daplink_flash/daplink_flash/device.py +++ b/lib/daplink_flash/daplink_flash/device.py @@ -4,65 +4,10 @@ class DaplinkFlash(object): - """MicroPython driver for the DAPLink Flash bridge (STM32F103 → W25Q64JV).""" + """High-level flash file operations via the DAPLink bridge.""" - def __init__(self, i2c, address=DAPLINK_FLASH_DEFAULT_ADDR): - self.i2c = i2c - self.address = address - self._buffer_1 = bytearray(1) - - # -------------------------------------------------- - # Low level I2C - # -------------------------------------------------- - - def _read_reg(self, reg, n=1): - """Read n bytes from register.""" - if n == 1: - self.i2c.readfrom_mem_into(self.address, reg, self._buffer_1) - return self._buffer_1[0] - return self.i2c.readfrom_mem(self.address, reg, n) - - def _write_reg(self, reg, data): - """Write data bytes to register.""" - self.i2c.writeto_mem(self.address, reg, data) - - def _write_cmd(self, cmd): - """Write a single command byte (no payload).""" - self._buffer_1[0] = cmd - self.i2c.writeto(self.address, self._buffer_1) - - # -------------------------------------------------- - # Device identification - # -------------------------------------------------- - - def device_id(self): - """Read WHO_AM_I register. Expected: 0x4C.""" - return self._read_reg(CMD_WHO_AM_I) - - # -------------------------------------------------- - # Status and error registers - # -------------------------------------------------- - - def _status(self): - """Read raw status register.""" - return self._read_reg(REG_STATUS) - - def _error(self): - """Read raw error register.""" - return self._read_reg(REG_ERROR) - - def busy(self): - """Return True if flash is busy.""" - return bool(self._status() & STATUS_BUSY) - - def _wait_busy(self, timeout_ms=30000): - """Poll busy bit until clear. Raises OSError on timeout.""" - elapsed = 0 - while self.busy(): - sleep_ms(5) - elapsed += 5 - if elapsed >= timeout_ms: - raise OSError("DAPLink Flash busy timeout") + def __init__(self, bridge): + self._bridge = bridge # -------------------------------------------------- # Filename management @@ -70,16 +15,16 @@ def _wait_busy(self, timeout_ms=30000): def set_filename(self, name, ext): """Set 8.3 filename. name: max 8 chars, ext: max 3 chars.""" - self._wait_busy() + self._bridge._wait_busy() n = name.upper().encode("ascii")[:FILENAME_LEN] e = ext.upper().encode("ascii")[:EXT_LEN] padded = n + b" " * (FILENAME_LEN - len(n)) + e + b" " * (EXT_LEN - len(e)) - self._write_reg(CMD_SET_FILENAME, padded) + self._bridge._write_reg(CMD_SET_FILENAME, padded) def get_filename(self): """Read current filename. Returns (name, ext) tuple, stripped.""" - self._wait_busy() - raw = self._read_reg(CMD_GET_FILENAME, FILENAME_LEN + EXT_LEN) + self._bridge._wait_busy() + raw = self._bridge._read_reg(CMD_GET_FILENAME, FILENAME_LEN + EXT_LEN) name = bytes(raw[:FILENAME_LEN]).decode().rstrip() ext = bytes(raw[FILENAME_LEN:]).decode().rstrip() return (name, ext) @@ -90,8 +35,8 @@ def get_filename(self): def clear_flash(self): """Erase entire flash memory.""" - self._wait_busy() - self._write_cmd(CMD_CLEAR_FLASH) + self._bridge._wait_busy() + self._bridge._write_cmd(CMD_CLEAR_FLASH) def write(self, data): """Append data to current file. data: bytes or str. @@ -105,17 +50,16 @@ def write(self, data): buf = bytearray(MAX_WRITE_CHUNK + 2) buf[0] = CMD_WRITE_DATA while offset < length: - self._wait_busy() + self._bridge._wait_busy() chunk_len = min(MAX_WRITE_CHUNK, length - offset) buf[1] = chunk_len buf[2 : 2 + chunk_len] = data[offset : offset + chunk_len] - # Zero-pad remainder for i in range(2 + chunk_len, len(buf)): buf[i] = 0 - self.i2c.writeto(self.address, buf) + self._bridge._writeto(buf) offset += chunk_len - self._wait_busy() - err = self._error() + self._bridge._wait_busy() + err = self._bridge._error() if err: raise OSError("DAPLink Flash write error: 0x{:02X}".format(err)) return length @@ -137,13 +81,10 @@ def read_sector(self, sector): Returns: bytes: 256 bytes of data. """ - self._wait_busy() - self._write_reg(CMD_READ_SECTOR, bytes([sector >> 8, sector & 0xFF])) - # F103 processes the command in its 30ms hook, then sets up DMA. - # After DMA setup, the F103 is no longer in listen mode — only - # a plain readfrom() will work (no register-based status poll). + self._bridge._wait_busy() + self._bridge._write_reg(CMD_READ_SECTOR, bytes([sector >> 8, sector & 0xFF])) sleep_ms(200) - return self.i2c.readfrom(self.address, SECTOR_SIZE) + return self._bridge._readfrom(SECTOR_SIZE) def read(self, length=None): """Read file content from flash. @@ -171,77 +112,3 @@ def read(self, length=None): result.append(data[i]) sector += 1 return bytes(result) - - # -------------------------------------------------- - # Config zone (1 KB persistent storage in F103 internal flash) - # -------------------------------------------------- - - def clear_config(self): - """Erase the 1 KB config zone.""" - self._wait_busy() - self._write_cmd(CMD_CLEAR_CONFIG) - sleep_ms(100) - self._wait_busy() - err = self._error() - if err: - raise OSError("DAPLink config clear error: 0x{:02X}".format(err)) - - def write_config(self, data, offset=0): - """Write data to the config zone at the given offset. - - The firmware performs a read-modify-write cycle so existing - data outside the written range is preserved. - - Args: - data: bytes or str to store. - offset: byte offset within the config zone (0-1023). - """ - if isinstance(data, str): - data = data.encode() - length = len(data) - if offset < 0 or offset >= CONFIG_SIZE: - raise ValueError("offset out of range: {}".format(offset)) - if offset + length > CONFIG_SIZE: - raise ValueError("data exceeds config zone boundary") - # Frame: cmd(1) + offset(2) + len(1) + data(N) + padding - buf = bytearray(MAX_WRITE_CHUNK + 2) - max_payload = len(buf) - 4 - buf[0] = CMD_WRITE_CONFIG - pos = 0 - while pos < length: - self._wait_busy() - chunk_len = min(max_payload, length - pos) - cur_offset = offset + pos - buf[1] = (cur_offset >> 8) & 0xFF - buf[2] = cur_offset & 0xFF - buf[3] = chunk_len - buf[4 : 4 + chunk_len] = data[pos : pos + chunk_len] - for i in range(4 + chunk_len, len(buf)): - buf[i] = 0 - self.i2c.writeto(self.address, buf) - sleep_ms(50) - pos += chunk_len - self._wait_busy() - err = self._error() - if err: - raise OSError("DAPLink config write error: 0x{:02X}".format(err)) - - def read_config(self): - """Read config zone content. - - Returns: - bytes: config data up to first 0xFF, or b'' if empty. - """ - result = bytearray() - for page_offset in range(0, CONFIG_SIZE, SECTOR_SIZE): - self._wait_busy() - hi = (page_offset >> 8) & 0xFF - lo = page_offset & 0xFF - self._write_reg(CMD_READ_CONFIG, bytes([hi, lo])) - sleep_ms(100) - data = self.i2c.readfrom(self.address, SECTOR_SIZE) - for i in range(SECTOR_SIZE): - if data[i] == 0xFF: - return bytes(result) - result.append(data[i]) - return bytes(result) diff --git a/lib/daplink_flash/examples/erase_flash.py b/lib/daplink_flash/examples/erase_flash.py index 1777d886..f0a3fbc7 100644 --- a/lib/daplink_flash/examples/erase_flash.py +++ b/lib/daplink_flash/examples/erase_flash.py @@ -2,11 +2,13 @@ from time import sleep_ms +from daplink_bridge import DaplinkBridge from daplink_flash import DaplinkFlash from machine import I2C i2c = I2C(1) -flash = DaplinkFlash(i2c) +bridge = DaplinkBridge(i2c) +flash = DaplinkFlash(bridge) name, ext = flash.get_filename() print("Current file: {}.{}".format(name, ext)) @@ -16,4 +18,4 @@ sleep_ms(1000) print("Done. Flash is empty.") -print("ERROR: 0x{:02X}".format(flash._error())) +print("ERROR: 0x{:02X}".format(bridge._error())) diff --git a/lib/daplink_flash/examples/flash_info.py b/lib/daplink_flash/examples/flash_info.py index 8be20f4c..ae007392 100644 --- a/lib/daplink_flash/examples/flash_info.py +++ b/lib/daplink_flash/examples/flash_info.py @@ -1,16 +1,18 @@ """Display DAPLink Flash bridge status and filename.""" +from daplink_bridge import DaplinkBridge from daplink_flash import DaplinkFlash from machine import I2C i2c = I2C(1) -flash = DaplinkFlash(i2c) +bridge = DaplinkBridge(i2c) +flash = DaplinkFlash(bridge) print("=== DAPLink Flash Info ===") -print("WHO_AM_I: 0x{:02X}".format(flash.device_id())) -print("STATUS: 0x{:02X}".format(flash._status())) -print("ERROR: 0x{:02X}".format(flash._error())) -print("Busy: ", flash.busy()) +print("WHO_AM_I: 0x{:02X}".format(bridge.device_id())) +print("STATUS: 0x{:02X}".format(bridge._status())) +print("ERROR: 0x{:02X}".format(bridge._error())) +print("Busy: ", bridge.busy()) name, ext = flash.get_filename() print("Filename: {}.{}".format(name, ext)) diff --git a/lib/daplink_flash/examples/read_file.py b/lib/daplink_flash/examples/read_file.py index 360e70a3..a22849f8 100644 --- a/lib/daplink_flash/examples/read_file.py +++ b/lib/daplink_flash/examples/read_file.py @@ -1,10 +1,12 @@ """Read and display the current file stored on flash.""" +from daplink_bridge import DaplinkBridge from daplink_flash import DaplinkFlash from machine import I2C i2c = I2C(1) -flash = DaplinkFlash(i2c) +bridge = DaplinkBridge(i2c) +flash = DaplinkFlash(bridge) name, ext = flash.get_filename() print("Reading file: {}.{}".format(name, ext)) diff --git a/lib/daplink_flash/examples/sensor_log.py b/lib/daplink_flash/examples/sensor_log.py index a6d82dad..1438a54c 100644 --- a/lib/daplink_flash/examples/sensor_log.py +++ b/lib/daplink_flash/examples/sensor_log.py @@ -7,6 +7,7 @@ from time import sleep_ms +from daplink_bridge import DaplinkBridge from daplink_flash import DaplinkFlash from machine import I2C @@ -17,8 +18,9 @@ # --- Init --- i2c = I2C(1) -flash = DaplinkFlash(i2c) -print("DAPLink Flash WHO_AM_I: 0x{:02X}".format(flash.device_id())) +bridge = DaplinkBridge(i2c) +flash = DaplinkFlash(bridge) +print("DAPLink Flash WHO_AM_I: 0x{:02X}".format(bridge.device_id())) # --- Generate and write data --- print("Writing {} rows to {}.{} ...".format(NUM_ROWS, FILENAME, EXT)) diff --git a/lib/daplink_flash/examples/write_csv.py b/lib/daplink_flash/examples/write_csv.py index 03d9b742..c7fa994d 100644 --- a/lib/daplink_flash/examples/write_csv.py +++ b/lib/daplink_flash/examples/write_csv.py @@ -2,13 +2,15 @@ from time import sleep_ms +from daplink_bridge import DaplinkBridge from daplink_flash import DaplinkFlash from machine import I2C i2c = I2C(1) -flash = DaplinkFlash(i2c) +bridge = DaplinkBridge(i2c) +flash = DaplinkFlash(bridge) -print("WHO_AM_I:", hex(flash.device_id())) +print("WHO_AM_I:", hex(bridge.device_id())) # Set filename and erase flash.set_filename("DATA", "CSV") diff --git a/lib/lis2mdl/examples/field_logger.py b/lib/lis2mdl/examples/field_logger.py index 68daddf9..572bef3e 100644 --- a/lib/lis2mdl/examples/field_logger.py +++ b/lib/lis2mdl/examples/field_logger.py @@ -6,6 +6,7 @@ from time import sleep_ms, ticks_diff, ticks_ms +from daplink_bridge import DaplinkBridge from daplink_flash import DaplinkFlash from lis2mdl import LIS2MDL from machine import I2C @@ -19,7 +20,8 @@ i2c = I2C(1) sensor = LIS2MDL(i2c) -flash = DaplinkFlash(i2c) +bridge = DaplinkBridge(i2c) +flash = DaplinkFlash(bridge) flash.set_filename("LIS2MDL", "CSV") if ERASE_FLASH_ON_START: diff --git a/lib/steami_config/README.md b/lib/steami_config/README.md index abc6a792..c3619162 100644 --- a/lib/steami_config/README.md +++ b/lib/steami_config/README.md @@ -10,7 +10,7 @@ updates and `clear_flash` operations. # Dependencies -* `daplink_flash` — low-level config zone access +* `daplink_bridge` — low-level I2C bridge and config zone access --- @@ -18,11 +18,11 @@ updates and `clear_flash` operations. ```python from machine import I2C -from daplink_flash import DaplinkFlash +from daplink_bridge import DaplinkBridge from steami_config import SteamiConfig i2c = I2C(1) -config = SteamiConfig(DaplinkFlash(i2c)) +config = SteamiConfig(DaplinkBridge(i2c)) config.load() print(config.board_name) @@ -156,7 +156,7 @@ Run with mpremote: mpremote mount lib exec " import sys sys.path.insert(0, '/remote/steami_config') -sys.path.insert(0, '/remote/daplink_flash') +sys.path.insert(0, '/remote/daplink_bridge') exec(open('/remote/steami_config/examples/show_config.py').read()) " ``` diff --git a/lib/steami_config/examples/calibrate_magnetometer.py b/lib/steami_config/examples/calibrate_magnetometer.py index bc504013..d2172766 100644 --- a/lib/steami_config/examples/calibrate_magnetometer.py +++ b/lib/steami_config/examples/calibrate_magnetometer.py @@ -12,7 +12,7 @@ import gc from time import sleep_ms -from daplink_flash import DaplinkFlash +from daplink_bridge import DaplinkBridge from lis2mdl import LIS2MDL from machine import I2C, SPI, Pin from ssd1327 import WS_OLED_128X128_SPI @@ -29,8 +29,8 @@ ) btn_menu = Pin("MENU_BUTTON", Pin.IN, Pin.PULL_UP) -flash = DaplinkFlash(i2c) -config = SteamiConfig(flash) +bridge = DaplinkBridge(i2c) +config = SteamiConfig(bridge) config.load() mag = LIS2MDL(i2c) config.apply_magnetometer_calibration(mag) @@ -153,7 +153,7 @@ def wait_menu(): show(["COMPAS", "", "Sauvegarde OK", "", "Verification..."]) gc.collect() -config2 = SteamiConfig(flash) +config2 = SteamiConfig(bridge) config2.load() mag2 = LIS2MDL(i2c) diff --git a/lib/steami_config/examples/calibrate_temperature.py b/lib/steami_config/examples/calibrate_temperature.py index 1e2bd8a7..19bfe91c 100644 --- a/lib/steami_config/examples/calibrate_temperature.py +++ b/lib/steami_config/examples/calibrate_temperature.py @@ -11,7 +11,7 @@ from time import sleep_ms -from daplink_flash import DaplinkFlash +from daplink_bridge import DaplinkBridge from hts221 import HTS221 from ism330dl import ISM330DL from lis2mdl import LIS2MDL @@ -21,8 +21,8 @@ from wsen_pads import WSEN_PADS i2c = I2C(1) -flash = DaplinkFlash(i2c) -config = SteamiConfig(flash) +bridge = DaplinkBridge(i2c) +config = SteamiConfig(bridge) config.load() # Read reference temperature from WSEN-HIDS (most accurate at ambient) @@ -51,7 +51,7 @@ print("\nCalibration saved.") # Verify by reloading with fresh sensor instances (simulates a reboot) -config2 = SteamiConfig(flash) +config2 = SteamiConfig(bridge) config2.load() verify_sensors = [ diff --git a/lib/steami_config/examples/show_config.py b/lib/steami_config/examples/show_config.py index e242ba35..a4972575 100644 --- a/lib/steami_config/examples/show_config.py +++ b/lib/steami_config/examples/show_config.py @@ -1,11 +1,12 @@ """Display the current board configuration stored in the config zone.""" -from daplink_flash import DaplinkFlash +from daplink_bridge import DaplinkBridge from machine import I2C from steami_config import SteamiConfig i2c = I2C(1) -config = SteamiConfig(DaplinkFlash(i2c)) +bridge = DaplinkBridge(i2c) +config = SteamiConfig(bridge) config.load() print("=== STeaMi Configuration ===") diff --git a/lib/steami_config/steami_config/device.py b/lib/steami_config/steami_config/device.py index f6cc32c9..6d8e6849 100644 --- a/lib/steami_config/steami_config/device.py +++ b/lib/steami_config/steami_config/device.py @@ -17,14 +17,14 @@ class SteamiConfig(object): """Persistent configuration stored in the DAPLink F103 config zone. Data is serialised as compact JSON and written via - ``DaplinkFlash.write_config()`` / ``read_config()``. + ``DaplinkBridge.write_config()`` / ``read_config()``. Args: - flash: a ``DaplinkFlash`` instance. + bridge: a ``DaplinkBridge`` instance. """ - def __init__(self, flash): - self._flash = flash + def __init__(self, bridge): + self._bridge = bridge self._data = {} # -------------------------------------------------- @@ -36,7 +36,7 @@ def load(self): Falls back to empty config if the zone contains invalid JSON. """ - raw = self._flash.read_config() + raw = self._bridge.read_config() if raw: try: self._data = json.loads(raw) @@ -47,8 +47,8 @@ def load(self): def save(self): """Save configuration to the config zone.""" - self._flash.clear_config() - self._flash.write_config(json.dumps(self._data, separators=(",", ":"))) + self._bridge.clear_config() + self._bridge.write_config(json.dumps(self._data, separators=(",", ":"))) # -------------------------------------------------- # Board info diff --git a/lib/wsen-hids/examples/data_logger.py b/lib/wsen-hids/examples/data_logger.py index 7b7020b6..6901aafa 100644 --- a/lib/wsen-hids/examples/data_logger.py +++ b/lib/wsen-hids/examples/data_logger.py @@ -10,6 +10,7 @@ """ from time import sleep_ms, ticks_diff, ticks_ms +from daplink_bridge import DaplinkBridge from daplink_flash import DaplinkFlash from machine import I2C from wsen_hids import WSEN_HIDS @@ -19,7 +20,8 @@ i2c = I2C(1) sensor = WSEN_HIDS(i2c) -flash = DaplinkFlash(i2c) +bridge = DaplinkBridge(i2c) +flash = DaplinkFlash(bridge) flash.set_filename("DATA", "CSV") if ERASE_FLASH_ON_START: diff --git a/lib/wsen-pads/examples/weather_station.py b/lib/wsen-pads/examples/weather_station.py index a4348c8f..a1bc6b2e 100644 --- a/lib/wsen-pads/examples/weather_station.py +++ b/lib/wsen-pads/examples/weather_station.py @@ -2,6 +2,7 @@ from time import sleep, sleep_ms +from daplink_bridge import DaplinkBridge from daplink_flash import DaplinkFlash from machine import I2C from wsen_pads import WSEN_PADS @@ -10,7 +11,8 @@ i2c = I2C(1) sensor = WSEN_PADS(i2c) -flash = DaplinkFlash(i2c) +bridge = DaplinkBridge(i2c) +flash = DaplinkFlash(bridge) sensor.set_continuous(odr=ODR_10_HZ) diff --git a/pyrightconfig.json b/pyrightconfig.json index d4ca64a5..e3e250e8 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -8,6 +8,7 @@ "lib/apds9960", "lib/bme280", "lib/bq27441", + "lib/daplink_bridge", "lib/daplink_flash", "lib/gc9a01", "lib/hts221", diff --git a/tests/scenarios/daplink_bridge.yaml b/tests/scenarios/daplink_bridge.yaml new file mode 100644 index 00000000..56266858 --- /dev/null +++ b/tests/scenarios/daplink_bridge.yaml @@ -0,0 +1,113 @@ +driver: daplink_bridge +driver_class: DaplinkBridge +i2c_address: 0x3B + +i2c: + id: 1 + +mock_registers: + # WHO_AM_I (expected 0x4C) + 0x01: 0x4C + # STATUS_REG (not busy) + 0x80: 0x00 + # ERROR_REG (no error) + 0x81: 0x00 + +tests: + - name: "Verify WHO_AM_I register" + action: read_register + register: 0x01 + expect: 0x4C + mode: [mock, hardware] + + - name: "Read WHO_AM_I via method" + action: call + method: device_id + expect: 0x4C + mode: [mock, hardware] + + - name: "Read status register" + action: call + method: _status + expect: 0x00 + mode: [mock] + + - name: "Read error register" + action: call + method: _error + expect: 0x00 + mode: [mock] + + - name: "Not busy when status is 0" + action: script + script: | + result = dev.busy() == False + expect_true: true + mode: [mock] + + - name: "Clear config sends command" + action: script + script: | + i2c.clear_write_log() + dev.clear_config() + log = i2c.get_write_log() + sent_clear = any(reg is None and 0x32 in data for reg, data in log) + result = sent_clear + expect_true: true + mode: [mock] + + - name: "Write config sends command" + action: script + script: | + i2c.clear_write_log() + dev.write_config(b"test") + log = i2c.get_write_log() + sent_write = any(reg is None and data[0] == 0x30 for reg, data in log) + result = sent_write + expect_true: true + mode: [mock] + + - name: "Write config sends write without prior clear" + action: script + script: | + i2c.clear_write_log() + dev.write_config(b"hi") + log = i2c.get_write_log() + sent_clear = any(reg is None and 0x32 in data for reg, data in log) + sent_write = any(reg is None and data[0] == 0x30 for reg, data in log) + result = sent_write and not sent_clear + expect_true: true + mode: [mock] + + # ----- Hardware ----- + + - name: "Status register readable" + action: call + method: _status + expect_not_none: true + mode: [hardware] + + - name: "Error register readable" + action: call + method: _error + expect_not_none: true + mode: [hardware] + + - name: "Write and read config round-trip" + action: hardware_script + script: | + dev.clear_config() + dev.write_config('{"test": 42}') + content = dev.read_config() + result = content == b'{"test": 42}' + expect_true: true + mode: [hardware] + + - name: "Clear config erases config zone" + action: hardware_script + script: | + dev.write_config("data") + dev.clear_config() + result = dev.read_config() == b"" + expect_true: true + mode: [hardware] diff --git a/tests/scenarios/daplink_flash.yaml b/tests/scenarios/daplink_flash.yaml index 3a4d9078..00a60eea 100644 --- a/tests/scenarios/daplink_flash.yaml +++ b/tests/scenarios/daplink_flash.yaml @@ -2,15 +2,25 @@ driver: daplink_flash driver_class: DaplinkFlash i2c_address: 0x3B -# I2C config for hardware tests (STeaMi board - STM32WB55) +dependencies: + - daplink_bridge + +# Custom init: DaplinkFlash now takes a DaplinkBridge, not i2c +mock_init: | + from daplink_bridge import DaplinkBridge + bridge = DaplinkBridge(i2c, address=0x3B) + dev = DaplinkFlash(bridge) + i2c: id: 1 -# Register values for mock tests -# These simulate the STM32F103 DAPLink flash bridge +# Hardware init: create bridge then flash +hardware_init: | + from daplink_bridge import DaplinkBridge + bridge = DaplinkBridge(i2c, address=0x3B) + dev = DaplinkFlash(bridge) + mock_registers: - # WHO_AM_I (expected 0x4C) - 0x01: 0x4C # STATUS_REG (not busy) 0x80: 0x00 # ERROR_REG (no error) @@ -19,37 +29,6 @@ mock_registers: 0x04: [0x54, 0x45, 0x53, 0x54, 0x20, 0x20, 0x20, 0x20, 0x43, 0x53, 0x56] tests: - - name: "Verify WHO_AM_I register" - action: read_register - register: 0x01 - expect: 0x4C - mode: [mock, hardware] - - - name: "Read WHO_AM_I via method" - action: call - method: device_id - expect: 0x4C - mode: [mock, hardware] - - - name: "Read status register" - action: call - method: _status - expect: 0x00 - mode: [mock] - - - name: "Read error register" - action: call - method: _error - expect: 0x00 - mode: [mock] - - - name: "Not busy when status is 0" - action: script - script: | - result = dev.busy() == False - expect_true: true - mode: [mock] - - name: "Get filename returns tuple" action: script script: | @@ -61,9 +40,10 @@ tests: - name: "Set filename writes to register" action: script script: | - i2c.clear_write_log() + _i2c = dev._bridge.i2c + _i2c.clear_write_log() dev.set_filename("DATA", "CSV") - log = i2c.get_write_log() + log = _i2c.get_write_log() wrote_filename = any(reg == 0x03 for reg, data in log) result = wrote_filename expect_true: true @@ -72,9 +52,10 @@ tests: - name: "Clear flash sends command" action: script script: | - i2c.clear_write_log() + _i2c = dev._bridge.i2c + _i2c.clear_write_log() dev.clear_flash() - log = i2c.get_write_log() + log = _i2c.get_write_log() sent_clear = any(reg is None and 0x10 in data for reg, data in log) result = sent_clear expect_true: true @@ -83,9 +64,10 @@ tests: - name: "Write sends chunked data" action: script script: | - i2c.clear_write_log() + _i2c = dev._bridge.i2c + _i2c.clear_write_log() dev.write(b"Hello") - log = i2c.get_write_log() + log = _i2c.get_write_log() sent_write = any(reg is None and data[0] == 0x11 for reg, data in log) result = sent_write expect_true: true @@ -118,62 +100,17 @@ tests: - name: "Read sector sends command" action: script script: | - i2c.clear_write_log() + _i2c = dev._bridge.i2c + _i2c.clear_write_log() dev.read_sector(0) - log = i2c.get_write_log() + log = _i2c.get_write_log() sent_read = any(reg == 0x20 for reg, data in log) result = sent_read expect_true: true mode: [mock] - - name: "Clear config sends command" - action: script - script: | - i2c.clear_write_log() - dev.clear_config() - log = i2c.get_write_log() - sent_clear = any(reg is None and 0x32 in data for reg, data in log) - result = sent_clear - expect_true: true - mode: [mock] - - - name: "Write config sends command" - action: script - script: | - i2c.clear_write_log() - dev.write_config(b"test") - log = i2c.get_write_log() - sent_write = any(reg is None and data[0] == 0x30 for reg, data in log) - result = sent_write - expect_true: true - mode: [mock] - - - name: "Write config sends write without prior clear" - action: script - script: | - i2c.clear_write_log() - dev.write_config(b"hi") - log = i2c.get_write_log() - sent_clear = any(reg is None and 0x32 in data for reg, data in log) - sent_write = any(reg is None and data[0] == 0x30 for reg, data in log) - result = sent_write and not sent_clear - expect_true: true - mode: [mock] - # ----- Hardware ----- - - name: "Status register readable" - action: call - method: _status - expect_not_none: true - mode: [hardware] - - - name: "Error register readable" - action: call - method: _error - expect_not_none: true - mode: [hardware] - - name: "Set and get filename round-trip" action: hardware_script script: | @@ -214,7 +151,6 @@ tests: dev.set_filename("PAGEBND", "TXT") dev.clear_flash() sleep_ms(500) - # Write 20 lines of 21 bytes each (420 bytes, crosses 256-byte page) for i in range(20): dev.write_line("ROW{:04d};AAAAAAAAAA".format(i)) sleep_ms(100) @@ -255,7 +191,6 @@ tests: dev.set_filename("SECTOR", "TXT") dev.clear_flash() sleep_ms(500) - # Write more than 256 bytes to fill at least 2 sectors data = "A" * 300 + "\n" dev.write(data) sleep_ms(100) @@ -266,20 +201,6 @@ tests: expect_true: true mode: [hardware] - - name: "Error register set when writing to full flash" - action: hardware_script - script: | - # Verify error register is clean after normal operations - dev.set_filename("ERRTEST", "TXT") - dev.clear_flash() - from time import sleep_ms - sleep_ms(500) - dev.write_line("test") - sleep_ms(100) - result = dev._error() == 0x00 - expect_true: true - mode: [hardware] - - name: "Erase then read returns empty" action: hardware_script script: | @@ -296,50 +217,19 @@ tests: expect_true: true mode: [hardware] - # ----- Config zone ----- - - - name: "Write and read config round-trip" - action: hardware_script - script: | - dev.clear_config() - dev.write_config('{"test": 42}') - content = dev.read_config() - result = content == b'{"test": 42}' - expect_true: true - mode: [hardware] - - - name: "Clear config erases config zone" - action: hardware_script - script: | - dev.write_config("data") - dev.clear_config() - result = dev.read_config() == b"" - expect_true: true - mode: [hardware] - - - name: "Clear flash does not erase config" - action: hardware_script - script: | - from time import sleep_ms - dev.clear_config() - dev.write_config("preserved") - dev.clear_flash() - sleep_ms(500) - result = dev.read_config() == b"preserved" - expect_true: true - mode: [hardware] - - name: "Config survives data file operations" action: hardware_script script: | from time import sleep_ms - dev.clear_config() - dev.write_config('{"cal": true}') + from daplink_bridge import DaplinkBridge + bridge = DaplinkBridge(i2c, address=0x3B) + bridge.clear_config() + bridge.write_config('{"cal": true}') dev.set_filename("TEST", "CSV") dev.clear_flash() sleep_ms(500) dev.write_line("row1") sleep_ms(100) - result = dev.read_config() == b'{"cal": true}' + result = bridge.read_config() == b'{"cal": true}' expect_true: true mode: [hardware] diff --git a/tests/scenarios/steami_config.yaml b/tests/scenarios/steami_config.yaml index 26f464b6..b3d29417 100644 --- a/tests/scenarios/steami_config.yaml +++ b/tests/scenarios/steami_config.yaml @@ -5,9 +5,9 @@ i2c: id: 1 i2c_address: 0x3B dependencies: - - daplink_flash + - daplink_bridge mock_init: | - class FakeFlash(object): + class FakeBridge(object): def __init__(self): self._data = b"" def read_config(self): @@ -18,12 +18,10 @@ mock_init: | self._data = data def clear_config(self): self._data = b"" - def clear_flash(self): - pass - dev = SteamiConfig(FakeFlash()) + dev = SteamiConfig(FakeBridge()) hardware_init: | - from daplink_flash import DaplinkFlash - dev = SteamiConfig(DaplinkFlash(i2c)) + from daplink_bridge import DaplinkBridge + dev = SteamiConfig(DaplinkBridge(i2c)) tests: # ----- Mock ----- @@ -31,7 +29,7 @@ tests: - name: "Load empty config returns empty data" action: script script: | - dev._flash._data = b"" + dev._bridge._data = b"" dev.load() result = dev.board_revision is None and dev.board_name is None expect_true: true @@ -44,7 +42,7 @@ tests: dev.board_revision = 3 dev.board_name = "STeaMi-01" dev.save() - dev2 = SteamiConfig(dev._flash) + dev2 = SteamiConfig(dev._bridge) dev2.load() result = dev2.board_revision == 3 and dev2.board_name == "STeaMi-01" expect_true: true @@ -214,7 +212,7 @@ tests: hard_iron_x=5.5, hard_iron_y=-2.2, hard_iron_z=0.3, ) dev.save() - dev2 = SteamiConfig(dev._flash) + dev2 = SteamiConfig(dev._bridge) dev2.load() cal = dev2.get_magnetometer_calibration() result = ( @@ -251,7 +249,7 @@ tests: - name: "Save and load config on hardware" action: script script: | - dev._flash.clear_config() + dev._bridge.clear_config() dev._data = {} dev.board_revision = 3 dev.board_name = "STeaMi-Test" @@ -259,7 +257,7 @@ tests: dev.save() from time import sleep_ms sleep_ms(200) - dev2 = SteamiConfig(dev._flash) + dev2 = SteamiConfig(dev._bridge) dev2.load() result = ( dev2.board_revision == 3 @@ -273,14 +271,16 @@ tests: action: script script: | from time import sleep_ms - dev._flash.clear_config() + from daplink_flash import DaplinkFlash + dev._bridge.clear_config() dev._data = {} dev.board_name = "Persist" dev.save() sleep_ms(200) - dev._flash.clear_flash() + flash = DaplinkFlash(dev._bridge) + flash.clear_flash() sleep_ms(500) - dev2 = SteamiConfig(dev._flash) + dev2 = SteamiConfig(dev._bridge) dev2.load() result = dev2.board_name == "Persist" expect_true: true @@ -297,12 +297,12 @@ tests: ref = hids.temperature() raw = hts.temperature() offset = ref - raw - dev._flash.clear_config() + dev._bridge.clear_config() dev._data = {} dev.set_temperature_calibration("hts221", gain=1.0, offset=offset) dev.save() sleep_ms(200) - dev2 = SteamiConfig(dev._flash) + dev2 = SteamiConfig(dev._bridge) dev2.load() dev2.apply_temperature_calibration(hts) calibrated = hts.temperature() @@ -314,7 +314,7 @@ tests: action: script script: | from time import sleep_ms - dev._flash.clear_config() + dev._bridge.clear_config() dev._data = {} dev.set_magnetometer_calibration( hard_iron_x=12.3, hard_iron_y=-5.1, hard_iron_z=0.8, @@ -322,7 +322,7 @@ tests: ) dev.save() sleep_ms(200) - dev2 = SteamiConfig(dev._flash) + dev2 = SteamiConfig(dev._bridge) dev2.load() cal = dev2.get_magnetometer_calibration() result = ( @@ -339,7 +339,7 @@ tests: from time import sleep_ms from lis2mdl import LIS2MDL mag = LIS2MDL(i2c) - dev._flash.clear_config() + dev._bridge.clear_config() dev._data = {} dev.set_magnetometer_calibration( hard_iron_x=10.5, hard_iron_y=-3.2, hard_iron_z=0.7, @@ -347,7 +347,7 @@ tests: ) dev.save() sleep_ms(200) - dev2 = SteamiConfig(dev._flash) + dev2 = SteamiConfig(dev._bridge) dev2.load() dev2.apply_magnetometer_calibration(mag) result = ( @@ -363,13 +363,13 @@ tests: action: script script: | from time import sleep_ms - dev._flash.clear_config() + dev._bridge.clear_config() dev._data = {} dev.set_temperature_calibration("hts221", gain=1.05, offset=-0.3) dev.set_magnetometer_calibration(hard_iron_x=8.0, hard_iron_y=-2.0, hard_iron_z=0.5) dev.save() sleep_ms(200) - dev2 = SteamiConfig(dev._flash) + dev2 = SteamiConfig(dev._bridge) dev2.load() tc = dev2.get_temperature_calibration("hts221") mc = dev2.get_magnetometer_calibration()