The snapshot that keeps non-default radios reporting the band they are
actually on was seeded lazily, on the first status publish. That publish
waits for a broker to connect, so a node whose MQTT is briefly unreachable
could take a live radio save first and bake the pending settings in as
though they were what the hardware got -- reproducing the wrong-band
attribution the snapshot exists to prevent.
Capture it in MeshCoreToMqttPusher.__init__ instead, together with the
default radio id. That id was being recomputed from live config on every
publish, but fabric.default_radio is itself restart-required: a pending
change would refresh the radio that was not retuned and freeze the one that
was, reporting both wrongly where rebuilding everything got one right.
Seeding at construction also removes the unlocked check-then-set on
_applied_radio_map, which the status-publish timer thread shared with
whatever triggered the first publish.
Multi-radio entries currently replace radio, sx1262, and transport
configuration sections wholesale. Partial sx1262 entries therefore discard
board settings supplied by the top-level hardware preset, including TCXO
control, DIO2 RF switching, and radio enable pins.
Merge mapping sections key-by-key instead, treating the top-level sections as
defaults and radios[] values as per-radio overrides. Keep radio_type as a
scalar override so heterogeneous fabrics continue to work.
Overlay makes two spellings of one setting reachable together, where the
entry's could lose to an inherited default it never wrote: en_pins beats
en_pin in SX1262Radio, and address/serial_number beat device_address/serial
in get_radio_for_board. An entry naming either spelling now displaces the
other, so the failure mode is not a radio that silently never powers up.
This also corrects duty-cycle metering. It builds its air profiles through
_merge_radio_entry, so a radios[] entry that set only its frequency was
metered on library defaults -- 62.5 kHz / SF8 / CR8 -- rather than on the
node's own top-level radio block, and reported no tx_power at all.
_inherited_air_profile's docstring already described the overlay semantics
this commit implements.
Inheritance reaches the MQTT radio map the same way, and there it needed a
guard. The map is rebuilt from config on every status publish, so once a
partial entry inherits the top-level radio block, saving the default radio's
bandwidth moved the reported bandwidth of every radio that omits one --
while only the default radio is retuned, the rest keeping their band until a
restart. Observers join that map against rx_radio_id to decide which band
carried a packet, so it would have attributed those radios' packets to a band
they were never on. build_radio_status_entries now takes the map built when
the radios were last really configured and keeps every non-default entry from
it, which is the line AirtimeBudgets._effective_air already holds for
metering; MeshCoreToMqttPusher seeds it on its first build.
Log each resolved radio config at debug level, redacting modem_tcp.token via
redact_modem_tokens_in_place, since what an entry inherited is otherwise
invisible and a wrong answer surfaces only as a radio that will not transmit.
Add regression coverage for TCXO/RF-switch inheritance, explicit false
overrides, partial radio air settings, CH341 device selection, per-radio
radio_type overrides, alias displacement, metering inheritance, token
redaction, and a live default-radio save leaving other radios' published
air settings alone.
Fixes#485
A radio id resolved to its frequency and modulation, which is enough to say
which band a packet was on but not how hard it was pushed. Two radios on one
node routinely run different power -- a wide local radio turned down to keep
off its neighbour, a narrow backhaul at full -- and nothing in the air settings
says so, which leaves an observer comparing their coverage with a term missing.
Each entry in the status `radios` map gains `tx_power`, in dBm.
The value is read through build_radio_profiles, so it inherits the same merge
and per-radio override rules as the rest of the profile rather than a second
reading of the config. get_radio_for_board applies a different default per
radio type -- 14 for kiss, 22 for the modems, and none at all for sx1262, which
requires the key -- so those defaults now live beside the other air defaults and
a config that is silent reports what is actually on the air.
Where the power cannot be known it is omitted rather than guessed, and unlike
the air settings it never voids the map: power does not decide which band a
packet was on, so an entry without it still attributes the packet correctly.
Single-radio nodes publish no map at all, as before.
Everything the map reports is what the node is configured with, not a reading
from the hardware, and the docstring now says so: a driver may clamp what it was
given (an SX1262 holds -9..22 dBm), and editing a radios[] entry does not
reconfigure that radio live, because config_manager only applies the top-level
radio section. That gap predates this field -- it applies to the frequency and
modulation already in the map -- but a power figure invites the comparison, so
it is written down rather than left to be discovered.
A boolean reads as unknown rather than as int(True)'s plausible-looking 1 dBm,
and an unreadable optional field logs at debug: this runs once per status
publish, so a warning there would repeat for the life of the node.
A two-radio bridge is one MeshCore node with one pubkey, and the path
byte it appends on repeat is the same whichever radio carried the
packet. Nothing on the air says which band a hop used, and an observer
receiving on one side can only infer the other by already knowing its
neighbour's frequency. The node itself does know, so say it.
Two halves, both needed:
- Packets gain rx_radio_id (the radio that heard it) and tx_radio_ids
(every radio that retransmitted it - a bridge repeating on both sides
lists both). tx_radio_ids is always a list, so a consumer never has to
branch on the JSON type, and is absent rather than empty when the
packet was not repeated.
- Status gains radios: [{id, radio}], mapping each id to its
MHz,kHz,SF,CR settings. Without it a radio id is just an
operator-chosen label. Ids are unique only within a node, so consumers
key on (origin_id, id). Rebuilt per publish, since air settings can
change from the web UI while the node runs.
Single-radio nodes publish neither and their payload stays byte-for-byte
what it was: the ids would say nothing the status message doesn't
already. A node whose radio settings cannot all be read publishes no map
at all rather than a partial one that would place a packet on the wrong
band.
On a Fabric node the top-level status radio field now reports the
default radio rather than the top-level radio: block, which is the base
entries inherit from and can describe a band no radio is on.