Commit Graph
16 Commits
Author SHA1 Message Date
TomandBen Meadors 7e11bde8c8 fix(beacon): repair the MeshBeacon radio switch/restore regression from #11573 (#11596)
* fix(radio): put the beacon restore back inside completeSending's if (p)

Reverts the RadioLibInterface and RadioInterface changes from #11573
(ac330e6a6). Hoisting MeshBeaconModule::reconfigureForBeaconTX() out of the
if (p) block changed its meaning from "a send completed" to "the radio went to
standby, for any reason" - and every driver's setStandby() calls
completeSending() unconditionally: on the pre-TX LBT scan, on startReceive(),
and inside reconfigure().

Two shipping faults followed, both confirmed on hardware the next day.

Every beacon transmitted on the wrong preset. isChannelActive() standbys the
radio immediately before each transmit, so the restore ran between the switch
and the key-up. The packet went out carrying the beacon channel hash with home
modem settings - inaudible to listeners on the target preset, an unknown hash
to listeners on the home one. Inert in both directions.

And unbounded recursion: the restore calls iface->reconfigure(), which
standbys, which calls completeSending(), which restores again, each level
running a full applyModemConfig(). It terminated in a HardFault and a silent
reboot (Reset reason 0x4 on nRF52, no panic output). The crash masked the
misdirection - the node died before Started Tx, so the wrong preset was
invisible until the recursion was fixed.

completeSending() clears sendingPacket at the top, so any nested call sees
p == NULL. The if (p) block was an accidental re-entrancy guard, and nothing
named it as such; removing it created both faults at once. Name it now.

This also reverts the beginSending() failure return that motivated the move,
and the startSend() scaffolding built to reach the restore on that path. The
payload bounds check it replaced is reinstated in the next commit, at a point
where refusing a packet is already a supported outcome.

* fix(radio): bound the payload at the radio queue, not mid-transmit

#11573 replaced beginSending()'s assert with a runtime check that logged,
released the packet and returned 0. beginSending() had never returned 0
before, so startSend() gained a failure path it had to unwind - and the
release moved ownership of the packet out of the caller that held it. That
new return value is what made hoisting the beacon restore look necessary.

The check itself is worth keeping. MeshPacket.encrypted has a nanopb maximum
of 256 bytes against a 240-byte radio buffer, and beginSending() is on the
path for relayed frames and phone-sourced packets, neither under our control.
Asserts are commonly compiled out in release builds, so what shipped was an
unchecked 256-into-240 memcpy driven by remote input.

Move it to Router::send(), immediately before iface->send(p) - the single
funnel for every over-the-air transmit. Refusing a packet there is already a
supported outcome: it returns TOO_LARGE, which is what perhapsEncode() already
returns for the same condition on the decoded path, and releases or NAKs
exactly as the duty-cycle limit above it does. Nothing radio-side has happened
at that point, so there is no half-started transmit to tear back down.

perhapsEncode()'s existing check does not cover this case: relayed and
phone-sourced frames arrive already encrypted and never reach it.

beginSending() keeps a last line of defence, but clamps rather than failing,
so it stays a call that always succeeds. Adds MAX_RADIO_PAYLOAD_LEN so both
sites name the same number instead of recomputing it.

Nothing about a beacon can trigger any of this - broadcast_message is
admin-truncated to 100 bytes, the whole MeshBeacon protobuf tops out at 180,
and observed beacons run to 106 - which is why this is separated from the
beacon changes rather than carried with them.

Tests: Router::send() refuses an oversized payload and still sends one that
exactly fills the buffer; beginSending() clamps instead of rejecting, and
leaves ordinary traffic whole.

* fix(beacon): guard the radio switch/restore against re-entry and early restore

Two checks in reconfigureForBeaconTX(), both independent of radio state, so
the switch/restore state machine no longer rests on sendingPacket's lifetime -
which is exactly the implicit coupling that let #11573 through.

A re-entrancy guard. Both branches end in iface->reconfigure(), whose
setStandby() runs completeSending(), which calls straight back in here. While
one call is applying a config, a nested call returns false and leaves it
alone. This covers the switch branch too, which had the same exposure with a
quieter symptom: a second switch before the restore would take the re-entrant
call as a restore and undo the switch still being applied, sending the beacon
on the home channel instead of its target.

A restore gate. The restore now waits for the packet that armed the switch to
actually finish, tracked by id against our own target table rather than by
asking the radio. Every caller that completes or abandons a beacon clears that
packet's target settings first, so a live entry means the TX has not happened
yet. cancelSending() now clears too, which is what keeps a cancelled beacon
from pinning the radio on the beacon config.

Together these make explicit the invariant completeSending()'s if (p) block
was carrying by accident: a future hoist of that call gets a logged no-op
instead of a crash and a misdirected beacon.

Also sets radioSwitched before reconfigure() rather than after, in both
branches, so the flag never describes a radio state that is not yet true.

Diagnostics, because every step of this dance was previously silent about its
own state. Count consecutive switches with no restore between them and log the
depth on both sides, so a change-change-change-restore run reads off the log;
switch #2 onwards prints the held home snapshot, which is the value that has
to survive a second switch. The restore names the config it is restoring to,
so a stale snapshot is visible directly. The re-entrancy guard logs when it
fires - expected exactly twice per beacon, so a burst means something new is
re-entering rather than a silent reboot. And setTargetRadioSettings() now
warns on the slot eviction that previously left a packet to key up on whatever
config was running - no crash, no log, wrong channel.

Reachable only with beacon broadcast enabled (the default flags are
LISTEN_ENABLED | LEGACY_SPLIT, so broadcast is off) and a target differing
from the running config; an identical target takes the early return and never
switches.

Tests: three re-entrancy cases against a RadioInterface whose reconfigure()
re-enters exactly as completeSending() does - bounded, so a regression fails
an assertion instead of overflowing the stack and taking the runner with it -
plus a restore that must defer until the beacon it switched for completes.

* fix(beacon,radio): address review findings on #11596

Payload ceiling was one byte too generous. RadioBuffer::payload is 240 bytes
because the buffer reserves MAX_LORA_PAYLOAD_LEN + 1, but the PHY caps a whole
frame at 255 and beginSending() adds a 16-byte header - so a 240-byte payload
produced a 256-byte frame. Define the ceiling as MAX_LORA_PAYLOAD_LEN -
sizeof(PacketHeader), matching what perhapsEncode() already enforces, with a
static_assert that it still fits the buffer.

Target-table eviction could unblock the restore gate. With every slot live,
setTargetRadioSettings() overwrote slot 0 - and if that slot held the packet the
outstanding switch is gated on, the restore came unblocked and put the home
config back under a beacon that had not keyed up. Skip that entry when choosing
a victim, and refuse the target outright if every slot is in flight. Needs
radioSwitched/switchedForId at file scope so the setter can see them.

Restore on every abandon path, not just the clear. cancelSending() dropped a
queued packet's target without restoring, so a beacon pre-switched by onNotify()
and then cancelled left the radio receiving on the beacon config;
removePendingTXPacket() did neither. Both now route through
abandonBeaconTarget(), as does startSend()'s tx-disabled branch. The restore
gate makes it a no-op when the abandoned packet is not the one we switched for.

No NAK on the oversize drop. p->channel is a wire hash by that point, not an
index, and Channels::getIndexByHash() is declared but never defined. Only
already-encrypted ingress can reach the gate anyway - perhapsEncode() bounds
everything it encodes - and those carry no index to answer on. Release and log.

Tests clear sendingPacket before releasing their packet, and assert against the
payload ceiling rather than the buffer size.

* fix(beacon): route the invalid-target drop through abandonBeaconTarget

onNotify()'s invalid-config drop was the one packet-abandonment path still
clearing the target directly instead of going through abandonBeaconTarget(),
so a packet that armed the radio switch and then failed validation would be
released with the radio left on the beacon config and nothing to restore it.
The helper's restore gate (targetRadioSettingsLive(switchedForId)) makes the
call a no-op for any packet that did not arm the switch, so this closes the
gap without risking a premature restore.

Also trims the switch-state comment to the two-line limit.

* fix(radio): take the abandoned packet as a pointer to const

cppcheck's constParameterPointer failed the check matrix on every board:
abandonBeaconTarget() only forwards the packet to clearTargetRadioSettings(),
which already takes a const pointer, so the parameter should be const too.

* refactor(radio): drive the beacon radio switch through TX hooks

RadioLibInterface named MeshBeaconModule at six call sites behind
MESHTASTIC_EXCLUDE_BEACON guards, so the driver carried per-packet beacon
state: when to switch preset, when a target config was invalid mid-transmit,
and when not to listen on a busy channel. Review on #11596 asked for the
module dependency to come out.

RadioTxHook is what the driver knows instead - beforeTransmit() returning
send/defer/drop, holdsRadio(), packetReleased() - on a self-registering
intrusive list, so nothing is allocated and a build without the beacon module
registers nothing and every call is a no-op. The four abandon paths (cancel,
remove-pending, TX disabled, completeSending) collapse onto one
packetReleased(), and the tri-state means the driver no longer has to know why
a packet wanted a re-delay or a drop.

MeshBeaconTxHook wraps the existing statics; the switch/restore logic, its
re-entrancy guard and its restore gate are untouched. It is created in
Modules.cpp inside the existing exclusion guard, so MESHTASTIC_EXCLUDE_BEACON
now works by nothing registering rather than by #ifdefs in the driver.

Behaviour is unchanged. The invalid-config LOG_DEBUG moves into the module and
the driver logs a generic refusal. Four tests cover the send/defer/drop mapping
and that an empty hook list is a no-op; native:test_mesh_beacon is 59/59.

Also notes in sendBeaconPacket that beacons uplink to MQTT on the primary
slot's uplink_enabled, and that the topic follows the beacon channel under the
crypto-override swap - both intentional.

* fix(beacon): restore the home config for a packet that jumps the queue

The restore gate added in 9cb7b96c9 refused to put the home config back while
the beacon that armed the switch was still live. That is right for a release -
completeSending() runs on every setStandby(), and restoring there would undo
the switch before the beacon had keyed up - but it also caught the case where
the driver is asking about a different packet it is about to transmit.

MeshPacketQueue::enqueue() inserts by priority (std::upper_bound over
CompareMeshPacketFunc), so an ACK or routing packet queued during the beacon's
deferred transmit delay lands ahead of it. beforeTransmit() then saw an
untagged packet, found the beacon still queued, skipped the restore and
returned PRETX_SEND - and the packet transmitted on the beacon's preset, slot
and region. It was encrypted and hashed for the home channel, so no receiver
on either preset could use it.

Apply the gate only to a null p. A non-null untagged packet is the driver
about to key up, which always restores; the restore returns PRETX_DEFER, so
the driver re-runs the delay and the channel scan on the config it will
actually transmit on. beforeTransmit() is the only caller that passes a
non-null untagged packet, so nothing else changes.

Found by CodeRabbit on #11596. native:test_mesh_beacon 60/60, including a
regression test for the queue transition; the four re-entrancy tests still
cover the null-p gate.

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-08-25 19:29:34 +00:00
Thomas Göttgens 8a15d9258f fix(test): unbreak test_radio under ASan (#11589)
* test_radio: prove the rejected packet was released via pool accounting, not pointer identity

* test-state.sh: silence the shell's own open failure when scanning /proc for survivors

* Trim the comments added with the test_radio and test-state fixes
2026-08-24 20:14:48 +00:00
Tadayoshi MIURA ac330e6a6b fix(radio): MeshBeacon heap leak and runtime packet payload size check (#11573)
* Fix for MeshBeacon packet leakage

* fix: add runtime payload size check against radiobuffer

* review fix for PR#11573: clear target radio settings before MeshBeacon packet release

* add unit test for radio buffer capacity check, removing related assert for the test

* review fix for PR#11573: add explicit verifaction against rejected packets
2026-08-23 11:00:30 +00:00
Tom bc035bb812 feat(lora): state a pinned userPrefs preset as the unset region's intent (#11507)
* feat(lora): state a pinned userPrefs preset as the unset region's intent

A vendor build can pin USERPREFS_LORACONFIG_MODEM_PRESET while leaving the
region unset, so a fresh flash comes up as region UNSET plus a deliberate
preset. Stock installs come up as region UNSET plus the LONG_FAST placeholder,
and nothing in FromRadio told the two apart - so clients treat every
unset-region node as factory-fresh and replace its preset with the region
default as soon as the user picks a region. A mesh pinned to SHORT_TURBO loses
every new node to LONG_FAST or LONG_TURBO, silently.

getRegionPresetMap() now emits an UNSET entry when, and only when, the build
pins a preset, stating that preset as both the group's sole entry and its
default. Stock builds are unchanged on the wire: no UNSET entry, which clients
already read as unconstrained.

This is intent, not enforcement. supportsPreset() still accepts any known
preset while the region is unset (#11496) and the radio is held silent either
way, so the device continues to honour whatever the user or an admin sets.

Costs one group slot and one region slot on pinned builds only (6->7 of 8,
34->35 of 38); exhaustion is logged and degrades to the existing unconstrained
behaviour.

* Trim comments to the project's one-to-two-line limit
2026-08-21 10:48:14 +00:00
Ethac.chenandThomas Göttgens 08722da3d9 fix(lr2021): live LF↔HF reconfigure via full begin() (#11279)
* fix(lr2021): full begin() on live LF/HF reconfigure.Avoid RadioLib -706 / assert when live-switching Sub-GHz ↔ LORA_24.

* fix(lr2021): align band-hop begin() with init() robustnessMirror.  RF-switch GPIOs, SPI/TCXO retries, and log CRC/RX-gain errors.

* chore: trunk fmt LR20x0Interface bandHop line wrap

* fix(lr2021): harden live band reconfigure (companion #1)

* fix(lr2021): reject invalid freq in band-hop path select

Require requestedMHz > 0 in isLr20x0BandHop, expose lr20x0ReconfigurePathfor FullBegin vs incremental selection, and extend native radio tests.

---------

Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com>
2026-08-05 10:07:14 +00:00
Tom b8b5582943 Regioninfo (#11056)
* advertise a superset of EU regional presets to client devices.

* trunk
2026-07-19 06:22:39 -05:00
Ben Meadors c5355641d3 Add MEDIUM_TURBO modem preset (#10988)
* Protobufs

* Wire up MEDIUM_TURBO modem preset

MEDIUM_TURBO (500 kHz, SF9, CR 4/5) already existed in the protobuf enum but
was never wired into firmware, so selecting it silently fell through to the
LONG_FAST default and rendered an "Invalid" display name.

Add its bw/sf/cr mapping (modemPresetToParams), display name (MediumTurbo/MedT),
PRESETS_STD membership (standard regions only — 500 kHz does not fit EU868's
250 kHz band, so it stays out of PRESETS_EU_868 and is rejected/clamped there),
and the MEDIUM SNR-grading bucket. Includes positive coverage in test_radio,
EU868-reject + US-accept coverage in test_admin_radio and test_mesh_beacon,
the STD preset count 9->10, an extended fuzz range, and the client-spec doc.

* Address review feedback on MEDIUM_TURBO tests

- test_mesh_beacon: assert has_mesh_beacon before checking the invalid preset was
  cleared, so the EU868-cleared test can't pass on a dropped message (matches the
  existing SHORT_TURBO test).
- test_fuzz_packets: draw modem presets from _ModemPreset_ARRAYSIZE instead of a
  hard-coded 17 so the fuzz range tracks future enum additions automatically.
2026-07-11 08:24:35 -05:00
TomandClaude Fable 5 d846780a9b More fuzz tests and small fixes for the findings (#10864)
* first pass tests

* more tests

* Fix two crafted-admin-packet crashes found by the E5 fuzzer

Both are reachable from an authorized admin (local from==0, admin channel,
or PKC) - remote DoS:

1. SIGFPE in LoRa config validation. A set_config LoRaConfig with
   use_preset=false and bandwidth=0 makes freqSlotWidth 0, so numFreqSlots
   is 0 and `hash(name) % numFreqSlots` (RadioInterface.cpp) divides by
   zero. Guard the modulo; the existing channel_num check then rejects/
   clamps the config.

2. Stack overflow in Channels::getKey. A SECONDARY channel at the primary
   slot with an empty PSK recursed into getKey(primaryIndex) forever. Skip
   the primary-key borrow when chIndex == primaryIndex.

Re-enable the E5 admin fuzzer to hit both triggers again (use_preset both
ways incl. bandwidth 0, plus the set_channel tag) as regression guards.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Correct fuzz-test invariants after the crash fixes

- E5 admin fuzz: node eviction under a filling NodeDB is legitimate, so
  assert only the bounded-count invariant, not that a specific seed node
  survives 6000 mutating ops.
- TMM blitz: scope off the nodeinfo direct-response send path (it needs a
  fully-wired MeshService/phone queue the fixture doesn't provide; the
  deterministic directResponse tests cover it). The crafted-nodenum
  rate/unknown/position cache stress is unchanged.

clod helped too

* realistic tests

* test: dedup fuzz RNG into shared test/support/DeterministicRng.h

The four in-tree fuzz suites (test_fuzz_decode, test_fuzz_packets,
test_hop_scaling, test_traffic_management) each carried a byte-identical
copy of the seeded 64-bit LCG (rngSeed/rngNext/rngByte/rngRange). Hoist
it into one shared header so there is a single generator to reason about
and no risk of the copies drifting. static inline keeps per-suite state
per translation unit and avoids -Wunused-function for suites that don't
use every helper. Also corrects a stale comment in test_traffic_management
(the blitz's nodeinfo direct-response path is intentionally left off).

No behavioral change: same constants, same per-suite seeds.

clod helped too

* test: fuzz uncovered ProtobufModule handlers and the MQTT downlink ingress

Extend the in-tree fuzz coverage to packet sources that previously had
none:

- test_fuzz_packets E8/E9/E10: drive PositionModule, DeviceTelemetryModule
  and NeighborInfoModule at handleReceivedProtobuf directly (via using-shims,
  bypassing the ProtobufModule reply/send path so no router is needed). The
  fixture already stands up nodeDB/service/channels, and nodeStatus/powerStatus
  are auto-initialized in main.cpp, so no new globals are required. Adds a
  shared fuzzRxHeader() helper for crafting adversarial RX packet headers.
- test_fuzz_decode: add meshtastic_KeyVerification to the decode table. The
  KeyVerification and StoreForward handler paths are documented as decode-level
  only, with the concrete reason each is intrinsic (private-state gating /
  PSRAM + self-pointer wiring), not a fixture gap.
- test_mqtt: test_receiveFuzzServiceEnvelope blitzes the non-RF broker-push
  ingress (onReceiveProto) two ways - raw garbage bytes that must fail envelope
  decode cleanly, and a well-formed ServiceEnvelope wrapping a crafted inner
  MeshPacket over crafted channel_id/gateway_id - exercising the channel match,
  isFromUs, XEdDSA receive policy and perhapsDecode chain. Adds a deliverRaw()
  passthrough to MQTTUnitTest.

All under the coverage env (ASan/LSan). No firmware/src changes. Full sweep
GREEN 27/27, 544 cases.

clod helped too

* Harden LoRa/channel config against crafted admin messages; consolidate test helpers

Production (review findings on the hot-fuzz crash fixes):
- Clamp bandwidth at the source (clampBandwidthKHz) in checkOrClampConfigLora
  and applyModemConfig so numFreqSlots can never be 0 for any consumer; a
  bandwidth-0 set_config previously passed validation and re-armed the SIGFPE
  on the next applyModemConfig.
- Guard applyModemConfig's hash % numFreqSlots (the validator's sibling modulo
  was fixed earlier but this one was still unguarded).
- Enforce the primary-channel invariant in Channels::onConfigChanged: a config
  demoting every slot now re-promotes the stale SECONDARY slot (keeping its
  key) or restores the default channel if the slot is DISABLED, instead of
  leaving every getPrimaryIndex() reader on a non-primary slot. The getKey
  recursion guard stays as defense-in-depth.

Tests:
- New test/support/MockMeshService.h and AdminModuleTestShim.h replace four
  byte-identical mocks and three divergent admin shims (test_mqtt's capturing
  mock is genuinely different and stays).
- DeterministicRng.h: add rngFill() (replaces 14 hand-rolled fill loops) and
  rngEdgeNodeNum() (unifies the three NodeNum boundary pools).
- Extract fuzzChannelSettings() shared by the set_channel case and fuzzBeacon.
- fuzzBeacon: the un-terminated branch now fills the whole buffer with non-NUL
  bytes so the strnlen bound is actually stressed (~50% of iterations, not ~4%).
- E6 beacon fuzz: replace the TEST_ASSERT_TRUE(true) tautology with real
  invariants (handler never consumes; offers land in lastReceivedOffer keyed
  to the sender).
- Trim the seven over-long comment blocks flagged against the 1-2 line rule;
  the FINDINGS trailer moves to this commit message (see production notes).

Full native suite GREEN 27/27 under the coverage (ASan/LSan) env.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Clamp UTF-8 char length in the emote walkers; add test_fuzz_emotes

A TEXT_MESSAGE payload is opaque protobuf bytes, so PB_VALIDATE_UTF8 never
screens it - invalid UTF-8 and truncated multi-byte lead bytes reach the
emote/width render path verbatim. EmoteRenderer's walkers advanced by
utf8CharLen(lead) without clamping to the bytes actually remaining, so a
truncated lead (e.g. a lone 0xF0, which claims 4 bytes) near the end of the
buffer made getUtf8ChunkWidth's memcpy read past the string. ASan confirms a
heap-buffer-overflow READ from measureStringWithEmotes.

Add utf8CharLenClamped() and use it at every walk site (width measure,
truncation cut-loop, and the draw-path text-run/chunk builders); the one
already-guarded site (matchAtIgnoringModifiers) is unchanged.

New test/test_fuzz_emotes drives measureStringWithEmotes and truncateToWidth
over adversarial byte strings (biased to embed/end in truncated multi-byte
leads) in exact-sized heap buffers so any over-read is a hard ASan fault. Its
headless display uses a synthetic font (firstChar 0, fontData centered in a
large buffer) so the stock OLEDDisplay::getStringWidth - which indexes the
font jump table with a signed char and over-reads for any byte >= 0x80 - does
not mask the finding. native-suite-count bumped 27 -> 28.

Full native suite GREEN 28/28 under the coverage (ASan/LSan) env.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Keep emote width measurement in-bounds for non-ASCII bytes

OLEDDisplay::getStringWidth (the utf8=false path EmoteRenderer uses on default
builds) indexes the font jump table by (c - firstChar) with a signed char and
no bounds check, so any byte outside printable ASCII - high bytes from UTF-8
text, but also a stray control byte like 0x0A - reads outside the font array.
On-device this reads adjacent flash and returns a garbage width; under ASan the
test_fuzz_emotes fuzzer flags it as a global-buffer-overflow, and it made the
non-ASCII width measurement meaningless either way.

The OLED driver is a pinned upstream dependency, so guard it firmware-side in
EmoteRenderer's getStringWidth helper: measure a sanitized copy where any byte
outside [0x20, 0x7E] counts as a '?' placeholder. Printable ASCII is unchanged
and the UA/RU lookup path is untouched.

test_fuzz_emotes now drives a real ArialMT font instead of the synthetic
in-bounds font it needed before this fix, so the suite exercises the true
production width path (utf8CharLen clamp + this sanitizer) end to end. The same
fuzzer tripped the global-buffer-overflow before this change.

Full native suite GREEN 28/28 under the coverage (ASan/LSan) env.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 13:19:01 -05:00
Tom 3becaf2d95 emdashes begone (#10847) 2026-07-01 19:01:27 -05:00
Ben Meadors dd1ec9d462 Lora region preset map (#10736)
* Added lora region and preset maps

* Protos

* Address PR review feedback

- Log (and break/skip) when the region preset map exceeds its array bounds
  instead of silently dropping regions
- Derive test bounds from the generated nanopb array sizes via sizeof()
  instead of hard-coded magic numbers
- Fix want_config sequence comment (missing comma, STATE_SEND_MODULECONFIG)
- Specify a language on the spec's fenced code blocks (markdownlint MD040)

* Fix want_config stall: handle STATE_SEND_REGION_PRESETS in PhoneAPI::available()

available() had a separate per-state switch that wasn't updated for the new
state, so it returned false ('unexpected state 5') and getFromRadio() was
never called - the config handshake stalled after metadata and the client
timed out. Verified via the native simulator integration test.
2026-06-19 19:56:24 -05:00
Austin ef51c7ec11 Add 2 Meter (~144mhz) Amateur Radio Regions (#10623)
Default slots:
ITU1_2M: Slot 26 (144.510 MHz)
ITU2_2M: Slot 51 (145.010 MHz)
ITU3_2M: Slot 33 (144.650 MHz)
2026-06-03 18:05:03 -04:00
Austin 3e873c51b7 Add TinyFast and TinySlow presets to modem configuration and menu actions (#10597) 2026-06-03 16:42:39 +01:00
Austinandcopilot-swe-agent[bot] cbfa8d7ffd Add low bandwidth conversions to MeshRadio (#10595)
* Add low bandwidth conversions to MeshRadio

* Add test cases for new bandwidth conversion values (8/10/16/21/42) and round-trip tests

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-06-01 18:24:41 -04:00
Ben MeadorsandCopilot 222d3e6b62 Fix zero CR and add unit tests for applyModemConfig coding rate behavior (#10070)
* Fix zero CR and add unit tests for applyModemConfig coding rate behavior

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix: initialize region configuration in setUp for RadioInterface tests

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-04 08:27:39 +11:00
TomandCopilot Autofix powered by AI 22d63fa69c Lora settings expansion and validation logic improvement (#9878)
* Enhance LoRa configuration with modem presets and validation logic

* Rename bootstrapLoRaConfigFromPreset tests to validateModemConfig for clarity and consistency

* additional tidy-ups to the validateModemConfig - still fundamentally broken at this point

* Enhance region validation by adding numPresets to RegionInfo and implementing validateRegionConfig in RadioInterface

* Add validation for modem configuration in applyModemConfig

* Fix region unset handling and improve modem config validation in handleSetConfig

* Refactor LoRa configuration validation methods and introduce clamping method for invalid settings

* Update handleSetConfig to use fromOthers parameter to either correct or reject invalid settings

* Fix some of the copilot review comments for LoRa configuration validation and clamping methods; add tests for region and preset handling

* Redid the slot default checking and calculation. Should resolve the outstanding comments.

* Add bandwidth calculation for LoRa modem preset fallback in clampConfigLora

* Remove unused preset name variable in validateConfigLora and fix default frequency slot check in applyModemConfig

* update tests for region handling

* Got the synthetic colleague to add mock service for testing

* Flash savings... hopefully

* Refactor modem preset handling to use sentinel values and improve default preset retrieval

* Refactor region handling to use profile structures for modem presets and channel calculations

* added comments for clarity on parameters

* Add shadow table tests and validateConfigLora enhancements for region presets

* Add isFromUs tests for preset validation in AdminModule

* Respond to copilot github review

* address copilot comments

* address null poointers

* fix build errors

* Fix the fix, undo the silly suggestions from synthetic reviewer.

* we all float here

* Fix include path for AdminModule in test_main.cpp

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* More suggestion fixes

* admin module merge conflicts

* admin module fixes from merge hell

* fix: initialize default frequency slot and custom channel name; update LNA mode handling

* save some bytes...

* fix: simplify error logging for bandwidth checks in LoRa configuration

* Update src/mesh/MeshRadio.h

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-20 07:43:47 -05:00
Ben MeadorsandCopilot 8894a0b711 Consolidate LoRa params / preset logic and fix display of preset values (#9413)
* Consolidate LoRa params / preset logic and fix display of preset values

* Move preset coercion logic to RadioInterface

* Fix some warnings

* Fix warnings

* STM32 fix

* Add unit tests

* Update src/mesh/RadioInterface.h

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-25 18:02:26 -06:00