This repository provides MicroPython drivers for the STeaMi board. The goal is to maintain a consistent, reliable, and maintainable codebase across all drivers.
Each driver must follow this layout:
lib/<component>/
├── README.md
├── manifest.py
├── <module_name>/
│ ├── __init__.py
│ ├── const.py
│ └── device.py
└── examples/
└── *.py
- The directory name must match the driver name (e.g.
mcp23009e,wsen-hids) - The main class must be exposed in
__init__.py - Drivers must be self-contained (no cross-driver dependencies)
- Every driver is automatically checked against the upstream firmware manifest by
tests/test_frozen_manifest.py. If a driver is intentionally not frozen (experimental, not yet integrated, etc.), add alib/<driver>/.not-frozenmarker file (it may be empty; optionally include a one-line reason) — the test will skip it and display the reason if present.
- Constants: use
from micropython import constwrapper inconst.pyfiles. - Imports: explicit imports only (
from <module>.const import NAME1, NAME2). Wildcardimport *is not allowed. - Naming:
snake_casefor all methods and variables. Enforced by ruff (N802, N803, N806). - Class inheritance:
class Foo(object):is the existing convention. - Time: use
from time import sleep_ms(notutime, notsleep()with float seconds). - Exceptions: use
except Exception:instead of bareexcept:. Enforced by ruff (E722). - No debug
print()in production driver code. Enforced by ruff (T20, examples and tests excluded). @micropython.native: do not use in driver code (device.py) — methods are dominated by I2C/SPI bus time, so native compilation gains nothing and reduces debuggability. In examples, it may be used on rendering hot-paths (tight pixel loops, math-heavy drawing functions) where a measurable speedup is expected. Do not apply it to functions with only a few comparisons or I/O calls. Note: the native emitter does not support generators, closures, orwithstatements.
- Constructor signature:
def __init__(self, i2c, ..., address=DEFAULT_ADDR)— first parameter is alwaysi2c(notbus), address uses keyword argument with a default fromconst.py. - Attributes:
self.i2cfor the I2C bus,self.addressfor the device address (notself.bus,self.addr). - I2C helpers: use private snake_case methods
_read_reg(),_write_reg()for register access. - Device identification:
device_id()— returns the device/WHO_AM_I register value. All I2C drivers with an ID register must implement this method. - Reset methods:
reset()for hardware reset (pin toggle),soft_reset()for software reset (register write),reboot()for memory reboot (reload trimming). - Power methods:
power_on()/power_off(). All drivers must implement both. - Status methods:
_status()returns the raw status register as an int (private),data_ready()returns True when all channels have new data,<measurement>_ready()for per-channel readiness (e.g.temperature_ready(),pressure_ready()). - Measurement methods: bare noun without unit suffix only for
temperature()(°C) andhumidity()(%RH). All others include the unit:pressure_hpa(),distance_mm(),voltage_mv(),acceleration_g(), etc.read()for combined reading returning a tuple,<measurement>_raw()for raw register values. - Mode methods:
set_continuous(odr)to start continuous measurements,trigger_one_shot()for a single conversion,read_one_shot()for trigger + wait + return data.
make lintTo automatically fix issues:
make lint-fixThe full configuration is in pyproject.toml ([tool.ruff.lint] section). The table below highlights the most impactful rule groups; additional groups (ASYNC, DTZ, EXE, G, ICN, INT, ISC, PGH, PIE, PYI, RSE, T10, TCH, YTT) are also enabled — refer to pyproject.toml for the complete list.
| Group | Description |
|---|---|
| A | Prevent shadowing Python builtins |
| B | Flake8-bugbear: common bug patterns |
| C4 / C90 | Comprehension style / McCabe cyclomatic complexity |
| E / W | Pycodestyle errors and warnings |
| F | Pyflakes (unused imports, undefined names, etc.) |
| I | Import sorting (isort) |
| N802, N803, N806 | PEP 8 naming: functions, arguments, and variables must be snake_case |
| PL | Pylint (complexity thresholds: max-branches=25, max-statements=65, max-args=10) |
| PERF | Perflint: performance anti-patterns |
| S110 | Flake8-bandit: no silent try/except: pass |
| SIM | Flake8-simplify: code simplification suggestions |
| T20 | No print() in production code (excluded for examples and tests) |
| RUF | Ruff-specific rules |
Some pycodestyle rules are in ruff's preview tier and must be enabled individually (because explicit-preview-rules = true). These catch formatting issues that were previously missed in PRs:
| Rule | Description |
|---|---|
| E203 | Whitespace before :, ;, or , (catches else :) |
| E225 | Missing whitespace around operator |
| E231 | Missing whitespace after , |
| E261 | At least two spaces before inline comment |
| E262 | Inline comment must start with # |
| E265 | Block comment must start with # |
| E275 | Missing whitespace after keyword (catches if(...)) |
| E301 | Blank line between methods |
| E302 | Expected 2 blank lines before function/class definition |
| E303 | Too many blank lines |
| E305 | Expected 2 blank lines after function/class definition |
| W391 | Too many newlines at end of file |
Some rules are ignored because MicroPython does not support the corresponding Python features:
| Ignored rule | Reason |
|---|---|
| B905 | zip(strict=) is not available in MicroPython |
| ISC003 | MicroPython does not support implicit concatenation of f-strings |
| PIE810 | MicroPython does not support passing tuples to .startswith() / .endswith() |
| SIM101 | isinstance() with merged tuple arguments is unreliable in MicroPython |
| SIM105 | contextlib.suppress is not available in MicroPython |
Commit messages follow the Conventional Commits format, enforced by commitlint via a git hook:
<type>[(<scope>)]: <Description starting with a capital letter ending with a period.>
Types: feat, fix, docs, style, refactor, test, ci, build, chore, perf, revert, tooling
Scopes (optional but enforced): if provided, the scope must be one of the allowed values. The scope is recommended for driver-specific changes but can be omitted for cross-cutting changes.
- Driver scopes:
apds9960,bme280,bq27441,daplink_bridge,daplink_flash,gc9a01,hts221,im34dt05,ism330dl,lis2mdl,mcp23009e,ssd1327,steami_config,vl53l1x,wsen-hids,wsen-pads,steami_screen - Domain scopes:
ci,docs,style,tests,tooling
fix(hts221): Fix missing self parameter in get_av method.
feat(ism330dl): Add driver support for temperature reading.
fix(wsen-pads): Correct pressure conversion formula.
docs: Update README driver table.
test(mcp23009e): Add mock scenarios for mcp23009e driver.
For local development (without dev container):
- Python 3.10+
- Node.js 22+ (for husky, commitlint, lint-staged, semantic-release)
arm-none-eabi-gcctoolchain (formake micropython-firmware;make daplink-firmwareignores the system toolchain and downloads its own pinned 10.3-2021.10 build)ccacheandninja-build(formake daplink-firmware)- For
make daplink-firmware: Linux x86_64, Linux aarch64, or Intel macOS only (the pinned toolchain is not published for Apple Silicon or Windows — use the dev container on those platforms) pyocd(formake micropython-deploy, installed viapip install -e ".[flash]")- OpenOCD (optional, for
make micropython-deploy-openocd) mpremote(installed viapip install -e ".[test]")- GitHub CLI (
gh)
Then run make setup to install all dependencies and git hooks. This creates a .venv with ruff, pytest, mpremote, and MicroPython type stubs for Pylance.
A dev container is available for VS Code (local Docker only, not GitHub Codespaces). It includes all prerequisites out of the box: Python 3.10, Node.js 22, ruff, pytest, mpremote, pyOCD, arm-none-eabi-gcc, OpenOCD, ccache, ninja-build, and the GitHub CLI.
- Open the repository in VS Code
- When prompted, click Reopen in Container (or use the command palette: Dev Containers: Reopen in Container)
- The container runs
make setupautomatically on creation
The container also provides:
- zsh + oh-my-zsh as default shell with persistent shell history
- Pylance configured with MicroPython STM32 stubs via
[tool.pyright]inpyproject.toml(no falseimport machineerrors) - Serial Monitor extension for board communication
- USB passthrough for mpremote, pyOCD, OpenOCD, and MicroPython firmware flashing (the container runs in privileged mode with
/dev/bus/usbmounted) - udev rules for the DAPLink interface (auto-started on container creation)
Note: GitHub Codespaces is not supported because the container requires privileged mode and USB device access for board communication.
- Set up your environment: open in the dev container, or run
make setuplocally - Create a branch from main (format:
feat/,fix/,docs/,tooling/,ci/,test/,style/,chore/,refactor/) - Write your code and add tests in
tests/scenarios/<driver>.yaml - Run
make cito verify everything passes (lint + tests + examples) - Commit — the git hooks will automatically check your commit message and run ruff on staged files
- Push your branch and open a Pull Request
If git hooks fail because node_modules/ is missing (for example on a fresh clone or after make deepclean), run make setup or npm install before committing.
The repository enforces LF line endings on all text files via .gitattributes. This prevents CRLF shebangs from breaking husky hooks on Windows + WSL. If you have an existing clone on Windows, run git add --renormalize . after pulling the .gitattributes change to re-normalize your working tree.
All pull requests must pass these checks:
| Check | Workflow | Description |
|---|---|---|
| Commit messages | check-commits.yml |
Validates commit message format |
| Linting | python-linter.yml |
Runs ruff check |
| Mock tests | tests.yml |
Runs mock driver tests |
| Example validation | tests.yml |
Validates example files syntax and imports |
| Frozen manifest | tests.yml |
Verifies every lib/*/ driver is declared in the upstream firmware manifest |
Releases are handled automatically by semantic-release when commits are pushed to main. The version is determined from commit messages:
fix:→ patch bump (v1.0.0 → v1.0.1)feat:→ minor bump (v1.0.0 → v1.1.0)BREAKING CHANGE:in commit body → major bump (v1.0.0 → v2.0.0)docs:,style:,test:,ci:,chore:→ no release
semantic-release automatically updates pyproject.toml, generates CHANGELOG.md, creates a git tag, and publishes a GitHub release with both firmwares attached:
- MicroPython firmware:
steami-micropython-firmware-vX.Y.Z.hex/.bin - DAPLink interface firmware:
steami-daplink-firmware-vX.Y.Z.hex/.bin
To force a specific version manually (override):
make bump # patch: v1.0.0 → v1.0.1
make bump PART=minor # minor: v1.0.1 → v1.1.0
make bump PART=major # major: v1.1.0 → v2.0.0The STeaMi board has two distinct firmwares:
- MicroPython firmware — runs on the STM32WB55 main MCU and exposes the drivers from this repository
- DAPLink firmware — runs on the STM32F103 interface chip and provides the I2C bridge, mass-storage, and CMSIS-DAP debug interface
The drivers in this repository are "frozen" into the MicroPython firmware. The Makefile automates cloning, building, and flashing both firmwares.
make micropython-firmware # Clone micropython-steami (if needed), link local drivers, build
make micropython-update # Refresh the MicroPython clone and board-specific submodules
make micropython-deploy # Flash MicroPython firmware via pyOCD (default)
make micropython-deploy-openocd # Flash MicroPython firmware via OpenOCD (alternative)
make micropython-deploy-usb # Flash MicroPython firmware via DAPLink USB mass-storage (alternative)
make micropython-clean # Clean MicroPython firmware build artifacts
make run SCRIPT=lib/steami_config/examples/show_config.py # Run with live output
make deploy-script SCRIPT=lib/.../calibrate_magnetometer.py # Deploy as main.py for autonomous use
make run-main # Re-execute main.py on the boardThe legacy short names (make firmware, make deploy, etc.) are deprecated and now print an error message asking which firmware (MicroPython or DAPLink) you intended to target.
The MicroPython firmware source is cloned into .build/micropython-steami/ (gitignored). A symbolic link replaces the submodule lib/micropython-steami-lib with your local working directory, so the firmware always includes your latest changes — even uncommitted ones.
Use make micropython-firmware for normal rebuilds from the existing local clone. Use make micropython-update only when you want to refresh the micropython-steami checkout itself or resync the board-specific submodules before rebuilding.
All these tools are included in the dev container. For local development, see the Prerequisites section.
DAPLink is the firmware running on the STM32F103 interface chip. It provides the USB mass-storage, CMSIS-DAP debug interface, and the I2C bridge used by daplink_bridge / daplink_flash / steami_config.
DAPLink consists of two parts:
- Bootloader (first stage, flashed at
0x08000000) — installed once at the factory, rarely updated. It provides the MAINTENANCE mode used to update the interface firmware. Updating it requires an external SWD probe. - Interface firmware (second stage, flashed at
0x08002000) — contains the I2C bridge, mass-storage, debug interface. Updated routinely, either via the MAINTENANCE USB volume or via an external SWD probe for recovery.
make daplink-firmware # Build the interface firmware (stm32f103xb_steami32_if)
make daplink-bootloader # Build the bootloader (stm32f103xb_bl)
make daplink-update # Refresh the DAPLink clone
make daplink-clean # Clean DAPLink build artifacts
# Routine interface update (no external probe)
make daplink-deploy # Alias for daplink-deploy-usb
make daplink-deploy-usb # Flash interface firmware via MAINTENANCE volume
# External SWD probe required (recovery, CI, bricked boards)
make daplink-deploy-pyocd # Flash interface firmware via SWD (pyocd)
make daplink-deploy-openocd # Flash interface firmware via SWD (openocd)
make daplink-deploy-bootloader # Flash bootloader via SWD (default: pyocd)
make daplink-deploy-bootloader-pyocd # Flash bootloader via SWD (pyocd)
make daplink-deploy-bootloader-openocd # Flash bootloader via SWD (openocd)The DAPLink source is cloned from steamicc/DAPLink into .build/DAPLink/ (gitignored). A Python virtualenv is created automatically inside the clone for the progen build tool.
MAINTENANCE mode (USB path): power on the board with the RESET button held until a MAINTENANCE USB volume appears (instead of the usual STeaMi volume). make daplink-deploy-usb copies the interface firmware to that volume and the board reboots automatically.
External SWD probe (bootstrap warning): the daplink-deploy-*-pyocd / -openocd and all daplink-deploy-bootloader* targets flash the DAPLink chip directly via SWD. They require an external probe (ST-Link, J-Link, or another CMSIS-DAP board) connected to the target board's SWD header. A board cannot reflash its own on-board DAPLink via its own SWD pins — use another board or a standalone probe. These paths are useful for recovering a bricked interface firmware, installing the bootloader at the factory, or automating CI flashing without manual button presses.
The SWD commands assume an ST-Link probe by default. To use a different probe (another DAPLink board, J-Link, …), override the OpenOCD or pyOCD configuration via DAPLINK_OPENOCD_INTERFACE, DAPLINK_OPENOCD_TRANSPORT, or DAPLINK_PYOCD_TARGET (see env.mk).
- Keep implementations simple and readable
- Follow existing drivers as reference
- Ensure documentation (
README.md) and examples are included for each driver