7.2 KiB
Rules Configuration Design for C Reimplementation
Background
HBLink3's conference bridge rules have been defined in a Python file (rules.py) since
the original DMRlink confbridge.py in 2013. Thirteen years of operator familiarity with
this format is a real asset. A C reimplementation of the protocol core should preserve that
format completely — not as a compromise, but because it is actually the right tool for what
the rules file does.
What the rules file actually is
rules.py is not application logic. It is a data definition file that uses Python as a
macro language. Its only meaningful output is BRIDGE_CONF — a dictionary of bridge
definitions, each containing a list of system members with their associated parameters.
The Python feature operators used in practice are straightforward: named lists defined once and referenced in multiple bridge members, variables holding common parameter sets, occasional conditional expressions for environment-specific tweaks. None of this is algorithmic; it is all in service of expressing structured data without repetition.
This means the rules file is already doing what Python is best suited for: readable, expressive configuration with reuse. The C runtime does not need to understand Python to benefit from it.
The separation of concerns
The architecture draws a clean line between two roles:
Configuration authoring — the operator's domain. Expressed in rules.py using Python.
Validated by a helper tool before the runtime ever sees it. Can be edited with any text
editor, generated by a web UI, or constructed programmatically. The format is stable and
human-readable.
Protocol runtime — the C core's domain. Operates entirely on a resolved, validated JSON representation of the rules. Has no Python dependency. Loads the JSON at startup and can reload it at any time without restarting.
These two roles communicate through a single, well-defined JSON schema. Anything that can produce conforming JSON can author rules — the Python file is one such producer, a web-based admin UI would be another, a script generating rules from a database would be another.
The helper tool
A small Python script (call it rules_export.py) bridges the two roles. Its job is:
-
Execute
rules.pyas normal Python. All variable assignments, list references, and any other Python constructs run to completion. By the time execution finishes,BRIDGE_CONFis a fully resolved plain data structure with no remaining references or indirection. -
Validate the resolved structure against the expected schema. Check that required fields are present, that types are correct, that system names referenced in rules correspond to known systems, and so on. Emit clear, actionable error messages if anything is wrong. This is where the "missing trailing comma" class of errors gets caught, with a Python traceback that points directly at the offending line.
-
Emit the validated structure as JSON to stdout (or to a file). The output contains only concrete values — no variable names, no Python expressions, no references. It is the fully evaluated result.
The helper is intentionally a separate tool rather than a startup step inside the C binary. This means an operator can run it manually to validate a rules change before applying it, a deployment script can run it as a preflight check, and a web-based admin UI can invoke it server-side to validate rules before saving them.
How the C core loads rules
At startup, the C core reads the JSON rules file from a configured path. It parses the JSON into its internal bridge table — an array of bridge structs, each containing an array of member structs with their parameters. This is the only representation the runtime uses. The JSON file is not kept open; it is read once, parsed, and the file handle is closed.
The JSON schema is documented separately and is the stable interface between configuration tooling and the runtime. The Python rules file format is one representation that maps to this schema; it is not the schema itself.
Live reload without restart
When an operator modifies rules.py, the workflow is:
-
Run the helper tool to validate and export the new JSON. If validation fails, no change is applied; the running daemon continues on the existing rules.
-
Signal the running daemon (SIGHUP is the Unix convention for configuration reload).
-
The daemon, at the top of its next event loop iteration, detects the signal flag and reads the new JSON file. It constructs a new bridge table in fresh memory alongside the existing one.
-
If the new table is valid, the daemon atomically replaces the pointer to the active bridge table with the pointer to the new one, then frees the old table. From this point forward all routing decisions use the new rules.
-
If the new table is invalid (malformed JSON, missing fields, etc.), the daemon logs the error and continues running on the existing rules. No traffic is disrupted.
Any call in progress at the moment of the swap completes under the old rules. The swap itself is instantaneous from the event loop's perspective.
Why the single-threaded model makes this safe
In a multi-threaded runtime, swapping the bridge table under live traffic requires locks, reference counting, or other synchronization to ensure no thread is mid-read on the old table when it is freed. This is non-trivial to get right.
In a single-threaded event loop model, the swap happens at a well-defined point — the top of a loop iteration, before any packet dispatch — at which moment by definition nothing else is touching the bridge table. There are no concurrent readers. The correctness argument is trivial: the old pointer is live until the new one replaces it, and exactly one thing holds the pointer at any time.
The brief pause while the JSON file is read and parsed is acceptable for this use case. Amateur radio PTT traffic is tolerant of a reload pause measured in tens of milliseconds. If future requirements demanded zero-pause reloads, the parsing work could be moved to a background thread that hands the completed table back to the event loop via a pipe, keeping the actual pointer swap on the main thread — but this is an optimization, not a requirement for correctness.
What this enables beyond the rules file
Once the JSON schema is the stable interface, multiple configuration front-ends become possible independently of one another:
- The existing
rules.pyPython format, unchanged, for operators familiar with it - A web-based admin UI with authentication that constructs and validates the JSON directly, with no Python file involved
- A REST endpoint on the admin service that accepts JSON rules and triggers a reload, enabling external orchestration systems to manage bridge configuration
- Automated rule generation from a database, a planning tool, or a coordination system
None of these require changes to the C runtime. The runtime speaks JSON; whatever produces that JSON is an independent concern.
This is the natural endpoint of what the Python rules file was always doing — expressing structured data in a readable, reusable way. The JSON schema makes that structure explicit and opens it to any tooling that can speak it.