Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
172 commits
Select commit Hold shift + click to select a range
a6e8dec
Fix Heltec Mesh Solar companion build: undef NONE macro from meshsola…
marcelverdult Sep 26, 2026
7cbf73c
refactor(prefs): move server NodePrefs into its own header and pin it…
marcelverdult Oct 19, 2026
a59561f
refactor(prefs): share the radio and GPS settings between server and …
marcelverdult Oct 19, 2026
292b2d4
Merge step 1: share the radio and GPS settings between both NodePrefs
marcelverdult Oct 19, 2026
a5ffadb
docs(readme): explain what this fork is for
marcelverdult Oct 19, 2026
678b4db
docs(refactoring): explain the refactoring series and its first step
marcelverdult Oct 19, 2026
63ea0af
tools(refactoring): add the hardware check tool used for the refactor…
marcelverdult Oct 19, 2026
4732e95
test(core): run Dispatcher and Mesh on the host and pin their tuning …
marcelverdult Oct 19, 2026
26cb127
refactor(core): read the tuning values from the node's radio settings
marcelverdult Oct 19, 2026
e0dbc46
docs(refactoring): design note for step 2, the core reading its tunin…
marcelverdult Oct 19, 2026
84e055f
Merge step 2: let the core read its tuning values from the radio sett…
marcelverdult Oct 19, 2026
3ecb6a3
tools(refactoring): add the diagnostic patch used for the step 2 hard…
marcelverdult Oct 19, 2026
b49eb6c
test(prefs): pin the edge cases of the radio CLI
marcelverdult Oct 19, 2026
4d43e19
refactor(prefs): drive the radio settings from one table
marcelverdult Oct 19, 2026
98ffa5c
docs(refactoring): design note for step 3, the radio settings table
marcelverdult Oct 19, 2026
a9c1f4c
Merge step 3: drive the radio settings from one table
marcelverdult Oct 19, 2026
cf2ed8e
docs(refactoring): drop the duplicated Hardware heading in 0002 and 0003
marcelverdult Oct 19, 2026
1153528
docs(refactoring): analysis and cut plan for the shared server base
marcelverdult Oct 19, 2026
b160f39
fix(identity): report a failed identity write
marcelverdult Oct 19, 2026
7e1dd92
test(server): run the real server roles on the host and pin the temp …
marcelverdult Oct 19, 2026
5d28fa4
docs(refactoring): record that the server roles now run on the host
marcelverdult Oct 19, 2026
07d9e7b
test(chat): pin the text length that sendGroupMessage accepts
marcelverdult Oct 19, 2026
bc5708f
fix(prefs): check the ranges after loading /prefs.json
marcelverdult Oct 19, 2026
e281f0e
fix(identity): report a failed identity write
marcelverdult Oct 19, 2026
8c59820
fix(sensors): let BSEC claim an address only if a BME68x answered
marcelverdult Oct 19, 2026
24664a6
fix(chat): refuse a negative text length for a group message
marcelverdult Oct 19, 2026
031428f
refactor(server): share the region and reply scope plumbing
marcelverdult Oct 19, 2026
d63f67c
test(server): pin the Region a packet is taken to have arrived on
marcelverdult Oct 19, 2026
3f9dc9d
Merge the BSEC chip id check so a BME280 keeps its address
marcelverdult Oct 19, 2026
bd1fcf7
test(variants): pin the millis() deadline question across the counter…
marcelverdult Oct 19, 2026
09b303c
fix(variants): make the GPS update deadline wrap-safe in seven variants
marcelverdult Oct 19, 2026
f9b369b
docs(fixes): note for the GPS millis deadline wrap
marcelverdult Oct 19, 2026
9c2fecb
chore: stop tracking the Python bytecode cache
marcelverdult Oct 19, 2026
9adee11
test(server): pin the advertisement schedule before it moves
marcelverdult Oct 19, 2026
5ce76bd
fix(radio): refuse an airtime factor that is not a number
marcelverdult Oct 19, 2026
3bc2e5f
fix(meshtracker_x1): inherit MicroNMEA in every environment of the va…
marcelverdult Oct 19, 2026
c237e89
Merge the wrap-safe GPS update deadline for seven variants
marcelverdult Oct 19, 2026
ad7b5b3
refactor(server): share the advertisement schedule
marcelverdult Oct 19, 2026
91357c5
test(server): tell the two advertisement send paths apart
marcelverdult Oct 19, 2026
c2c007b
Merge the MicroNMEA dependency fix for the MeshTracker X1 variant
marcelverdult Oct 19, 2026
3095228
fix(radiolib): pass the boosted gain mode to sx126xResetAGC
marcelverdult Oct 19, 2026
1005121
docs(fixes): note for the LLCC68 AGC reset argument
marcelverdult Oct 19, 2026
9100904
refactor(server): share the temporary radio parameters
marcelverdult Oct 19, 2026
c54cf27
fix(companion): state the minimum frame length of 14 more commands
marcelverdult Oct 19, 2026
47f5297
test(server): pin the identity, file system and stats behaviour befor…
marcelverdult Oct 19, 2026
3162e17
refactor(server): share the identity, file system and stats housekeeping
marcelverdult Oct 19, 2026
9f4c968
fix(companion): rename the InterfaceType NONE enumerator so a macro c…
marcelverdult Oct 19, 2026
f1bf12e
ci: run the group message suite
marcelverdult Oct 19, 2026
2119cc8
Merge the InterfaceType enumerator rename so a dependency macro canno…
marcelverdult Oct 19, 2026
67b8843
fix(prefs): persist extra_sf and the discovery timestamp
marcelverdult Oct 19, 2026
8a91abe
Merge the LLCC68 AGC reset argument fix
marcelverdult Oct 19, 2026
480d1a6
ci: run the server role tests
marcelverdult Oct 19, 2026
875f3ad
refactor(server): share the ACL peer lookup
marcelverdult Oct 19, 2026
da9af7b
docs(refactoring): record what steps 4a to 4e achieved
marcelverdult Oct 19, 2026
670889e
docs(fixes): note for the group message length fix
marcelverdult Oct 19, 2026
92248b1
docs(fixes): note for the range check after loading prefs
marcelverdult Oct 19, 2026
efeec20
docs(refactoring): the sensor keeps its own retransmit delay
marcelverdult Oct 19, 2026
a18117b
docs(fixes): base the LLCC68 claim on the include graph, not on binar…
marcelverdult Oct 19, 2026
d8b7e35
docs(fixes): note for the identity write result
marcelverdult Oct 19, 2026
c93672a
docs(fixes): replace the guessed history of the LLCC68 defect with th…
marcelverdult Oct 19, 2026
7ce79e2
test(sensor): run the sensor role on the host and pin what it does today
marcelverdult Oct 19, 2026
183744c
Merge the group message length fix
marcelverdult Oct 19, 2026
04967a7
Revert the InterfaceType enumerator rename in favour of the upstream …
marcelverdult Oct 19, 2026
a45a603
Merge the upstream proposal for the meshsolar NONE macro collision (#…
marcelverdult Oct 19, 2026
254e421
fix(companion-ui): keep the status bar behind the display guard, drop…
marcelverdult Oct 19, 2026
f886603
fix(esp32): return a value from ESPNOWRadio::setRxBoostedGainMode
marcelverdult Oct 19, 2026
173dc32
docs(fixes): note for the ESP-NOW boosted gain return value
marcelverdult Oct 19, 2026
e18abec
docs(fixes): note for the airtime factor NaN check
marcelverdult Oct 19, 2026
401d6af
Merge the ESP-NOW boosted gain return value fix
marcelverdult Oct 19, 2026
a86f30a
Merge the identity write result fix
marcelverdult Oct 19, 2026
f7063c0
Merge the range check after loading /prefs.json
marcelverdult Oct 19, 2026
1895ea8
docs(fixes): note for the frame length guards
marcelverdult Oct 19, 2026
ec04c8c
docs(fixes): record that the LLCC68 fix is already in upstream PR #3496
marcelverdult Oct 19, 2026
687573f
Merge the frame length guards for fourteen more commands
marcelverdult Oct 19, 2026
4988795
Merge the airtime factor NaN check
marcelverdult Oct 19, 2026
f8f350b
docs(fixes): note for persisting extra_sf and the discovery timestamp
marcelverdult Oct 19, 2026
b068f43
Merge persisting extra_sf and the discovery timestamp
marcelverdult Oct 19, 2026
8eed55e
test(prefs): cover the two new prefs keys in the characterization suite
marcelverdult Oct 19, 2026
927c645
docs(fixes): note for the companion UI guard and leak
marcelverdult Oct 19, 2026
bd40249
Merge the companion UI guard and the version string leak
marcelverdult Oct 19, 2026
70e6423
test(mocks): make Print actually format numbers
marcelverdult Oct 19, 2026
7e27a98
fix(identity): verify the identity after closing the file
marcelverdult Oct 19, 2026
26a9bce
fix(radiolib): score a packet at the spreading factor the radio reports
marcelverdult Oct 19, 2026
49ed34b
docs(fixes): note for the spreading factor in the packet score
marcelverdult Oct 19, 2026
62d712b
Merge the spreading factor fix for the packet score
marcelverdult Oct 20, 2026
7075e3a
test(mocks): make min and max functions, not macros
marcelverdult Oct 20, 2026
fed9334
fix(sensor): make multi.acks effective and apply RX boosted gain at boot
marcelverdult Oct 19, 2026
83e854d
docs(refactoring): two findings from the sensor alignment
marcelverdult Oct 19, 2026
3ff46b1
docs(refactoring): record that step 4f is ready to start
marcelverdult Oct 19, 2026
d070993
docs(server): bring the base class comment up to date
marcelverdult Oct 19, 2026
e300f40
docs(refactoring): the build matrix for steps 4a to 4e
marcelverdult Oct 19, 2026
0a93bc4
docs(refactoring): tie the matrix numbers to the current branch tips
marcelverdult Oct 19, 2026
cc1e6c3
Merge the shared server base (steps 4a to 4e)
marcelverdult Oct 20, 2026
ae0b9f2
Merge the sensor alignment, part one: multi.acks and RX boosted gain
marcelverdult Oct 20, 2026
83bce79
test(server): pin the boot sequence of all three roles before it moves
marcelverdult Oct 20, 2026
133fe68
refactor(server): share the boot sequence
marcelverdult Oct 20, 2026
b3d6086
fix(RegionMap): let the wildcard's flags decide a scoped flood with t…
marcelverdult Oct 20, 2026
201ec9c
docs(fixes): note for the transport code 0 wildcard fix
marcelverdult Oct 20, 2026
78e7c02
docs(refactoring): the boot sequence note, the build matrix and the l…
marcelverdult Oct 20, 2026
3e68578
docs(refactoring): record the hardware check of the sensor alignment
marcelverdult Oct 20, 2026
4cf3161
Merge the shared boot sequence (step 4f)
marcelverdult Oct 20, 2026
9e39d32
docs(refactoring): record the board test of the boot sequence
marcelverdult Oct 20, 2026
4943263
Merge the transport code 0 wildcard fix (ledger issue 10)
marcelverdult Oct 20, 2026
2e456f7
test(repeater): check that a denied wildcard stops a code 0 scoped flood
marcelverdult Oct 20, 2026
3b58ebd
fix(sensor): apply the flood hop limits, and lift stored zero limits …
marcelverdult Oct 20, 2026
4fdb7f8
test(radiolib): characterize the receive detection of the chip classes
marcelverdult Oct 20, 2026
ddca1a5
test(variants): pin how a variant creates a new identity
marcelverdult Oct 20, 2026
ebb1473
refactor(radiolib): share the receive detection of the chip classes
marcelverdult Oct 20, 2026
248b1d1
refactor(variants): define radio_new_identity() once for RadioLib var…
marcelverdult Oct 20, 2026
24ce8b5
docs(refactoring): the note for the flood hop limits, and the ledger
marcelverdult Oct 20, 2026
aa02052
Merge the sensor's flood hop limits with the one-time prefs upgrade
marcelverdult Oct 20, 2026
6c75a9c
test(server): pin which packets each role forwards before the check m…
marcelverdult Oct 20, 2026
e5553e1
refactor(server): share the forwarding checks every role makes
marcelverdult Oct 20, 2026
d05bfb7
Merge the identity read back after closing the file
marcelverdult Oct 20, 2026
a0ef4a9
docs(refactoring): design note 0009, one RadioLib adapter per chip fa…
marcelverdult Oct 20, 2026
675245a
ci: run the chip class receive detection suite
marcelverdult Oct 20, 2026
b4f6f4a
docs(refactoring): the note for step 4g, the shared forwarding checks
marcelverdult Oct 20, 2026
a6b1c93
Merge the shared forwarding checks (step 4g)
marcelverdult Oct 20, 2026
0674597
docs(refactoring): design note 0008, one generic variant target (cut A)
marcelverdult Oct 20, 2026
ed64d3e
fix(t-echo): clear a stuck I2C bus before Wire.begin()
marcelverdult Oct 20, 2026
a3d3961
docs(refactoring): record the board description decision in note 0008
marcelverdult Oct 20, 2026
fba65e8
docs(fixes): note on the T-Echo I2C bus clear
marcelverdult Oct 20, 2026
a5f9b69
test(variants): pin the FEM RX gain handlers of the four Heltec boards
marcelverdult Oct 20, 2026
2884fd3
refactor(variants): share the FEM RX gain handlers of the four Heltec…
marcelverdult Oct 20, 2026
e555194
fix(radio): refuse "nan" and "inf" by the text, not by the bits
marcelverdult Oct 20, 2026
5b5a3df
Merge the text check for nan and inf in the radio CLI
marcelverdult Oct 20, 2026
5b353eb
Merge fix/nrf52-i2c-bus-clear: clear a stuck I2C bus before Wire.begi…
marcelverdult Oct 20, 2026
ddc237c
Merge one receive detection for the RadioLib chip classes (step 9a)
marcelverdult Oct 20, 2026
771327a
Merge one shared radio_new_identity() for the RadioLib variants (step…
marcelverdult Oct 20, 2026
a12d301
docs(refactoring): list steps 9 and 11 in the step table
marcelverdult Oct 20, 2026
0a47cad
test(companion): characterize the frame protocol on the host
marcelverdult Oct 20, 2026
309b971
tools(refactoring): replay the companion frame sweep against a board
marcelverdult Oct 20, 2026
e0aa8d7
docs(refactoring): the design note for the companion command table (0…
marcelverdult Oct 20, 2026
7fda7ea
refactor(companion): dispatch the frame protocol through a command table
marcelverdult Oct 20, 2026
07ff7f0
tools(refactoring): compare accepted companion commands between two i…
marcelverdult Oct 20, 2026
e09dfc6
docs(refactoring): record the verification of step 10b
marcelverdult Oct 20, 2026
ebb7db6
Merge the command table for the companion protocol (steps 10a and 10b)
marcelverdult Oct 20, 2026
bb5b104
docs(refactoring): list step 10 in the step table
marcelverdult Oct 20, 2026
91ea10c
docs(refactoring): record cut B of note 0008, one FEM RX gain handler
marcelverdult Oct 20, 2026
4417399
Merge the shared FEM RX gain handler of four Heltec boards (step 11, …
marcelverdult Oct 20, 2026
e5a8f16
docs(refactoring): list cut B of step 11 in the step table
marcelverdult Oct 20, 2026
c28513b
docs(refactoring): design cut C of note 0011, one default radio_init()
marcelverdult Oct 20, 2026
da1589c
test(variants): pin how 31 nRF52 variants start the radio
marcelverdult Oct 20, 2026
2b7122c
refactor(variants): define radio_init() once for 31 nRF52 SX1262 vari…
marcelverdult Oct 20, 2026
3d48b1d
docs(refactoring): record cut C of note 0011, one default radio_init()
marcelverdult Oct 20, 2026
68e0fcd
Merge one default radio_init() for 31 nRF52 boards (step 11, cut C)
marcelverdult Oct 20, 2026
629cedb
docs(refactoring): design step 9b, one template for the SX126x chip c…
marcelverdult Oct 21, 2026
568891f
docs(refactoring): wording in the 9b collision list
marcelverdult Oct 21, 2026
61d3a4f
test(radio_chips): pin std_init() of SX1262, SX1268 and LLCC68 with a…
marcelverdult Oct 21, 2026
2228a5d
refactor(radiolib): one template CustomSX126x for SX1262, SX1268 and …
marcelverdult Oct 21, 2026
43ca021
docs(refactoring): design step 10c, a bounded frame reader for the co…
marcelverdult Oct 21, 2026
2206ff0
tools(refactoring): count the terminators written through a text pointer
marcelverdult Oct 21, 2026
ef78d5e
docs(refactoring): six writes into the frame, not five
marcelverdult Oct 21, 2026
29e35f0
test(companion): pin where each command's frame ends
marcelverdult Oct 21, 2026
b1adc9a
refactor(helpers): FrameReader, reads a frame's fields in order and n…
marcelverdult Oct 21, 2026
2db0e8e
refactor(companion): 34 handlers read their fields through FrameReader
marcelverdult Oct 21, 2026
90b5da2
refactor(companion): the 14 handlers with optional fields, paths or l…
marcelverdult Oct 21, 2026
f4072b5
test(companion): the flood scope key needs all 16 bytes
marcelverdult Oct 21, 2026
6ba581a
docs(refactoring): record step 10c of note 0010, a bounded frame reader
marcelverdult Oct 21, 2026
9718786
test(companion): pin the three reads past the end that step 10c removed
marcelverdult Oct 21, 2026
2e2324a
Merge a bounded frame reader for the companion handlers (step 10c)
marcelverdult Oct 21, 2026
51b0fdc
docs(refactoring): list step 10c in the step table
marcelverdult Oct 21, 2026
102c5e7
docs(refactoring): record step 9b, one template for the SX126x chip c…
marcelverdult Oct 21, 2026
134c5a1
docs(refactoring): explain the 9b image and flash differences
marcelverdult Oct 21, 2026
7aa78f1
Merge one template for the SX126x chip classes (step 9b)
marcelverdult Oct 21, 2026
ed6cc08
docs(refactoring): list step 9b in the step table
marcelverdult Oct 21, 2026
7bab5b3
test(companion): compare encrypted packets by shape, not by content
marcelverdult Oct 21, 2026
e0d05fd
Merge the encrypted packet comparison fix of step 10c
marcelverdult Oct 21, 2026
e06a3ff
docs(refactoring): design step 9c, one wrapper template per chip family
marcelverdult Oct 21, 2026
fb0003c
test(radiolib): characterize the eight chip wrappers on the host
marcelverdult Oct 21, 2026
1234eb9
refactor(radiolib): one wrapper template per chip family
marcelverdult Oct 21, 2026
c4f780d
docs(refactoring): results of step 9c
marcelverdult Oct 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/run-unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
uses: ./.github/actions/setup-build-environment

- name: Run Unit Tests
run: pio test -e native -e native_kiss_modem -vv
run: pio test -e native -e native_kiss_modem -e native_core -e native_ofast -e native_chat -e native_identity_store -e native_node_prefs -e native_radio_wrappers -e native_server -e native_sensor -e native_repeater -e native_repeater_full -e native_region -e native_radio_chips -e native_variant_target -e native_variant_target_override -e native_companion -vv

- name: Upload Test Results
# Upload test results even if the test step failed.
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ cmake-*
compile_commands.json
.venv/
venv/
__pycache__/
*.pyc
platformio.local.ini
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
> ## About this fork
>
> This is a fork of [meshcore-dev/MeshCore](https://github.com/meshcore-dev/MeshCore)
> that refactors the firmware so new features are cheaper to add, without changing
> how it behaves on air, towards the companion apps, on the CLI or on flash.
>
> - **What and why:** [docs/refactoring/](docs/refactoring/README.md) explains the
> series, the rules every step follows (characterization tests first, no behaviour
> change, full build matrix, hardware upgrade and downgrade tests) and the
> [architecture review of v1.18.0](docs/refactoring/architecture-review-v1.18.md)
> the steps are based on.
> - **Branches:** each step lives on its own `refactor/...` branch based on upstream
> `dev` and is prepared as a separate pull request against upstream. This branch,
> `integration`, combines all finished steps on top of upstream and is the base
> for further work in this fork.
> - **Status:** see the table in [docs/refactoring/README.md](docs/refactoring/README.md#steps).
>
> Everything below this box is the unchanged upstream README.

## About MeshCore

MeshCore is a lightweight, portable C++ library that enables multi-hop packet routing for embedded projects using LoRa and other packet radios. It is designed for developers who want to create resilient, decentralized communication networks that work without the internet.
Expand Down
232 changes: 232 additions & 0 deletions docs/fixes/af-nan-bit-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
# "set af nan" was accepted

## Context

`set af` parses its argument and then checks the range:

```cpp
float af = strtof(&command[7], &end);
if (end == &command[7] || af < 0 || af > 9) {
strcpy(reply, "ERROR: af must be 0-9");
} else {
setAirtimeFactor(af);
strcpy(reply, "OK");
}
```

(`src/helpers/CommonRadioPrefs.cpp:69-79` on `upstream/dev` 428a95c6.)

`strtof("nan")` consumes the token, so the parse guard does not fire, and under
IEEE 754 every ordered comparison with a NaN is false, so neither `af < 0` nor
`af > 9` fires either. The else branch stores the NaN and answers `OK`.

`airtime_factor` is the duty cycle budget. The roles hand it to the airtime
accounting, for the repeater through `examples/simple_repeater/MyMesh.h:157`
with matching getters in the room server and the sensor, so a NaN there is not
cosmetic. `set dutycycle` writes the same field through `100/dc - 1` and had the
same hole.

Step 3 of the refactoring series already knew about this and pinned it as
current behaviour rather than changing it: `test/mocks/RadioPrefsChecks.h:238`
on `integration` carries `EXPECT_EQ("OK", accepted("set af nan"))` behind
`#ifndef __FAST_MATH__`, with a comment calling it odd.

### Why only these two commands

The other float commands in the same file phrase their guard the other way
round, `if (value >= low && value <= high) { accept } else { error }`, and a NaN
makes that condition false, so it lands in the error branch. Measured on the
host with both shapes:

| token | guard as in `set af` | guard as in `set rxdelay` |
|---|---|---|
| `nan` | **accepted** | error |
| `-nan` | **accepted** | error |
| `inf` | error | error |
| `1e40` | error | error |
| `0.5` | accepted | accepted |

So on `upstream/dev` the two commands that are reachable with a NaN are exactly
`set af` and `set dutycycle`, the two written as "reject if out of range". That
is also why the board measurement found `af` and nothing else.

The positively phrased guards are safe by accident of phrasing, not by design:
their result still depends on the compiler once `-ffast-math` is in play, which
is why step 3 put a bit check behind all of them on `integration`
(`inRange`, `src/helpers/CommonRadioPrefs.cpp:65`).

## Decision

Keep the parse guard, keep both comparisons, keep both reply strings, and add a
check that the value is a finite number at all. The check reads the bits:

```cpp
bool isFinite(float v) {
uint32_t bits;
memcpy(&bits, &v, sizeof(bits));
return (bits & 0x7F800000u) != 0x7F800000u;
}
```

That is integer arithmetic, so no optimisation can fold it away, and it answers
the same on every platform with any flags. The same guard goes in front of
`set dutycycle`, whose range stays 1..100.

### Why not a comparison or a library call

nRF52 firmware is built with `-Ofast`, which includes `-ffast-math`, and that
lets the compiler assume NaN and infinity never occur. The outcome then depends
on the compiler, and the two disagree. Measured with the same test file:

| | `set af nan` on `upstream/dev` |
|---|---|
| host, IEEE comparisons | accepted |
| host, `-Ofast` (Apple clang 21) | rejected |
| board, nRF52 with `arm-none-eabi-gcc -Ofast` | accepted |

A host run with `-Ofast` alone would therefore have reported this defect as
absent. `std::isnan` is no better: it is the same comparison in a different
spelling and is equally free to be folded away.

## Alternatives considered

- **Compile this file without fast math.** Rejected. It would hide the problem
behind a build flag that any variant can change, and the defect exists under
plain IEEE rules as well, which is where it was measured.
- **Take the table driven `inRange` from step 3.** Not available here: this
branch starts from `upstream/dev`, where the radio CLI is still written out by
hand. The two will meet when this branch is merged, and the shape of the check
is deliberately the same so they can be unified.
- **Refuse the value one layer down, in the parser.** That is the right place
for a value read from `/prefs.json` and is what the range check fix does. It
is the wrong place here: this value comes from a CLI command and the caller
has to be told that its command was refused.
- **Also rewrite the positively phrased guards.** Not done, see above: they
reject a NaN under IEEE, and on `integration` step 3 has already put the bit
check behind them. Changing them here would widen a fix whose defect is the
two commands measured above.

## Compatibility

- Every value that was accepted before is still accepted, including the 99 that
`set dutycycle 1` stores, and every rejected value is still rejected with the
same message. Only values that are not finite numbers change from accepted to
refused.
- Server roles and the companion share this handler
(`src/helpers/CommonCLI.cpp:184` and
`examples/companion_radio/NodePrefs.h`), so one change covers every role.

## Verification

### Host tests

New `test/test_radio_prefs_af` drives `CommonRadioPrefs::handleCommand` through
a test double. Nine cases: every spelling of not a number, infinity and
overflow, the garbage and out of range cases that must stay rejected with their
old message, the range that must stay accepted, `get af`, and the duty cycle
range including the 99 that step 3 pinned.

The suite runs twice, in `native` with IEEE comparisons and in the new
`native_ofast` with the flags the nRF52 firmware uses:

| | before the change | after |
|---|---|---|
| `native` (IEEE) | 2 of 9 red | 9 of 9 green |
| `native_ofast` | 9 of 9 green | 9 of 9 green |

The second row is the point of the environment, not a contradiction: on this
host `-Ofast` already rejects the NaN, so an `-Ofast` run alone would have
shown nothing. Both rows green afterwards is what says the result no longer
depends on the flags.

All suites of this branch: `pio test -e native -e native_kiss_modem -e
native_ofast` reports 70 of 70.

### Mutations

Five deliberate errors, each had to turn the suite red. Two of them are only
caught by the IEEE run, which is worth recording:

| Mutation | `native` (IEEE) | `native_ofast` |
|---|---|---|
| `set af` without the finite check | red | green |
| `set dutycycle` without the finite check | red | green |
| bit comparison inverted | red (4) | red (2) |
| wrong mask | red | red |
| mask compared against zero | red (3) | red |

### Builds

All 20 environments of the matrix build on this commit, and so does
`upstream/dev` 428a95c6 in its own worktree with the same toolchain.
`Heltec_v3_terminal_chat` was built as well, because it is the one role with its
own prefs structure, to show it is untouched.

| Environment | Build | RAM against dev | Flash against dev |
|---|---|---|---|
| `Heltec_v3_companion_radio_ble` | OK | +0 | +32 |
| `Heltec_v3_companion_radio_wifi` | OK | +0 | +32 |
| `Heltec_v3_repeater` | OK | +0 | +32 |
| `Heltec_v3_room_server` | OK | +0 | +28 |
| `Heltec_v3_sensor` | OK | +0 | +28 |
| `LilyGo_T-Echo_kiss_modem` | OK | +0 | +0 |
| `LilyGo_T-Echo_room_server` | OK | +0 | +32 |
| `PicoW_repeater` | OK | +0 | +24 |
| `ProMicro_companion_radio_usb` | OK | +0 | +32 |
| `ProMicro_repeater` | OK | +0 | +32 |
| `ProMicro_room_server` | OK | +0 | +32 |
| `ProMicro_sensor` | OK | +0 | +32 |
| `RAK_3112_repeater_bridge_espnow` | OK | +0 | +32 |
| `RAK_4631_companion_radio_ble` | OK | +0 | +32 |
| `RAK_4631_repeater` | OK | +0 | +32 |
| `RAK_4631_room_server` | OK | +0 | +32 |
| `RAK_4631_sensor` | OK | +0 | +32 |
| `waveshare_rp2040_lora_repeater_bridge_rs232` | OK | +0 | +24 |
| `wio-e5-mini_repeater` | OK | +0 | +24 |
| `wio-e5-mini_sensor` | OK | +0 | +24 |
| `Heltec_v3_terminal_chat` | OK | +0 | +0 |

RAM is unchanged everywhere, and flash grows by 24 to 32 bytes in every build
that compiles this handler. Exactly two builds come out byte identical to dev,
and both are explained rather than suspicious: `LilyGo_T-Echo_kiss_modem` adds
only `examples/kiss_modem/` to its sources, and `Heltec_v3_terminal_chat` runs
`examples/simple_secure_chat`, which keeps its own prefs structure and never
reaches `CommonRadioPrefs`.

### Hardware

techo-b43b, `LilyGo_T-Echo_room_server`, both images marked and each marker
confirmed in the image before flashing
(`unzip -p firmware.zip '*.bin' | grep -c -a <tag>` = 1) and reported by `ver`
in the same run that produced the readings.

| command | `dev-428a95c6` | `af-nan-wt` |
|---|---|---|
| `set af nan` | `OK` | `ERROR: af must be 0-9` |
| `get af` afterwards | `> 0` | `> 1.0`, unchanged |
| `set af -nan` | not measured | `ERROR: af must be 0-9` |
| `set af inf` | not measured | `ERROR: af must be 0-9` |
| `set af abc` | not measured | `ERROR: af must be 0-9` |
| `set dutycycle nan` | `OK - 0.0%` | `ERROR: dutycycle must be 1-100` |
| `get af` afterwards | `> 0` | `> 2.0`, unchanged |
| `set af 2` | not measured | `OK`, `get af` `> 2.0` |
| `set dutycycle 1` | not measured | `OK - 1.0%`, `get af` `> 99.0` |

Worth recording: on `dev` the poisoned field reads back as `> 0`, so nothing on
the CLI showed that it held a NaN. The board was left on the values it came
with, `af 1.0` and `dutycycle 50.0%`.

## Follow-ups

- **The merge into `integration` needs a hand.** Two things there are not a
textual conflict and must not be resolved by taking one side:
`test/mocks/RadioPrefsChecks.h:238-242` expects `set af nan` to be accepted
and has to be turned into a rejection, and `integration` already has an
environment called `native_ofast` with a different `test_filter`, which has to
become the union of both rather than either one.
- `isFinite` now exists twice with the same shape, here and in
`ConfigSerializer.cpp` from the range check fix. Worth unifying once both are
on `integration`.
- `set radio` parses `freq` and `bw` with `strtof` and discards the end pointer
entirely, so `set radio abc def 8 5` is read as zeros and rejected by the
range check rather than by the parse. Harmless today, and a separate change.
Loading
Loading