Imported from wirenboard/wb-mge (
AGENTS.md). Install upstream withnpx skills add wirenboard/wb-mge. Copyright stays with the author.
Agent Instructions
Build or Test Command
make build-frontend unittests test-frontend build-idf-project
To run all tests:
make test
Every recipe that needs the ESP-IDF sources scripts/idf_env.sh internally — no manual source needed. That script activates the EIM toolchain (~/.espressif/tools/activate_idf_<tag>.sh) when it is installed, otherwise falls back to the IDF_PATH already in the environment (Docker, CI with export.sh), and in both cases verifies that the IDF actually in use is the expected version.
The expected version is pinned in four places, all of which must agree: EIM_IDF_VERSION in Makefile (default v5.4.4), the base image tag in Dockerfile (FROM espressif/idf:v5.4.4), the idf version range in main/idf_component.yml (>=5.4.4,<5.5.0) and the resolved idf version in dependencies.lock (5.4.4). Two mechanical checks enforce that: make check-idf-pins — a prerequisite of apply-idf-patches, build-idf-project, build-idf-project-qemu and flash — compares EIM_IDF_VERSION with every FROM espressif/idf: tag and with the lock, and scripts/idf_env.sh compares the IDF actually in use with EIM_IDF_VERSION. The range in main/idf_component.yml is not a third check: the component manager evaluates it only when it really re-resolves the dependencies, and with the lock committed and the manifest untouched that never happens — so it does not catch an off-pin build. It applies when a re-resolve is triggered (no lock, edited manifest, changed set of direct dependencies), rejecting an IDF outside the range with no versions of idf match.
Each check has its own escape hatch, and each disables only itself: IDF_VERSION_CHECK=0 skips the installed-vs-expected comparison in scripts/idf_env.sh (IDF_PATH must still be a real checkout); IDF_PINS_CHECK=0 skips check-idf-pins (Makefile vs Dockerfile vs dependencies.lock) with a warning. Neither affects the range in main/idf_component.yml — that one has no override and must be widened in the file.
So make EIM_IDF_VERSION=v5.5.2 ... alone is not enough to build off the pin. Building against another IDF on purpose (e.g. v5.4.2, to reproduce the uart_set_pin regression) means: install that IDF, widen the range in main/idf_component.yml by hand — editing the manifest is what triggers the re-resolve that checks the range, so it has to cover the target IDF — then make IDF_PINS_CHECK=0 EIM_IDF_VERSION=v5.4.2 build-idf-project. Such a build re-resolves the dependencies and rewrites dependencies.lock — revert it and the manifest afterwards. See "Building against a different ESP-IDF" in README.md.
IDF_ENV_QUIET=1 suppresses the "Using ESP-IDF ..." banner.
Running individual QEMU API tests
To run a specific test file against QEMU:
make qemu-test PYTEST_ARGS="23_test_tx_disabled.py"
To run a specific test function:
make qemu-test PYTEST_ARGS="23_test_tx_disabled.py::test_tx_disabled_blocks_uart_transmission"
The make qemu-test target:
- Rebuilds the QEMU firmware (incremental)
- Creates the QEMU flash image
- Launches QEMU
- Runs pytest with
--qemuflag - Kills QEMU after tests complete
make qemu-coverage builds an instrumented firmware, runs the suite (reboot tests excluded), pulls .gcda over GET /gcov, and writes a branch-coverage report to build/qemu_coverage/index.html.
make coverage-combined merges the host unit-test tracefiles (make coverage) with the QEMU e2e tracefile (make qemu-coverage) into one report at build/combined_coverage/index.html (merge-only; line/function coverage is a union, branch coverage is not comparable across the two compilers and is indicative only).
Collecting (listing) QEMU API tests without running them
To verify that pytest can find and collect all tests in a file — without building firmware or launching QEMU — use:
make qemu-collect-only
Rules
- Always build before delivering results to the user. Run the build command above and confirm it succeeds before presenting any changes.
- Always run tests after every fix. After each code change, run the relevant test suite and confirm all tests pass before proceeding. For backend (C) changes:
make unittests. For frontend changes:make test-frontend. - Always update documentation when renaming or changing build targets. After any change to
Makefileorqemu.mk(adding, removing, or renaming targets; changing dependencies), grep all*.mdfiles for the old target names and update every occurrence. README.md, README_QEMU.md, AGENTS.md, andbugs/*/README.mdall reference make targets by name. - Fixing a bug: follow Red-Green-Refactor. First write a test that reproduces the bug and confirm it fails (Red); then write the fix and confirm the test now passes (Green); then refactor while keeping the test green.
- After writing new QEMU API tests, always verify collection. Run
make qemu-collect-only PYTEST_ARGS="<filename>"immediately after writing or modifying a test file to confirm pytest can collect the tests (no syntax errors, no import errors) before proceeding.
Backend (Embedded C) Coding Standards
Full style guide: https://raw.githubusercontent.com/wirenboard/codestyle/refs/heads/master/embedded_c.ru.md
Naming
- Use
snake_caseeverywhere — no CamelCase for functions or variables. - All functions and
#defines must be prefixed with the module/library name (e.g.mcp230xx_read_gpio()). typedef-d types end with_t; plainstructnames do not.- Enum variants must start with the enum type name as prefix (e.g.
W1_TRANSACTION_SENDfor enumw1_transaction).
Declarations
- Functions not intended for external use must be
static. - Prototypes of
staticfunctions go in the.cfile; public interface goes in the.hfile. - No magic numbers — use
#defineconstants.
Formatting
- 4 spaces per indentation level; tabs are forbidden.
- Max line length: 120 characters.
- Binary operators surrounded by spaces; unary operators are not.
- Always use braces
{}around block bodies, even single-line ones. - Opening brace on the same line as the statement, except for function definitions and multi-line conditions — those get the brace on a new line.
- Operations must always be explicitly parenthesised even when precedence is obvious:
(a || (b && (!c))).
Macros
- Avoid preprocessor macros where language features suffice; prefer
const,static inline, or compile-timeif. - Macros that contain statements must use
do {} while (0). - All macro names are UPPER_CASE.
Comments
- All code comments must be written in English.
- Short comments go after the line; long comments go before the line.
//is aligned to the nearest position that is a multiple of 4 characters; one space between//and the text.
Frontend Coding Standards
SVG Icons
- All SVG files must reside in the
assets/folder as separate files — no inline SVG markup in templates.
Inline Styles
- No inline styles (
style="...") are allowed anywhere in templates.
Localization
- Every user-visible text string in the UI must be localized via the i18n system. No hardcoded strings in templates or scripts.
Comments
- A comment is warranted only where the code is not self-explanatory (complex logic or an edge case).
- A comment must not duplicate what the code already states: a function/variable name, a test
describe/ittitle, a component/class name in the markup, or a description of how something is laid out in CSS. - No divider/banner comments (
// ==========) or bare structural section labels (// Props,// State,/* Header */) — they add nothing the code does not already convey.
CSS Class Naming (BEM)
We use BEM (Block, Element, Modifier) methodology for CSS class names.
- Block — independent component:
.button,.menu,.card - Element — part of a block:
.button-icon,.menu-item,.card-title - Modifier — variant of a block or element:
.button-iconMobile,.menu-itemActive,.card-titleWithButton
Block→Element separator: - (hyphen).
Element→Modifier separator: camelCase (no additional separator): .menu-itemActive.
Hardware: GPIO and Interface Pin Mapping
Ethernet Interface RTL8201FI
| ESP32 | GPIO18 | GPIO23 | GPIO0 | GPIO5 | GPIO19 | GPIO22 | GPIO21 | GPIO25 | GPIO26 | GPIO27 |
|---|---|---|---|---|---|---|---|---|---|---|
| RTL8201FI | MDIO | MDC | CLK | RST | TXD0 | TXD1 | TXEN | RXD0 | RXD1 | CRS_DV |
GPIO Expander TCA9535 (I2C address: 0x20)
ESP32 connects to TCA9535 via: SDA → GPIO32, SCL → GPIO33.
In the table below, ON means a logic high on the port enables the node. For the Status LED, if the port is in HiZ state, the LED is on.
| TCA9535 port | Function | Logic |
|---|---|---|
| PD00 | RS485-1 terminator | ON |
| PD01 | RS485-2 terminator | ON |
| PD02 | RS485-1 pull-up | ON |
| PD03 | RS485-2 pull-up | ON |
| PD04 | LED Wi-Fi | OFF |
| PD05 | LED Eth | OFF |
| PD06 | VOut and LED VOut | ON |
| PD07 | Status LED | ON |
| PD10 | MIO disable | OFF |
RS-485 Interfaces
| ESP32 | GPIO10 | GPIO9 | GPIO4 | GPIO14 | GPIO12 | GPIO15 |
|---|---|---|---|---|---|---|
| RS485-1 | TX | RX | RTS | |||
| RS485-2 | TX | RX | RTS |
Note: for ESP32, RX is input and TX is output.
Important: Inside the module, the MIO part on STM32 is connected to RS485-2 and operates via Modbus RTU at the Modbus address printed on the device label. The MIO part can be disabled via the TCA9535 PD10 pin.
Buttons
| ESP32 | GPIO34 |
|---|---|
| Config (B1) | + |
Note: the button drives the ESP32 pin to logic low when pressed.
QEMU and tests — gotchas
Killing QEMU correctly
pkill -9 qemu-system-xtensa silently fails (stderr warning, exit 1, zero
processes killed): the process name qemu-system-xtensa is longer than 15
characters, and without -f pkill matches against the 15-char-truncated name
in /proc/PID/comm, which mismatches the real one.
Always use -f (match against the full command line):
pkill -9 -f qemu-system-xtensa
Verify it actually died:
pgrep -af qemu-system-xtensa # must be empty
ss -tnlp 2>/dev/null | grep 8080 # port must be free
Killing the wrapper bash script does NOT kill the QEMU child
pkill -9 -f make qemu-tests kills the script but the qemu-system-xtensa it
spawned keeps running → it still holds port 8080 → the next QEMU cannot bind
(Could not set up host forwarding rule) → the next run is garbage. After
killing any test-runner script, always also explicitly kill its children:
pkill -9 -f qemu-system-xtensa
pkill -9 -f pytest
qemu_flash.bin is a writable MTD!
The firmware writes to the NVS partition and those changes persist in the
file across QEMU runs. Tests that change network settings (e.g. eth_dhcpc=false
plus a static IP) leave the flash in a state where the next boot makes the
server unreachable on the QEMU network (hostfwd only NATs to the
DHCP-assigned 10.0.2.15). Before each test run always regenerate the image:
rm -f build/qemu_flash.bin && make qemu-create-flash-image
Shell command style
Claude Code's circuit breakers fire even in bypass mode on "dangerous-looking" commands. To avoid getting stuck on confirmation prompts, follow these rules:
Don't chain commands with ; or && on one line
Each command is a separate Bash call. Exception: trivial cd dir && cmd and etc
If there are more than two steps, write a script and run it.
No ps … | grep … | awk … | xargs kill
To kill processes by name, use:
pkill -f '<stable pattern>'
kill -9 only when pkill (SIGTERM) already failed, and only against a
specific PID you just obtained and displayed in the output. Never kill
processes based on ps | grep results without explicitly verifying the PID.
Destructive operations — one per call
rmwith a glob (rm -f dir/*) — separate command, no neighbors on the line.- Truncating redirect (
> file) — separate command. rm -rfon paths with variables ($VAR,~,$HOME/…) — forbidden. Firstechothe path, confirm it expanded correctly, then delete.- Never
rm -rf /…orrm -rf ~under any framing.
Background processes
nohup … &, disown, setsid — separate command. Do not silence stderr
(2>/dev/null is forbidden when launching workers — the log is needed for
diagnostics). After launch, print the PID and verify with ps -p $PID as
a separate call.
Long sequences — use a script
If you need to: stop old → clean up → start new — that's three steps,
three calls. Or put them in scripts/<task>.sh, commit it, and invoke the
script. Easier to review, and the circuit breakers stay quiet.
What NOT to do (antipatterns from real interruptions)
# ❌ everything on one line, kill via grep, glob rm, truncate, nohup, hidden stderr
cd /x; ps -ef|grep foo|grep -v grep|awk '{print $2}'|xargs -r kill -9; sleep 2
> /x/out.txt; rm -f /x/fails/* 2>/dev/null
nohup /x/run.sh > /x/log 2>&1 &
Split this into 4–5 separate calls with clearly named steps.
