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.py and the entry points (boot.py, main.py, *_monitor.py) access machine/network directly; 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 only config; tests read defaults (config.py is missing from the repository).

  • tools/ – helper scripts for the PC (run under CPython).

  • tests/ – pytest on CPython; tests/fakes.py holds hardware doubles (such as FakeI2C).

  • 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.md with 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); write flag[0] = value in 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. contextlib is missing as well (ruff SIM105 suggests suppress).

  • time.sleep_ms/ticks_ms are missing under CPython: inject waits and clocks (a sleep parameter, 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 raises Wifi Internal State Error – call wlan.disconnect() first (but not right after a radio restart, where the following connect() 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 with PM_NONE in awake mode.

  • A soft reset (by mpremote, for instance) does not disconnect the WLAN; boot.py then finds an existing connection. Disconnect the WLAN cleanly before machine.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, then hold=False, or there is a glitch (on the display’s RST pin that would reset the controller).

  • umqtt.simple computes the length of a str in 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 deploy does 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 interrupting main.py via mpremote you have WATCHDOG_S until 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 into build/; afterwards tests/test_micropython.py runs tests/mpy_smoke.py under both interpreters and compares the output – add new pure modules there

  • make format – format automatically

  • make docs – build the documentation into docs/_build/html

  • make deploy – compile the modules, copy them to the board via USB and restart it

  • make 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 USB

  • make watch – battery state every 5 s via WLAN