fix rfswitch_table cross-file merge; drop now-stale checker warning

Three CodeRabbit findings on 09e0d390c, addressed together since #2
and #3 are the same root cause:

1. PortduinoGlue.cpp: require an exact "DIO<n>" match when parsing
   rfswitch_table.pins. sscanf's %d stops at the first non-digit, so
   "DIO5invalid" silently parsed as DIO5 at runtime even though
   ConfigCheck.cpp's static validator (exact match against
   kRfSwitchPins) already rejected it - checker and loader disagreed.

2. PortduinoGlue.cpp: reset all 5 pin slots and all 8 mode rows before
   applying a table, rather than only overwriting what the new table
   mentions. A later config.d file that omitted a mode a prior file
   had set (e.g. only redefining MODE_TX) let the earlier file's
   MODE_RX leak through, contradicting "last file wins" - the rule
   every other Lora: key already follows.

3. ConfigCheck.cpp: with #2 fixed, rfswitch_table behaves like any
   other cross-file key, so removed the special-cased ERROR in
   checkCrossFileOverlap ("These do NOT override each other... OR of
   every table") - it described the pre-fix OR-accumulation bug and
   is no longer accurate. Falls through to the generic "last file
   wins" INFO now. Renamed/repurposed the rfswitch-sticky fixture to
   rfswitch-last-wins and updated its assertion (was rc=1 asserting
   the old error text, now rc=0 asserting the generic info) and the
   fixtures README.

Verified: bin/test-config-check.sh GREEN 69/69 against an isolated
native build, including the renamed assertion.
This commit is contained in:
nomdetom committed 2026-08-10 14:05:17 +01:00
1 parent 09e0d390c7
commit ee9b5b81ee
9 files changed
+33 -40

No files matched your search

+6 -6
View File
@@ -402,12 +402,12 @@ assert "config.d overrides are reported" 0 configd-conflict/config.yaml check \
"files define a 'Lora:' section" \
"The file loaded last wins" \
"Result: 0 errors,"
# Switch tables are the one place "last wins" is false: the loader only ever writes
# HIGH, so the effective table is the OR of every file. Proven with --output-yaml.
assert "switch tables across files do not override" 1 rfswitch-sticky/config.yaml check \
"These do NOT override each other" \
"a HIGH from an earlier file survives a later file that sets LOW" \
"Enable exactly one"
# rfswitch_table follows "last file wins" like every other Lora: key: no special-cased
# error, just the standard cross-file-overlap info.
assert "rfswitch tables across files: last one wins" 0 rfswitch-last-wins/config.yaml check \
"'Lora.rfswitch_table' is set in 2 files" \
"The file loaded last wins" \
"Result: 0 errors,"
echo
echo "--check takes precedence over --output-yaml:"
-12
View File
@@ -915,18 +915,6 @@ void checkCrossFileOverlap(const PathIndex &paths, const std::map<std::string, s
if (coveredByAncestor)
continue;
// The one place "last wins" is untrue: the loader only ever writes HIGH, so the effective
// table is the OR of every table loaded -- a switch state belonging to none of them.
if (entry.first == "Lora.rfswitch_table") {
findings.push_back({kError, across, 0,
"'Lora.rfswitch_table' is set in " + std::to_string(entry.second.size()) + " files (" +
describeOwners(entry.second) +
"). These do NOT override each other: the loader only ever writes HIGH, so a HIGH "
"from an earlier file survives a later file that sets LOW there, and the radio ends "
"up driving the OR of every table. Enable exactly one"});
continue;
}
findings.push_back({kInfo, across, 0,
"'" + entry.first + "' is set in " + std::to_string(entry.second.size()) + " files (" +
describeOwners(entry.second) + "). The file loaded last wins"});
+11 -2
View File
@@ -1095,14 +1095,23 @@ bool loadConfig(const char *configPath)
}
if (yamlConfig["Lora"]["rfswitch_table"]) {
portduino_config.has_rfswitch_table = true;
// A later file's table fully replaces an earlier one, matching "last file wins"
// for every other Lora: key, rather than leaving omitted pins/modes as carryover.
for (int i = 0; i < 5; i++)
portduino_config.rfswitch_dio_num[i] = -1;
for (int m = 0; m < RFSW_MODE_COUNT; m++) {
portduino_config.rfswitch_mode_present[m] = false;
portduino_config.rfswitch_mode_high[m] = 0;
}
const YAML::Node table = yamlConfig["Lora"]["rfswitch_table"];
// Store the DIO number as written; the slot it maps to is per-radio. Anything
// not spelled "DIO<n>" leaves the slot unused.
// not spelled exactly "DIO<n>" (trailing junk included) leaves the slot unused.
for (int i = 0; i < 5; i++) {
const std::string name = table["pins"][i].as<std::string>("");
int dioNum = 0;
if (sscanf(name.c_str(), "DIO%d", &dioNum) == 1 && dioNum >= 0 && dioNum <= INT8_MAX)
if (sscanf(name.c_str(), "DIO%d", &dioNum) == 1 && dioNum >= 0 && dioNum <= INT8_MAX &&
name == "DIO" + std::to_string(dioNum))
portduino_config.rfswitch_dio_num[i] = (int8_t)dioNum;
}
+5 -7
View File
@@ -182,13 +182,11 @@ the last-loaded file is reset to its default - here `config.yaml` sets
The load order within `config.d/` comes from the filesystem, so the report warns
rather than assuming alphabetical order.
`rfswitch-sticky/` covers the one place where "the file loaded last wins" is false,
and it documents a firmware bug rather than a configuration mistake. Its `config.d/`
holds two switch tables; the last one loaded sets `MODE_RX` LOW on both pins, but the
loader only ever writes HIGH and never writes LOW back, so the HIGH from the earlier
file survives and the effective table is the OR of both. Verified with
`meshtasticd --output-yaml`. Until the loader is fixed, `--check` reports this as an
error and tells you to enable exactly one.
`rfswitch-last-wins/` covers `Lora.rfswitch_table` across two `config.d/` files. It
follows the same "last file loaded wins" rule as every other `Lora:` key - the loader
resets a table's pins and mode rows before applying a replacement, so an earlier
file's `MODE_RX` setting cannot leak through a later file that omits it. `--check`
reports this as the standard cross-file-overlap info, not a special-cased error.
## Running these as a normal boot
@@ -0,0 +1,7 @@
# Sets MODE_RX LOW on both pins. Whichever of this file and a-first.yaml the filesystem
# returns last should fully replace the other's table, per "last file wins".
Lora:
Module: lr1121
rfswitch_table:
pins: [DIO5, DIO6]
MODE_RX: [LOW, LOW]
@@ -0,0 +1,4 @@
# Primary config for the rfswitch_table last-wins case. The two files in config.d/
# each define a Lora.rfswitch_table; the one loaded last should fully replace it.
General:
ConfigDirectory: config.d/
@@ -1,9 +0,0 @@
# FAULT, and it is a firmware bug rather than a typo. This file is loaded LAST and says
# MODE_RX is LOW on both pins, but the loader only ever writes HIGH and never writes
# LOW back, so the HIGH from a-first.yaml survives. The effective table is the OR of
# both files -- a switch state that neither file asked for. Verified with --output-yaml.
Lora:
Module: lr1121
rfswitch_table:
pins: [DIO5, DIO6]
MODE_RX: [LOW, LOW]
@@ -1,4 +0,0 @@
# Primary config for the sticky-switch-table case. The two files in config.d/ each
# define a Lora.rfswitch_table.
General:
ConfigDirectory: config.d/