Contributing¶
MicroPython firmware (v1.29, ESP32-S3) for a battery-powered Home Assistant e-paper dashboard. Bug reports and merge requests are welcome; this page describes the structure, rules and tools.
Structure¶
src/– modules copied flat onto the board’s file system.Only
board.pyand the entry points (boot.py,main.py,*_monitor.py) accessmachine/networkdirectly; other modules get hardware objects injected.Drivers (such as
max17048.py) get their bus injected, which makes them testable on the host.Logic (formatting, layout, evaluation) lives in pure modules without hardware access.
Data sources (Home Assistant, later SignalK for instance) are interchangeable: they are created in the entry point and injected, deliver values in a source-neutral format, and layout and display know no concrete source.
src/defaults.py– all default settings (versioned).src/config.py– the configuration of your installation, ignored by git (template:config_example.py):from defaults import *, then credentials (WLAN, Home Assistant, MQTT) and everything that differs. Firmware and tools read onlyconfig; tests readdefaults(config.py is missing from the repository).tools/– helper scripts for the PC (run under CPython).tests/– pytest on CPython;tests/fakes.pyholds hardware doubles (such asFakeI2C).docs/– this documentation (Sphinx with MyST Markdown); the API reference is generated from the docstrings.
Rules¶
New code comes with tests; hardware access is tested through fakes.
Use only language features MicroPython supports as well (no dataclasses, typing only sparingly, %-formatting for output).
Docstrings in Google style for modules, classes and public functions (ruff checks this).
Each fact lives in one place and everything else links to it: settings are explained in the docstring after them in
src/defaults.py; docstrings refer to modules, functions and settings in backticks (`firmware`,`defaults.WATCHDOG_S`), which the docs turn into links; docs pages link them as[`WATCHDOG_S`](#defaults.WATCHDOG_S).When behaviour changes, search code comments, docstrings and
docs/for statements based on the old behaviour and fix them in the same change.Before every commit:
make check(ruff lint + format check + pytest) must pass.Small commits that work on their own; English commit messages with a body explaining why.
Machine-readable values (JSON, MQTT, identifiers) in English. The texts on the display and in the web interface are German for now (a configurable language is on the backlog).
Licenses: software GPL-3.0-or-later, schematic CERN-OHL-S-2.0, enclosure/STL CC BY-SA 4.0 (see README). Adopt third-party code, fonts, icons and models only if their license fits (for code: GPLv3 compatible, such as MIT, BSD, Apache-2.0, LGPL; not GPLv2-only or “non-commercial”, for example), and add every new third-party part to
THIRD_PARTY_NOTICES.mdwith the license text from the original source. If in doubt, raise it in the merge request.
MicroPython pitfalls¶
Found while working on this board (ESP32-S3, MicroPython 1.29):
Methods like
list.__setitem__cannot be called directly (AttributeError); writeflag[0] = valuein a function, or keep state in an object attribute. Code in the entry points is not covered by the MicroPython comparison, so keep such logic in modules.zip(..., strict=True)does not exist, yet ruff (B905) suggests it; in firmware code iterate by index instead.contextlibis missing as well (ruff SIM105 suggestssuppress).time.sleep_ms/ticks_msare missing under CPython: inject waits and clocks (asleepparameter, for instance) so that modules stay testable on the host.time.gmtime()counts from 2000-01-01 until NTP has set the clock; treat times before that as invalid (clock.MIN_VALID_YEAR).wlan.connect()during a background reconnect raisesWifi Internal State Error– callwlan.disconnect()first (but not right after a radio restart, where the followingconnect()would come to nothing).The first WLAN connection attempt after a reset occasionally hangs in
STAT_CONNECTING; a second attempt after a radio restart works.WLAN power saving (default
PM_PERFORMANCE) delays answers by up to 2 s; switch it off withPM_NONEin awake mode.A soft reset (by mpremote, for instance) does not disconnect the WLAN;
boot.pythen finds an existing connection. Disconnect the WLAN cleanly beforemachine.reset(), or the router ignores the board for up to 40 s afterwards.Pins that should hold a level in deep sleep need
hold=True; on waking, first set the desired level, thenhold=False, or there is a glitch (on the display’s RST pin that would reset the controller).umqtt.simplecomputes the length of astrin characters instead of bytes: messages with umlauts get cut off. Always publish bytes (json.dumps(...).encode()).RTC memory (
machine.RTC().memory()) holds 2048 bytes and survives deep sleep; flash, on the other hand, does not last with writes every minute.Large modules (fonts) take over a second to import (compilation); import them only where needed, and precompile them (
make deploydoes that).While MicroPython blocks in C code (
socket.accept()with a long timeout, for instance), USB is not serviced: mpremote then hangs on the first write. Let blocking calls wait only in short slices (≤ 0.2 s).The watchdog (
machine.WDT) cannot be stopped again: after interruptingmain.pyvia mpremote you haveWATCHDOG_Suntil the reset.Test Home Assistant templates with real Jinja2 (
tests/test_ha_templates.py): tests with canned answers miss syntax errors that only Home Assistant rejects.
Commands¶
make check– lint and tests (including the CPython/MicroPython comparison, if built)make micropython– build the MicroPython unix port and mpy-cross intobuild/; afterwardstests/test_micropython.pyrunstests/mpy_smoke.pyunder both interpreters and compares the output – add new pure modules theremake format– format automaticallymake docs– build the documentation intodocs/_build/htmlmake deploy– compile the modules, copy them to the board via USB and restart itmake deploy-wlan– upload changed modules over the WLAN; a sleeping board is woken via the maintenance mode (MQTT)make monitor– battery state every 5 s via USBmake watch– battery state every 5 s via WLAN