Board reader
The board’s own devicetree, read for real: preprocessed, parsed, and reduced to the sockets, buses and controllers the analyzer needs. This is the stage that needs a build recipe — include directories and bindings — and the one that runs the C preprocessor.
rigc.board
The board reader: the board’s own devicetree, read for real.
- resolve.py – board NAME -> .dts path (explicit –board-dts, or
standalone discovery) plus the two board-level diagnostics (unknown board, board not rig-enabled)
- project.py – edtlib.EDT -> model.Board projection: every socket,*
node’s gpio/pwm/adc maps, buses and cs-pool default
- edt_build.py – the generic edtlib.EDT construction (cpp + bindings),
BSD-3-Clause, no product imports (see its own header)
- census.py – the text-only west rigs –boards-for census, over
board sources rather than a real edtlib read
resolve.load_board is the production entry point (cli.py calls it right before the analyzer runs); project.load_board/project_edt are its edtlib-side counterpart, callable directly against an already-built EDT (the unit tests’ path, no cpp). census.census_boards/boards_for serve the –boards-for query and never touch a real devicetree.
rigc.board.resolve
Board DT reader – analyzer-side input: the analyzer reads the board DT to find socket nodes by compatible. Delegates entirely to project/edt_build’s edtlib.EDT reader over the board’s own devicetree.
This module keeps two responsibilities of its own, both about board RESOLUTION rather than DT mechanics (those live in project/edt_build):
board NAME -> .dts path, explicit (the in-build path: dts.cmake already resolved BOARD_DIR via boards.cmake, so it passes –board-dts directly) or discovered (the standalone/CLI fallback, via zephyr’s own list_boards.py – consumed, not forked).
the two board-level diagnostics that keep “phys-board” physically meaningful: a board that does not exist at all (discovery finds no such directory) vs. a board that exists but never opted in (its devicetree declares no socket,* node).
Diagnostics and dependency data are RETURN values: load_board returns (Board | None, diagnostics, Deps) rather than writing into accumulators handed in from outside – the board’s own .dts joins the same returned-value deps shape every other reader in this package already uses.
rigc.board.project
Board DT reader, edtlib-based – the production reader resolve.load_board delegates to. It projects a real board’s own devicetree, read via a standalone edtlib.EDT (see edt_build.py), onto model.Board / model.BoardSocket: the analyzer reads the board DT to find socket nodes by compatible. model.py’s dataclasses are populated here, never redefined.
pwm_map / adc_map project the socket node’s standard pwm-map /
io-channel-map nexuses – read the same way gpio-map already is, via
edtlib.Node.maps() – onto a position -> (controller label, channel) shape
(see boards/extend/seeed/seeeduino_lotus/grove_sockets.dtsi). Not every
socket carries these maps (only PWM/ADC-capable ones do); node.maps simply
omits the key for a *-map property the node doesn’t author, so the loops
below are no-ops for sockets without one.
rigc.board.edt_build
Standalone edtlib.EDT construction over a single real devicetree file.
This is a generic reader layer, under a BSD-3-Clause license (see SPDX header above), deliberately decoupled from the Apache-2.0 product layer – it knows nothing about rigs, sockets, or any other rigc product concept, only devicetree/edtlib mechanics plus the one piece of Zephyr CMake convention (a build_info.yml’s cmake.devicetree section) needed to recover the include/bindings directories a real west build used. It is the candidate for upstreaming into python-devicetree itself, so it must never import a rigc product module (model / analyzer / diag / loader) – only the standard library, PyYAML, and devicetree.edtlib.
Recipe (mirrors cmake/modules/dts.cmake + scripts/dts/gen_defines.py): cpp the board .dts with -nostdinc plus one -isystem per include dir and -D__DTS__ (no other defines – linemarkers stay intact, so dtlib/edtlib source references point at the ORIGINAL board files, not the preprocessed temp file), then hand the preprocessed file plus the bindings dirs to edtlib.EDT.
$ZEPHYR_BASE is needed only to locate the devicetree package itself (scripts/dts/python-devicetree/src) – this module carries no other tie to a Zephyr checkout. That lookup is deferred to ensure_devicetree_on_path(), called from build_edt() rather than at import time, so a caller that only needs BuildRecipe / recipe_from_build_info / preprocess (none of which touch devicetree.edtlib) can use this module with $ZEPHYR_BASE unset.
rigc.board.census
A text-only census of board rig-extensions, and the query it answers: west rigs –boards-for <target>.
This module is namespace-agnostic and stays that way: boards_for takes a loaded Rig, so whether the caller got that Rig from a persisted rig.yml or from a promoted shield’s synthesized pair is settled entirely in rigs.py before anything here runs. Nothing below needs to learn the difference.
The claim, bounded: reading a board’s REAL devicetree
needs cpp + edtlib + a BuildRecipe, and every board this tooling can build
is an hwmv2 extension a standalone catalog scan never sees – a real
per-board read costs a real cmake configure per candidate. That is not a
query. So this module censuses board rig-extension SOURCES instead
(*.dts/*.dtsi text, regex, no dtlib) and answers a narrower, cheaper
question: which boards’ typed sockets satisfy a rig’s socket requirements
– reference resolution (defining label or conventional alias), connector-
type mating, bus subset exposure, and stackability. It is NOT a promise
the rig builds there: GPIO position routing, CS-pool allocation, address
domains and net analysis all need the board’s real devicetree, which a
text scan cannot see.
The dash trap: compatible = “socket,<type>” names the type with dashes (e.g. “arduino-r3”) – project.py’s own _project_socket keeps them, since the value feeds mating_ok against shield.plugs, which is the identical dashed spelling. This module’s census keeps the SAME dashed form for exactly that reason. A caller wanting the label-CONVENTION check (does a socket carry a label matching its type, “<type>” or “<type>_<silkscreen>”) underscores it there, once, for that one comparison only – never here, and never by mutating a CensusBoard’s type_name in place.
The partial Board this census can build: only label,
type_name, buses (membership, not target – see census_board), src and
path are real; gpio_map/pwm_map/adc_map/cs_pool stay empty/None, since a
text scan cannot resolve a *-map’s phandle target or a binding’s default.
Its only valid consumer is analyzer.sockets.resolve_sockets, which is
exactly what boards_for runs – ONE implementation of the mating/subset/
alias/stackability rule, never restated here. An unrouted gpio_map is
harmless for that rule (compose_socket treats an unrouted parent
position as socket-local); bus pass-through still checks, which is the
part that matters. resolve_sockets’s stackability sweep indexes
types[type_name], reachable only once a shield has already mated the
socket (so its type is known-valid) – no KeyError is reachable here,
and this module does not “defend” against one with a silent .get.
`–rigs-for` is deliberately NOT implemented: the inverse query – which rigs a given board satisfies – is the same census read backwards, but needs every rig loaded, and it is not on the critical path this slice serves. Noted here as the considered non-implementation, not an oversight.
Known limitation, not a bug: a rig that declares its board PER VARIANT with a sockets: map (ard_datalogger) loads its instances with that variant’s board-specific socket labels baked in (nucleo_ard for the default nucleo variant) – so –boards-for ard_datalogger answers nucleo alone, and –boards-for ard_datalogger/ frdm answers frdm alone. That is CORRECT under today’s coordinate (the rig’s content already commits to one board’s labels); it is exactly the portability gap that content migration to conventional labels and strict board/rig symmetry exist to open up.
rigc.dtsio
DTS plumbing for the SHIELD-template side, and for shield-declared
per-instance-parameter token vocabularies (shield,param-includes:). This
module never touches the board DT (that is an analyzer-slice concern);
what’s here is cpp + stock dtlib parsing of .shield translation units
(shield templates are pre-instantiation text with no binding/schema of
their own, so there is nothing for edtlib to attach type info to),
dt-bindings/connector/*.h position-index header parsing, and
resolve_token/check_include, the per-instance-parameter mechanism’s own
synthetic-TU resolution.
The cpp/unit-test seam: cpp is a subprocess, so nothing that invokes it is unit-testable. run_cpp/parse_dts/parse_tu/check_include/ resolve_token are integration-only by construction; is_int_literal, words, render_prop, src_of are pure and get unit tests directly.
No module-scope $ZEPHYR_BASE lookup: devicetree.dtlib is located via get_dtlib(), called only from inside a function – pytest imports every module in a directory before a marker expression (e.g. -m “not build”) deselects any one item, so a module-scope lookup would break collection for selections that never run it.