bussard design¶
Living design document: what bussard is, why it is feasible, the architecture, and the roadmap. Updated as decisions are taken. For a hands-on introduction read the README and howto.md; the complete command and schema reference is reference.md.
1. What we're building¶
bussard: an open-source (MIT), cross-platform CLI for KNX. No GUI.
The goal is to use a configuration-first approach for managing KNX:
- Configuration lives in YAML files in a git repo (group addresses, links, device parameters), so changes, including LLM-proposed ones, are reviewable diffs.
bussardcompiles that YAML and pushes it to the devices over the bus.bussardalso observes the bus: live monitor, telegram capture, decode against the same YAML model, device introspection.- An MCP server exposes those read/write operations as tools.
Non-goals: replacing ETS for certification, planning, or documentation. Supporting every KNX device ever made.
Design goals (north star)¶
- Easy and quick to install: a single static binary, no runtime.
- Primary target: home owners and small installations; larger ones later.
- Speed matters for every operation: flushing configurations, validating, getting feedback from the bus.
- All configuration is file based.
- Fail fast and fail gracefully; never leave the KNX system or its components in an inconsistent, non-functional state.
Why this is possible at all¶
The .knxprod product-data format is known to the public, the download
procedure is described inside the product data itself (LoadProcedures), and open-source
implementations exist for both ends of the problem, just not for the download side.
2. The layer cake¶
A KNX device's configuration is three separable things with very different difficulty:
| Layer | What | Difficulty |
|---|---|---|
| (a) Individual address | 1.1.4 |
Standardised management procedure. Trivial. |
| (b) Links | group address table, association table, com-object table | Standardised loadable parts; on System B / mask 07B0+ property-based and writable through documented interface objects. Moderate. |
| (c) Parameters | channel mode, runtime, wind-alarm behaviour, ... | Device-specific memory layout described only in the .knxprod. Hard. |
Most day-to-day changes are (b), but all three layers are in scope: the goal is the
entire installation lifecycle in YAML, from init to a running house. assign does (a);
plan/apply do (b) for System B devices. For (c), the loop is closed on System B:
device files carry a parameters: section (imported from the ETS project, validated
against the product model, #46), and flash computes the parameter memory image from
vendor defaults plus those overrides and streams it, using per-module-instance base
offsets (module_bases:, #48) to place per-channel parameters. Which path links take
(properties on System B vs. memory writes on older System 1/2) depends on the mask
version each device reports; bussard scan reports this.
3. What's inside a .knxprod¶
A ZIP containing XML (Catalog.xml, Hardware.xml, ApplicationProgram XML) plus an
RSA-1024 signature. The ApplicationProgram XML contains:
ParameterTypes: enums with value/text pairs, ints with min/max, stringsParameterswithOffset/BitOffsetinto the parameter memory blockParameterRefs,ComObjects(DPT + flags),ComObjectRefsLoadProcedures: a declarative download script (LdCtrlWriteRelMem,LdCtrlTaskSegment,LdCtrlLoadCompleted, ...) telling the tool exactly how to download into this device
The flashing mechanics are literally described in the file. Building a downloader means writing an interpreter for that little language plus the parameter-to-memory allocator. XML namespaces differ across ETS 4 / 5.x / 6.x; budget several parser variants.
The signature is authentication, not access control: it stops third parties from making
files ETS accepts; it doesn't stop reading legitimately downloaded files. Vendor
application XML is copyrighted: bussard never bundles extracted product data; users supply
their own .knxprod files (available free from manufacturer service sites and the MyKNX
catalogue). The EU interoperability exception (Software Directive 2009/24/EC Art. 6)
covers making an independently created program interoperable, the same footing as Samba
and Wine. Handling details live in product-data.md.
4. Ecosystem survey and licensing constraints¶
Surveyed 2026-09. Key constraint: bussard is MIT, so GPL code cannot be depended on, forked, or ported.
| Project | Language | License | Use for bussard |
|---|---|---|---|
knx-rs, knx-rs-prod (metaneutrons) |
Rust | GPL-3.0-only | Behavioral reference only, clean-room rules. Cannot depend/fork/port. |
calimero |
Java | GPL-2.0 + Classpath exception | Reference documentation for management procedures; clean-room rules (read behavior, write a spec, implement from the spec). |
knx-go (vapourismo) |
Go | MIT | Primary design reference for KNXnet/IP: parallel Tunnel/Router transports behind one interface. Unmaintained, so reference not dependency. |
xknxproject |
Python | MIT | Test oracle for the .knxproj importer (its JSON output vs. ours). |
thelsing/knx |
C++ | see repo | Device side of the download protocol; shows what the receiver expects. |
OpenKNXproducer |
.NET | see repo | Reference for per-ETS-version XML namespace handling. |
knxkit |
Rust | EPL-2.0/GPL-3.0 | Stalled, no routing, pre-production. Not used. |
knx-ip / KNXyz |
Rust | MIT | Too new/unaudited for the core. Watching. |
Consequence: the transport + cEMI layer is written from scratch (MIT). The wire surface
is small (~1-2 KLOC): KNXnet/IP framing, tunneling (CONNECT / CONNECTIONSTATE heartbeat /
TUNNELING_REQUEST+ACK sequence counters / DISCONNECT), routing (multicast
224.0.23.12:3671, ROUTING_INDICATION / LOST_MESSAGE / BUSY), and cEMI L_Data decode
including 6-bit small APDUs. The .knxprod reader (bussard-prod) is likewise written
from scratch: parsing XML from a ZIP; signing is never needed since bussard only reads
vendor files.
5. Architecture¶
5.1 Crate layout (Rust workspace)¶
crates/
bussard-model/ # GA/IA/DPT/flags types, DPT codecs, YAML schema, loader, validation
bussard-ets/ # shared ETS-XML primitives: streaming parsers, DPT/flag helpers, zip guard
bussard-project/ # .knxproj import: AES zip + PBKDF2, streaming XML → model
bussard-prod/ # .knxprod reading (import-product)
bussard-transport/ # trait BusConnection; Tunnel + Router impls; cEMI codec
bussard-bus/ # bus-service actor: one Transport owner, frame subscriptions, L4 leases
bussard-monitor/ # decode pipeline, SQLite capture, formatters
bussard-mgmt/ # layer-4 connection-oriented transport, management procedures, table read
bussard-download/ # LoadProcedure interpreter, table/memory image builder, plan/apply/flash
bussard-ha/ # Home Assistant config generation (ha-config)
bussard-secure/ # KNX Secure primitives: .knxkeys keyring, Data Secure session
bussard-mcp/ # MCP stdio server
bussard-viz/ # the `bussard viz` web server and its embedded frontend
bussard-cli/ # clap binary
The separation that matters most: bussard-mgmt is fiddly and protocol-correct, heavily
tested against real devices; bussard-download is pure-ish computation (YAML + product
data → byte image), unit-testable without a bus. bussard-bus owns the single transport
connection and hands out exclusive layer-4 leases so a management session and live group
traffic share one gateway tunnel slot. bussard-ets factors the streaming-XML primitives
shared by the .knxproj and .knxprod importers.
5.2 The YAML model¶
The user's KNX-as-code repo: bussard.yaml (connection), groups.yaml (the GA plan),
links.yaml (com-object → GA assignments), devices/*.yaml. The full layout and every
field are specified in reference.md; this section
keeps only the design decisions behind the schema.
- Deterministic, sorted emission, so re-imports and hand edits produce minimal diffs.
- Strict parsing everywhere: unknown fields and duplicate keys are rejected, so a typo is an error, not a silent no-op.
- Every generated file carries a banner naming what generated it and what is hand-editable; regenerated sections carry an explicit marker.
address:is a device's identity; the filename slug is cosmetic. Re-import is idempotent: pruned devices leave, renamed devices replace their old file.- The informational com-object
namelives only inlinks.yaml; device files carry no duplicate. Payloadsizeis derived from the DPT and serialized only when no DPT exists. protected: trueon a GA is the safety gate: the CLI requires--force, MCP refuses outright.- Device
parameters:store only values that differ from the vendor default (diff- friendly), keyed<name-slug>@<ref-id>because a name alone is ambiguous across module instances. They sit in the hand-editable zone but are replaced with ETS truth on re-import; the generated zone below the marker (module_bases:,com_objects:) is never hand-edited.
The loop is deliberately Terraform-shaped: import → validate → plan → apply, with
plan producing a reviewable diff. That's what makes the LLM workflow safe: the model
edits YAML, a human approves a diff, the tool executes.
5.3 Validation¶
Strict parsing, then rule passes with rustc-style diagnostics and --format json. The
diagnostic table (E001-E012) lives in
reference.md.
5.4 Monitor and capture¶
cEMI → {timestamp, source, destination GA, APCI, payload} → resolve GA to name + DPT →
typed value; source resolves to device name, and (source, GA) to the sending com object's
name (from links.yaml). Unknown GAs/DPTs degrade gracefully to raw hex, never a
failure; decode-size mismatches are shown inline as a debugging signal. capture stores
raw cEMI bytes plus a decoded snapshot in SQLite so telegrams can be re-decoded after
model fixes. The JSON Lines contract shared by monitor --json and the MCP telegram
tools is specified in reference.md.
5.5 MCP server¶
Stdio server holding the loaded model and a live bus connection feeding a bounded ring buffer. The tool list, parameters and tiers are in reference.md. The design stance:
- Read-only by default;
--passivefor a server that never transmits;knx_write_grouponly with--allow-writes, and it hard-refusesprotected: trueGAs with no MCP override. The LLM must ask a human, who can runbussard write ... --forcefrom the CLI. The write tool's description states the consequences plainly for LLM callers (actuators move; prefer asking the human when uncertain). - Programming and download (
plan,apply,flash) stay CLI-only and out of the MCP surface: they run through a plan/confirm/backup/verify ladder a human drives.
6. Roadmap¶
Phase 0, read-only and zero risk. Shipped. Import an existing .knxproj (ETS 6,
including password-protected exports) into the YAML model. monitor + capture +
decode. Read-only MCP. This alone delivers LLM-assisted debugging.
Phase 1, runtime writes. Shipped. Group value read/write from the CLI, the opt-in MCP write tool, and Home Assistant KNX config generation from the same YAML: one source of truth.
Phase 2, links. Shipped. scan (device discovery with mask/manufacturer/order
report), import-product (.knxprod reading with an order-number pointer index, see
product-data.md), assign (programming-mode address assignment),
adopt (the guided new-device wizard), and reconstruct (read a System B device's tables
back and diff, plus a --line sweep that synthesizes a fresh ETS-less model). The link
downloader itself shipped as plan (read-only table diff) and apply (backup, write,
verify) for System B devices. The workflows are documented in howto.md.
Phase 3, parameters and application download. Shipped for System B. flash does the
ETS-free application download from a .knxprod into a System B device: pre-flight plan
(mask gate and unsupported-op refusal before any write), progress, Loaded + spot-check
verification. The parameter image is computed from vendor defaults plus the device file's
parameters: overrides, with module_bases: resolving per-channel placement. Hardening
shipped along the way: A_Authorize on every management connect (flash --bcu-key for
keyed devices, free access otherwise, #52), per-chunk read-back verification, and strict
load-state checking that fails fast when a device does not honour a segment allocation.
The download runs over a single management connection for its whole duration, exactly as
ETS does; a genuinely dead connection fails cleanly, and re-running flash is safe
because the download is idempotent.
The frontier. System 7 (mask 0705, plus 0701/0700): parsed and classified, but
not flashable; its segment-based procedures (LdCtrlAbsSegment, LdCtrlWriteMem to
absolute segments) are refused at pre-flight. 57B0 (KNXnet/IP System B) is likewise
refused; the gates require exactly 07B0. LdCtrlTaskSegment/LdCtrlTaskCtrl1 and
unrecognized ops (notably LdCtrlCompareRelMem, a read-and-compare that blocks two
otherwise-executable MDT apps) are refused whole; LdCtrlCompareRelMem is the top
roadmap op. These remain ETS-only until implemented and validated byte-wise against ETS
dumps.
The interop wall¶
The management stack is validated against thelsing/knx as an independent foreign peer,
built and run as a separate process over KNXnet/IP routing by the virtual-device test
harness (see tests-support/virtual-device/README.md):
assign and scan clear it. But the only thelsing binary that speaks KNXnet/IP routing
multicast reports mask 57B0 (KNXnet/IP System B), while reconstruct/apply/flash
gate on 07B0 (TP System B). There is no stock thelsing binary that both speaks IP
multicast and reports 07B0, so the table-level rungs cannot be reached over routing
without either relaxing the mask gate to also accept 57B0 (they share the same
BauSystemBDevice table stack), a modified thelsing variant, or driving knx-linux-tp
over a TP-UART. The full table cycle against a foreign peer is therefore still pending;
validating it needs a 07B0-reporting device (a spare TP actuator, or a TP-UART
variant).
KNX Virtual notes¶
KNX Virtual (Windows) is useful for discovery and assign, but is not a faithful flash
target and is not what flash is tuned for:
- It reports
Loadedimmediately after StartLoading instead of the conformantLoading, and it wedges and drops the L4 connection when a too-large foreign vendor application is written onto a device that cannot hold it. Earlier releases carried KV-specific flags (--tolerate-nonconformant-load-states,--pace,--verify batched, windowed--reconnect-everyreconnect downloads) to work around this. Those were later proven to be papering over an over-sized-application mistake, not a real protocol issue: real KNXnet/IP gateways and real devices hold one stable connection for the whole download and report conformant load states, exactly as ETS does. The workarounds were removed; load-state handling is now strict, so a device that cannot hold an application fails fast instead of being silently overrun. - Management reads need a loaded application, so
reconstructagainst a fresh KV device has nothing to read until after a first flash.
Verification strategy (phase 2 onward)¶
- Program a device with ETS.
- Dump its memory over the bus.
- Store the byte image as a golden fixture.
- bussard's job is to reproduce it byte-for-byte from the YAML.
This turns "did I understand the allocator?" into a failing test. Additionally, run an independent implementation (Calimero) against the same device and diff the frames.
The flashability corpus and sweep findings¶
tests-support/product-corpus/ turns the product-data pointer index into a repeatable
"can bussard flash this today?" check. fetch.sh reads corpus.txt (one order number
per index entry) and runs bussard import-product --order-number … --yes-download for
each, downloading and checksum-verifying every file into a git-ignored cache/. The
env-gated test crates/bussard-download/tests/flash_corpus.rs then dry-runs plan_flash
over every application program in the cache and reports which lower to an executable
plan, which are refused, and, for the refused System B apps, which load-procedure op
blocked them. With BUSSARD_PRODUCT_CORPUS unset the test skips green, so CI never
downloads vendor data. See tests-support/product-corpus/README.md for the
clean-machine repro.
A wider one-off sweep read a much larger corpus through the same library code: 220
.knxprod files across 8 manufacturers (MDT, Zennio, Theben, Elsner, Lingg & Janke,
Steinel, EAE Technology, Arcus-EDS). The full pointer list (URL, SHA-256, size, vendor)
lives in tests-support/product-corpus/sweep-manifest.json; like the index it holds
pointers only, never vendor bytes. The sweep script may unzip a zip-of-.knxprod
locally, which the committed index deliberately does not.
Headline numbers: 384 application programs, 83 executable, 4 refused, 0 parse failures.
read_knxprod parsed every one of the 220 files without error, over schema versions
/11, /13, /14, /20, /21, /23 and 13 distinct mask families, including
BCU1/BCU2, KNX-RF and coupler masks. Only the 82 System B (07B0) apps are flash
candidates, and 78 of those lower to an executable plan.
What blocked the four refusals:
LdCtrlCompareRelMem(2 apps). A masked, inverted relative-memory verify op the parser keeps asLoadOp::Rawand the planner refuses (MDT BE-GTSx6Tx, MDT JTA blind push button). The top roadmap op: it is a read-and-compare sitting inside otherwise-complete procedures.- Enum default not a declared member (2 apps). The Zennio Z40 and Z70 v2 panels
declare a parameter whose own default
Valueis not one of its enumeration's members, socompute_parameter_imagerefuses the whole download (UnresolvableImage). The strict membership check has real user cost here: it blocks two otherwise fully executable panels over a vendor data-quality quirk.
Other unhandled load ops the sweep surfaced (all kept as LoadOp::Raw, none yet blocking
an executable procedure): LdCtrlTaskCtrl2, LdCtrlTaskPtr, LdCtrlDeclarePropDesc,
LdCtrlDelay, LdCtrlCompareMem. Load-op use splits cleanly by mask family: System B
(07B0) procedures are property-and-MCB based (RelSegment, WriteRelMem,
LoadImageProp, CompareProp), System 7/2 (0705/0701/0021) procedures are segment
based (AbsSegment, WriteMem), and the RF/coupler masks are where the
TaskCtrl2/TaskPtr/DeclarePropDesc ops appear.
Parameter-type coverage: the four encodable shapes dominate (Int, Enum, Text, Float).
Six type elements fall through to ParameterType::Other and are preserved by name but
encoded only as a byte-multiple fallback: TypeColor, TypeTime, TypeIPAddress,
TypePicture, TypeRawData, and stray TypeRestriction. None is silently dropped.
Each new construct has a fabricated regression fixture (fake data reproducing the
structural shape, never vendor content) under crates/bussard-ets/tests/fixtures/ and
crates/bussard-prod/tests/fixtures/; see those directories' README.md for the
intended test per fixture. The sweep is a point-in-time study, not a CI job; the
committed corpus (corpus.txt + flash_corpus.rs) is the ongoing check.
7. Performance¶
Be precise about what's winnable. TP1 is 9600 baud (roughly 30-50 telegrams/s), so the frame count of a big download is fixed. Winnable: startup overhead (sub-second to first frame vs. ETS project-open latency), pipelining independent device operations, extended frames/larger APDUs where the mask supports them, and differential downloads by default. Expect a large multiple on multi-device operations, near-parity on a single big download; the everyday win is a group-address change in ~2 seconds instead of a minute.
8. Constraints and guardrails¶
- Tunnel contention. Cheap KNXnet/IP interfaces allow few tunnel connections, and a home-automation system may hold one. Routing (multicast) is a first-class transport, decided in the transport crate's API from day one.
- Bus flooding. Rate-limit writes; an agent in a retry loop must not brown out TP1.
- MCP safety. Read-only by default; writes only through an approved plan; denylist for safety-critical objects.
- Failed download leaves a device unloaded.
applywrites a pre-state table backup undercaptures/backups/before any write; a re-apply is idempotent.flashhas no backup (a factory-fresh device has no prior application to save), so recovery there is a re-flash or an ETS download. Keep a fresh.knxprojbackup as the last resort.
9. The hard residue¶
- ETS DCA plugin devices: configured by compiled .NET, no XML to interpret. These stay ETS-only, permanently. Audit target installations for them early.
- KNX Data Secure: secure commissioning with the FDSK is additional work where enabled.
- Per-manufacturer quirks that ETS handles by special-casing.