The handshake's third message must be PKI-encrypted, and licensed operation
neither sends nor decrypts PKI. A licensed node still opened a session,
raised the security-number prompt, and waited until it timed out.
A licensed node now refuses to start a session, from the admin message or
the screen, and tells the client why. It ignores a remote node's request
without a NAK, as a busy node does. The screen's node menu hides the Key
Verification entry in licensed mode.
Claude-Session: https://claude.ai/code/session_011DjtCrF8NLMGs9DtEVQkUK
Co-authored-by: Claude <noreply@anthropic.com>
* fix(motion): include NodeDB.h for the global `config`
MotionSensor.cpp uses config.display unconditionally (compass
orientation, wake-on-motion) but only reached the global `config`
through a fragile transitive include via Power.h/Screen.h. On a
screenless board that also excludes the WiFi/web stack, that
transitive path disappears and the file fails to compile with
'config' was not declared in this scope. Include mesh/NodeDB.h
directly so MotionSensor always compiles regardless of which
modules a variant excludes.
Co-authored-by: Cursor <cursoragent@cursor.com>
* cleaning up comments
Removed comments explaining the inclusion of NodeDB.h.
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
* fix(gps): match Airoha idle_int sleep to the documented sequence
For boards that wake the AG3352 through idle_int (ThinkNode M8 and M9):
- Drop $PAIR382,1 before the sleep command. It locks the CPU out of
sleep, which works against both $PAIR003 and $PAIR650.
- Hold idle_int low 50 ms before sending, and wait 50 ms after the
command takes before EN cuts the core, as Elecrow's reference does.
- A successful $PAIR650 need not reply, so also accept NMEA going quiet
for a full cycle as success; resend once otherwise.
Boards with RTC_INT are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ThinkNode M8: reset the GPS out of backup mode at boot
If the MCU resets while the AG3352 is in backup mode, the GPS stays
there and the cached-probe check gets no answer, forcing a full re-probe.
Pulse RESETB in earlyInitVariant(), as the M5 does with its wake pulse
in #12113.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Key verification: create the responder's NodeDB entry on commit
An initiator that did not hold the responder in NodeDB could not complete
manual key verification: DO_VERIFY logged "Attempted to commit key, but
unknown node" and the key was never marked verified.
KeyVerificationModule consumes the responder's hash2 reply, so module
dispatch stops before RoutingModule sniffs the packet into NodeDB and no
entry is created. commitVerifiedRemoteNode() assumed one existed. When it
does not, commit the pending key the reply carried (the key the user has
just confirmed) through commitRemoteKey(), which creates the entry.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Key verification: commit only from the final-confirmation state
DO_VERIFY with a matching nonce, and the on-screen Accept callbacks, called
commitVerifiedRemoteNode() in any state. The app learns the nonce at the
security-number prompt, so an early DO_VERIFY (or one after a wrong number)
committed the unchecked pending key as ManuallyVerified. Require
SENDER_AWAITING_USER or RECEIVER_AWAITING_USER before committing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011DjtCrF8NLMGs9DtEVQkUK
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* initial commit for L76K/AG3352 dual GNSS detection
* GPIO fixes according firmware demo
* resolve rabbits
* define PIN_GPS_SWITCH
* fix wake button and power pin
* clear response to avoid logging junk data
* increase IO07 samples to more reliably detect version
* ThinkNode M5: read the v1.0 GPS switch on GPIO10
On a v1.0 (L76K) board the GPS switch is sensed on GPIO10, not GPIO18.
Watching GPIO18 there meant flipping the switch did nothing. Pick the pin
by revision, as the GPS UART pins already are; m5HasAg3352 is set in
earlyInitVariant() before createGps() configures and starts watching it.
Tested on a v1.0 board: switch off and on now turns the GPS off and on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ThinkNode M5: trunk fmt variant.h
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* SX126x: follow RadioLib's XTAL fallback and log the chip's state after init
Since 7.5.0 (92e2f333f1) RadioLib retries config() on the XTAL inside begin() when the TCXO
will not start, zeroes its own Vref and returns success. The TCXO_OPTIONAL ladder therefore
never builds its XTAL instance, and Meshtastic kept reporting "TCXO, Vref 1.8" on crystal boards.
- reinitChip(): take RadioLib's zeroed Vref after a successful begin()
- init(): one line with the oscillator in use, chip mode, command status and device errors
- the ladder's SX1262/SX1268 success lines report the oscillator actually in use
- drop the embedded pre-init "DIO3 as TCXO Vref" debug line, which only stated the attempt
* tronk
* SX126x: don't report a stale chip state when the diagnostic read fails
status is a function-local static filled by parseStatusCb, so an
SPIreadStream() that fails before parsing left the previous call's byte in it,
and the init log reported that as the current mode and command status. Check
the read and log the failure instead.
* fix(sx126x): keep RadioLib's status validation on the diagnostic read
The stand-in parseStatusCb captured the status byte but always returned
RADIOLIB_ERR_NONE, so a failed command or a 0x00/0xFF (no chip) byte was
logged as chip state. Chain to the saved parser so those fail the read.
* SX126x: comment out the post-init GetDeviceErrors read
The raw SPI read and parseStatusCb swap run after startReceive(), outside a
RadioSequence, so they can race the RX readout task. Kept as a commented-out
debug aid for init failures.
---------
Co-authored-by: Tom <116762865+Nestpebble@users.noreply.github.com>
* CI: let a running push run finish instead of cancelling it
#12162 put pushes in one concurrency group per branch with
cancel-in-progress, so a merge landing mid-run threw away a part-built
full matrix. Set cancel-in-progress only for non-push events. A push
now lets the running run finish and waits as the group's single pending
run; a newer push replaces that pending run before it starts, so pushes
that land mid-run collapse into one follow-up run. PRs still cancel
their superseded runs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011DdE8uBENCwUMApqYdut2L
* CI: correct the concurrency group comment
A PR groups on head_ref and a push on the full ref, so the two never
share a group; the comment said they did. The expression is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011DdE8uBENCwUMApqYdut2L
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat(variants): add RAK WisMesh Dongle. USB-powered headless adapter on the RAK4631 module. Hardware model stays RAK4631 until protobufs add WISMESH_DONGLE. The boot VDD gate is disabled because phone VBUS makes the SAADC sample look unsafe.
* add board_level
* fix(rak_wismesh_dongle): set VDD gate to 1 mV so cppcheck passes
---------
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
* CI: run pio check on PRs only, not in the merge queue
The merge queue re-ran cppcheck on the same --level pr boards the PR run
had already checked. Restrict the check job to pull_request; ci-gate
already treats a skipped check as passing, so merge_group runs gate on
the builds alone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011DdE8uBENCwUMApqYdut2L
* CI: cancel superseded push runs on the same branch
Push runs fell back to github.run_id for their concurrency group, so
every merge to develop ran the full 156-board matrix to completion even
when a newer merge had already landed. Group pushes by github.ref so the
newer push cancels the older run. PRs keep grouping by head_ref, and
merge_group, schedule and workflow_dispatch runs keep a group of their
own, so the queue, the nightly and releases are never cancelled.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011DdE8uBENCwUMApqYdut2L
* CI: move axiometa-genesis-mini out of the PR board set
Set its board_level to release. PRs and the merge queue now build 17
boards instead of 18; the board is still built in the full matrix on
push, nightly and release.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011DdE8uBENCwUMApqYdut2L
* CI: reuse the merge queue's PR-level builds in the post-merge run
A post-merge push to master/develop lands the exact commit its
merge_group run already built, so the PR-level boards were compiled
twice. When the push's merge_group run is found and its ci-gate passed,
setup marks those boards 'reuse' and build_firmware.yml copies their
firmware and manifest artifacts from that run instead of building. The
artifacts keep their names, so the size report baseline, the RAM/flash
budget gate and MemBrowse are unchanged.
If no such run is found (event/* and feature/* pushes, a push that
bypassed the queue, an API failure), every board builds as before.
ci-gate is read through the check-runs API: the run's jobs endpoint
returns 502 for a run this size.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011DdE8uBENCwUMApqYdut2L
* CI: skip macOS, Windows and debian-src on reused post-merge pushes
When setup finds the merge_group run that already built this commit
(reuse_run_id), these jobs ran on that exact commit there and nothing
on push consumes their output: the debian-src artifact is only read by
release-artifacts on workflow_dispatch. test-native, WASM and Docker
still run, because only pushes to develop save their caches.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011DdE8uBENCwUMApqYdut2L
* CI: only skip OS builds on push when the merge queue run passed them
ci-gate needs only build and check, so a merge_group run whose macOS,
Windows or debian-src job failed still merges, and skipping those jobs
on the push hid the failure. setup now sets skip_os_builds only when the
latest attempt of every MacOS, Windows and build-debian-src check run in
that merge_group run concluded success and all three are present; the
jobs skip on that flag instead of reuse_run_id.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011DdE8uBENCwUMApqYdut2L
* CI: harden the post-merge reuse lookup (review fixes)
- Find the merge_group run at any status. The queue merges once ci-gate
passes, often while jobs outside it still run, so requiring a
completed run usually found nothing and rebuilt every board.
- Mark a board 'reuse' only if its firmware and manifest artifacts are
still present and unexpired in that run; any other board compiles.
setup now needs version to build the exact artifact names.
- Never skip MacOS, Windows or build-debian-src on the default branch:
those jobs save their caches only on its pushes. The skip stays for
master.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011DdE8uBENCwUMApqYdut2L
* CI: drop the macOS/Windows/debian-src skip on post-merge pushes
Those jobs save their caches only on default-branch pushes, so the skip
was already limited to master, where it saved too little to justify the
check-run lookup. They now run on every push exactly as before this PR.
Reuse of the PR-level firmware builds is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011DdE8uBENCwUMApqYdut2L
* CI: look up the merge_group run by workflow file, not display name
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011DdE8uBENCwUMApqYdut2L
* CI: build no firmware in the merge queue; drop post-merge artifact reuse
Per review: the merge queue now only batches PRs into develop/master.
setup is skipped on merge_group, which skips the build and check
matrices that depend on it, and ci-gate accepts a skipped build on
merge_group only; a PR must still build. Post-merge pushes build the
full matrix as before.
With no firmware built in the queue there is nothing to reuse, so the
post-merge artifact copy (reuse-build, the merge_group run lookup and
setup's dependency on version) is removed and build_firmware.yml is
back to develop's version. The merge_group branches of setup's matrix
generation are removed with it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011DdE8uBENCwUMApqYdut2L
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Stop treating BATTERY_PIN -1 as a battery ADC
Six variants defined BATTERY_PIN -1 to mean "no battery sense", but every guard
is `#ifdef BATTERY_PIN`, so the analog battery read compiled in anyway. On the
Seeed Xiao S3 it samples an unwired GPIO1 and reports a battery that is not
there: 0% at 3112 mV on one board, charging at 6310 mV on another. On nRF52,
analogRead(-1) indexes past the pin map.
Leave BATTERY_PIN undefined on those boards, which takes the existing
external-power path, and fail the build when a variant defines a negative pin.
* Keep the INA battery path on boards without a battery ADC pin
AnalogBatteryLevel is the only reader of a battery INA configured through
power.device_battery_ina_address, and analogInit() only installed it when
BATTERY_PIN was defined. Dropping the -1 placeholder from six variants
therefore also dropped their INA path. Install AnalogBatteryLevel on any
board without a pin when the environmental sensors are compiled in, and
let the PowerFSM treat such a board as externally powered only while no
battery source has been found.
Admin writes can enable Store & Forward before the scheduled reboot
constructs its module. Check the singleton before calling isServer() so
the configuration response reaches the phone without a null dereference
and pending settings transactions can commit.
Validated tbeam-s3-core builds and USB first-enable YAML/standalone tests
against an unpatched baseline, enabled controls, and reboot persistence.
The full native coverage suite and Trunk checks pass; native tests do not
execute this ESP32-only condition.
The other platforms moved their Crypto comment to datasource=git-refs in
#11065 and Renovate has kept them current since (#11879, 1c817c2). rp2040
and rp2350 still say datasource=github-tags, and meshtastic/Crypto has no
tags, so Renovate never updates them: both are still on 591ff9a.
Switch the comment to the same git-refs form and bump the pin to the
current main (9ceb213), which brings in meshtastic/Crypto #4, #5, #7 and #8.
* InkHUD: load offline map tiles from SD cards
InkHUD map backgrounds previously came only from MapTile.h compiled into
the firmware image. This commit expands that to read map tiles from an SD Card.
* Firmware-compiled tiles remain the first choice, with SD Card secondary.
* SD Card MapTiles' header position table and payload descriptors loaded into PSRAM.
* The index is cached in PSRAM when allocation succeeds; otherwise streamed index access.
* Validates the file magic, layout, counts and index/data bounds before use.
* Added an MTB1 reader for /Map/MapTile.bin
* Supports sparse and grid indexes together with white, black and raw LZ4 block entries.
* Moved the tile format constants and LZ4 decoder out of MapApplet.
* Reuses the applet's existing decoded-tile buffer.
Compressed tile imagery remains on the card. Rendering resolves
coordinates from the cached index, takes spiLock for a bounded
open/seek/read/close operation, releases the bus, then decompresses
the tile. Memory use therefore stays bounded by the index and one
compressed and decoded tile rather than the size of the map file.
The T5S3 E-Paper V2 connects its SD card and LoRa radio to the same SPI
pins. Drop SDCARD_USE_SPI1 only from the InkHUD environment so both use
the default SPI instance and the existing shared lock. Keep SD map
enablement and preload setup in the T5S3 variant; the non-InkHUD V2
environment retains its existing SPI1 configuration.
Update MapApplet to walk only the visible tile-coordinate range, wrap
tile X coordinates across the antimeridian and check embedded tiles
before requesting an SD tile. A missing, invalid or temporarily busy SD
source simply leaves that background tile undrawn without changing the
existing map applets or their compiled-in tile behavior.
* PowerFSM: service non-input GPIO wakeups after light sleep
Before c308d0aca, a light-sleep wake that was not caused by the button
triggered EVENT_WAKE_TIMER. Despite its name, that event is also the
PowerFSM path for leaving LS after a peripheral interrupt so the normal
loop can service the pending hardware IRQ.
c308d0aca (feat: Support Elecrow ThinkNode M9, August 17, 2026) split
ESP_SLEEP_WAKEUP_GPIO out of the default switch branch to recognize
KB_INT as a key press. The new GPIO branch retained the EVENT_PRESS path
but omitted the previous fallback for GPIO wakes that were not buttons.
546b678d5 later added explicit motion-interrupt handling, but other GPIO
wake sources still produced no PowerFSM event.
LoRa DIO and other ESP32 peripheral wake sources are reported as
ESP_SLEEP_WAKEUP_GPIO. When neither the button nor motion line is
asserted, lsIdle() therefore remains in LS and can enter light sleep
again before the cooperative loop services the pending interrupt. A
latched IRQ can repeatedly wake the CPU into the same path, producing a
wake/sleep loop while delaying radio and other peripheral handling.
Restore the old fallback after the button and motion checks. An
unattributed GPIO wake now triggers EVENT_WAKE_TIMER, moving the FSM
from LS to NB or DARK so the regular loop can handle the interrupt
before light sleep is attempted again. Button wakes still use
EVENT_PRESS, motion wakes still use EVENT_INPUT, and non-input GPIO
wakes do not turn on the screen.
* InkHUD: centralize map tile zoom lookup
Select the embedded zoom metadata once and share the lookup between the zoom count and index helpers. Preserve the SD-card fallback while avoiding duplicate ternary expressions when the default tile metadata is empty.
* InkHUD: bound or stream SD map indexes
Limit index caching to a configurable percentage of available PSRAM
and fall back to block-based SD streaming when the index is too large or
allocation fails. Release the SPI lock and yield between blocks to keep
other tasks responsive.
Retry temporary SD failures and publish map header state only after all
required index reads complete successfully.
* added requested build flag for sdcard maps to t3s3 variant
---------
Co-authored-by: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com>
* feat(raksensorhub): add SensorHub uplink driver for WisMesh Hub
Add HAS_RAKHUB env rak_wismesh_sensorhub with 1-Wire bus + IPSO cache.
Ship standard environment metrics only; soil/water chemistry stays gated until SoilWaterMetrics lands in firmware.
* fix(raksensorhub): harden RX length, MODBUS log, and power TTL
Reject oversized frames without uint16 wrap, validate MODBUS before head[] access, TTL hub power like other scalars, and set board_level for CI matrix.
* fix(raksensorhub): track HubPower freshness per field
Use separate timestamps for voltage, current, and capacity so one IPSO cannot keep stale siblings fresh, and percent no longer depends on volMv.
* fix(raksensorhub): stamp 0-unset cache times with Time::skipZero
A wrap or boot 0 tick must not look like never-updated; freshness reads use Time::stampMillis() too.
---------
Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com>
* Don't let a stale HEADER_ERR re-arm RX over a live frame
Before: a header that fails on noise, a collision or a foreign frame
latches HEADER_ERR and no RX_DONE follows it. `checkStaleRxFlags()`
counts HEADER_VALID and HEADER_ERR as one boolean, so the error starts
the max-packet window that re-arms RX. A good frame arriving inside that
window raises HEADER_VALID beside the error, which does not restart the
window - a header was already up. One max packet after the error was
first seen, the poll re-arms, and `startReceive()`'s standby aborts the
frame still on the air.
After: the window is timed from the valid header alone. A HEADER_ERR
beside a HEADER_VALID is retired at once and decides nothing, because one
header raises one flag or the other (SX1262 DS rev 2.1 table 13-29), so
the two together are two frames and the error is the older one.
A lone stale HEADER_ERR past its window is now cleared rather than
re-armed on every chip but SX128x. The re-arm exists for the SX1280 wedge
of DS rev 3.3 16.2, which a clear does not unwedge; on SX126x, LR11x0 and
LR20x0 the demodulator is already hunting for a preamble again, and a
clear never aborts a frame that may be in one. `headerErrWedgesRx()` is
the per-chip hook, true only on SX128x. SX126x already clears HEADER_ERR
unconditionally in `resumeRunningReceive()` for the same reason.
The busy decision is unaffected: `receiveDetected()` reads only
HEADER_VALID and PREAMBLE_DETECTED, so the cost of the bug was a dropped
reception, never a talk-over.
How: `staleRxFlagAction()` takes HEADER_VALID and HEADER_ERR separately
plus the per-chip wedge flag, and gains a ClearHeaderErr verdict.
`checkStaleRxFlags()` keeps a second stamp bit for "the window was
started by a valid header", so a valid header restarts a failed one's
window the way develop already has a header restart a bare preamble's.
Five native cases cover it, two of them the regression itself.
Found and analysed by Tom (NomDeTom), who also wrote the commit the
behaviour came from, 45262f825 in #11137.
Claude-Session: https://claude.ai/code/session_01DLRPmdJQTu6MDG7o2QLceR
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* Retire a dead frame's preamble with its stale header error
A failed header latches PREAMBLE_DETECTED beside HEADER_ERR, and both belong to
the frame that died. Clearing only the error leaves the preamble up, so the next
send's receiveDetected() holds TX for one max packet time over a frame that is
already gone - a wait the re-arm this path replaces never cost, because
startReceive()'s standby cleared the whole flag register at once.
Clear both when no valid header stands beside the error. A valid header's own
window is untouched, as before.
Found by NomDeTom while reviewing the parent change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DLRPmdJQTu6MDG7o2QLceR
* Re-arm on a wedging chip when both header flags are latched
Where a header error wedges RX (SX1280 DS rev 3.3 16.2), HEADER_VALID and
HEADER_ERR latched together do not mean what they mean elsewhere. RX cannot
reach a later frame's header while wedged, so the error is the newer of the
two: the frame behind that valid header is already dead and the chip is deaf.
Retiring the error and leaving the valid header's window to expire kept it deaf
for another poll tick. Re-arm instead - there is nothing left for the standby
to abort.
Every other chip keeps hunting for a preamble after a header error, so there
the error is the older flag and a clear is still right.
Found by NomDeTom reviewing the parent commits.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DLRPmdJQTu6MDG7o2QLceR
* Retire a dropped frame's RX flags, not just its RX_DONE
clearReadIrqs() is the give-up path: a frame has completed, nobody will
read it out, and its flags are cleared so the next RX_DONE raises a fresh
edge. It cleared RX_DONE and CRC_ERR only, leaving HEADER_VALID and
PREAMBLE_DETECTED latched for a frame that no longer exists.
checkStaleRxFlags() then times its max-packet window from that dead
frame. The next frame's own HEADER_VALID lands on a bit that is already
set, so the window is never restarted - the same shape as the stale
HEADER_ERR bug this PR fixes, one step earlier in the path - and when it
expires startReceive()'s standby aborts the live frame. The valid-header
restart cannot help here: the stale flag and the live frame's flag are
the same bit.
readData() clears every flag on a successful read, so a drop now clears
what a read would have. That takes the same risk a read takes: a frame
arriving during the drop loses its latched preamble, so LBT is blind to
it until its header. The alternative is the frame being aborted outright,
which is what happens today.
Found by Tom (@NomDeTom) reviewing this PR.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DLRPmdJQTu6MDG7o2QLceR
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* LoRa: size the preamble hold for the frame that could be arriving
The hold ran for getPacketTime(DATA_PAYLOAD_LEN + sizeof(PacketHeader)):
249 bytes at our own coding rate. A sender's header carries its own CR, up
to 4/8, and that header is unreadable when the hold starts, so a legal
255-byte frame at CR 4/8 with CRC outlives the hold and we key up over it.
That is the case the hold exists for, since LoRa we cannot decode - a
foreign sync word - never produces HEADER_VALID at all.
maxRxFrameMsec() times the 255-byte PHY frame at CR 4/8 with CRC from the
configured modem, through the radio's own calculateTimeOnAir(), and falls
back to getPacketTime(MAX_LORA_PAYLOAD_LEN) if the radio answers an error
code (#11935). At SHORT_TURBO that is 159 ms rather than 100 ms, at
LONG_FAST 3.3 s rather than 2.1 s. The HEADER_VALID ageing in
receiveDetected() takes the same bound, for the same reason.
checkStaleRxFlags() and checkCadHandoffTimeout() keep the shorter window:
they decide a latched flag is dead and recover the chip, where waiting
longer delays the recovery rather than protecting a frame.
Three test cases in test/test_radio pin the sizing, the error fallback and
that the hold outlives the flag clear that starts it. FakeIrqRadio now
answers calculateTimeOnAir() and honours a clear.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CStem6K6MQRBNR8YCFgtCE
* LoRa: let the pre-TX CAD settle an old bare-preamble hold
A hold runs for one max frame whether or not anything is really arriving:
159 ms at SHORT_TURBO, but 2.1 s at LONG_FAST and 14 s at LONG_SLOW. For
all of it the TX path only re-rolls its backoff - canSendImmediately() is
false, so the pre-TX CAD never runs and nothing re-examines the channel.
A noise preamble therefore costs seconds of TX and can overflow the Tx
queue, which is what @GUVWAF objected to on #11968.
A frame we could decode latches HEADER_VALID within the rest of the
preamble plus the ~12.25 symbols of sync word, SFD and explicit header.
Once that grace has passed with no header, the sighting is noise or LoRa we
cannot read, and neither deserves the rest of the hold: preambleHoldPeekable()
lets the TX path fall through to its CAD and send if the channel is clear.
At LONG_FAST the grace is 248 ms against the old code's own 262 ms deadline,
and 8 ms at SHORT_TURBO, so a spurious preamble now blocks TX for slightly
less than develop did before the hold existed - and then actually listens.
HEADER_VALID keeps the full hold: that is a real frame on our own modem.
receiveDetected() notes a header even while the hold short-circuits, so a
frame whose header arrives mid-hold is never peeked past, and a new hold
forgets the previous one's header. Our own transmit ends the hold, since the
CAD has just judged the channel.
preambleHeaderGraceMsec() computes the grace from sf and bw rather than
RadioInterface::preambleTimeMsec, which is whole milliseconds: above 2 GHz a
12-symbol preamble is 0.95 ms, so that value is 0 and the grace would be 0,
making every sighting peekable the moment it was seen.
Six more cases in test/test_radio: the grace at wide-LoRa and sub-GHz
presets, its rounding, and the peek inside the grace, after it, behind a
header and across a new hold.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CStem6K6MQRBNR8YCFgtCE
* LoRa: ask for the frame bound before reading back the CR it timed
test_preambleHold_isSizedForTheWorstCaseFrame read phy.lastCodingRate
straight after the first preamble sighting, where nothing has yet asked
the chip to time anything: preambleHoldActive() short-circuits on the
unset stamp, so the sighting that starts a hold never reaches
calculateTimeOnAir() and the fake still reports a coding rate of 0.
Ask maxRxFrameMsec() for the bound first, then assert the CR and CRC it
was asked for. The case pins the same thing it always meant to - that the
hold is timed at CR 4/8 with CRC rather than at our own CR.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CStem6K6MQRBNR8YCFgtCE
* LoRa: scale the preamble hold's airtime fallback to CR 4/8
When calculateTimeOnAir() cannot time the worst-case frame, maxRxFrameMsec()
falls back to getPacketTime(), which times it at our own coding rate. A
configured rate below 4/8 then produced a hold shorter than the frame the hold
exists to cover. Scale that figure up to 4/8; the preamble does not scale with
CR, so the result runs long, which is the safe direction for a hold.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013mdEBL5t2xdysijGG6ejQR
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* ThinkNode M8: switch the GPS to the AG3352Q
Later M8 units replace the ATGM336H with an Airoha AG3352Q. Drop the
ATGM336H standby pin and wire up idle_int (P1.07) so the existing
GPS_SLEEP_INT path stops the engine with $PAIR003 before GPS_EN cuts
the 0.8 V core, as on the ThinkNode M9. Default baud is now 115200.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ThinkNode M8: put the AG3352 into backup mode for sleep
$PAIR003 only stops the GNSS engine, so with the 1.8 V rail always on
the receiver never reached low power (1.5 mA measured by Elecrow).
Add GPS_AIROHA_RESET_WAKE for boards without RTC_INT that have RESETB:
sleep sends $PAIR650,0 (with idle_int low) before EN goes low, and wake
pulses RESETB once EN is back up. Enable it on the M8.
At shutdown, hold idle_int high and RESETB released instead of letting
the variant's pin sweep float them through System OFF.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Add a build option to ignore the USB console's light-sleep veto
#12141 vetoes light sleep while a USB host has the hardware USB-serial/JTAG
console open, because no shipping chip keeps the USJ pad alive across a sleep
and the host gives up before we surface again. That is right for a user, and
it also means light sleep can no longer be watched in a debug log over USB,
which is how the path is normally developed. mverch67 asked for a way to turn
it off on #12141.
MESHTASTIC_LIGHT_SLEEP_IGNORE_USB_CONSOLE does that. It gates the veto rather
than the detector, so usbConsoleHostAttached() still answers honestly and the
only thing that changes is whether lsIdle() lets the sleep through - and, with
it, the "Skip light sleep: USB console host attached" line, which would
otherwise be logged once per stay in LS by a build that is deliberately
sleeping anyway.
Nothing defines it. A build that does accepts losing the console to the first
sleep until the board is power cycled.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTiifXrVc4YdonmLRCeWhw
* Make the light-sleep USB opt-out a single defaulted define
The helper added a function to say what the condition can say itself. Define
MESHTASTIC_LIGHT_SLEEP_IGNORE_USB_CONSOLE to 0 by default and test it inline,
so there is one name to grep for and a variant can override it with a -D.
Behaviour is unchanged: with the define at its default the condition reduces
to the same !usbConsoleHostAttached(), and the detector is still untouched, so
usbConsoleHostAttached() keeps answering honestly for any other caller.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTiifXrVc4YdonmLRCeWhw
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
doLightSleep() skips a light sleep when the LoRa IRQ line is already
asserted, and bounds that to LORA_IRQ_SKIP_SLEEP_MAX consecutive skips so a
line stuck high cannot stop the node sleeping for good. But the counter was
reset on the sleep it finally allowed, so the bound never stuck: a genuinely
stuck-high line produced five skips, one sleep, five skips, one sleep, for as
long as it stayed high.
Each of those skipped attempts is not free. It runs waitEnterSleep() and the
full notifyLightSleep / notifyLightSleepEnd observer pass, which detaches,
arms and restores every wake pin - the path #12100 and #12131's interrupt
storm live on - so the loop costs far more than the sleep it is protecting.
Reset the counter only when the line is actually read low, which is the
condition that says the radio is clearing its IRQs again. Nothing changes for
the transient case the skip exists for: the line goes low as soon as the main
loop services the packet, and the next sleep clears the count.
Claude-Session: https://claude.ai/code/session_01GTiifXrVc4YdonmLRCeWhw
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
enableLoraInterrupt() had two branches for the same job. The first did the
DIO1 pull-down, the LORA_RESET and LORA_CS pull-ups and the HAS_LORA_FEM
sleep-RX setup before arming the wake pin; the second armed the pin and did
none of that. The condition choosing between them was
SOC_PM_SUPPORT_EXT_WAKEUP, an IDF backwards-compatibility alias defined only
on ESP32, S2 and S3 - the same alias main-esp32.cpp:427 already warns about
for the ext1 deep-sleep path - so C3, C6, H2 and P4 compiled the second one.
On those chips DIO1 therefore entered light sleep with no pull-down, which
on a line that is not externally pulled reads high and wakes the node from
every sleep; LORA_CS and LORA_RESET entered unpulled; and a LoRa FEM was not
put into its sleep RX mode. Nothing here uses ext wakeup: it is a GPIO
light-sleep wake source, and gpio_wakeup_enable() and the pin pulls are
available on every ESP32 variant. So the first branch now compiles
everywhere and the second is gone.
The restore half of the same path was already unconditional - its
#if SOC_PM_SUPPORT_EXT_WAKEUP / #else carried two identical branches,
vestigial from #12131 - so that is collapsed too.
Claude-Session: https://claude.ai/code/session_01GTiifXrVc4YdonmLRCeWhw
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* Radio: end a CAD handoff whose RX hits a damaged header
When CAD hands off to RX and that RX ends on a damaged header (CRC_ERR or
HEADER_ERR with no RX_DONE), the RX readout task cleared those flags and
returned. Nothing was notified and cadHandoffRxStart stayed set, so the
radio thread never re-armed. Where the chip had stopped listening, the node
stayed deaf until the main loop's backstop logged "CAD>RX timeout" and
re-armed: 200 ms to over a second later. Before the readout task, the same
edge went to handleReceiveInterrupt(), which read the error, cleared the
flags and re-armed.
The bench saw this on an LR2021 (X1) running develop: 5 "CAD>RX timeout"
lines across 3 runs, where pre-#11983 develop logged none in 4 runs.
Now the task hands every frameless end of a handoff's RX (TIMEOUT, CRC_ERR,
HEADER_ERR) to the radio thread, which clears the flags through
handleReceiveInterrupt() and re-arms, as it already did for TIMEOUT.
Co-Authored-By: Jonathan Bennett <jbennett@incomsystems.biz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REkPVFh6kvG4AZJ5A8AtM9
* Radio: don't strand a damaged header's flags when its notification is dropped
Three follow-ups to the handoff end, from review of the first commit.
The readout task's notify(ISR_RX, !rxArmedBeforeTxDone) deliberately does not
overwrite a pending TX notification, so the RX notification can be dropped.
That is pre-existing for the TIMEOUT case, and the ISR_TX path does put the
chip back into RX, but the first commit moved the clearing of CRC_ERR /
HEADER_ERR to the radio thread, so in the dropped case nothing clears them
until checkCadHandoffTimeout(). notify() returns false exactly when it
dropped, so clear them at that site in that case, as the code did before, and
leave the open handoff to the backstop.
handleReceiveInterrupt() logged "CAD>RX pkt" for a frameless end, and read a
length that is not a frame's. A damaged header now takes the same exit as an
empty window, with its own log line, so a capture says which of the two
happened and getPacketLength() is not asked about a packet that never
arrived. rxBad is unchanged either way: neither case counted before.
An empty window inside a handoff can carry a damaged header's flags alongside
the timeout, both routed to DIO1, so that exit clears them together. Plain RX
is untouched there and still leaves HEADER_ERR to checkStaleRxFlags().
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTiifXrVc4YdonmLRCeWhw
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Button E was PIN_BUTTON1 with a hand-rolled FunctionButton map in
InputBroker (single press: ping, double: privacy toggle, 5 s hold:
shutdown). The rotary encoder already drives the UI, so define the pin as
CANCEL_BUTTON_PIN instead and drop the M8 special case: a press now
cancels and a 4 s hold shuts down, through the generic cancel button.
variant_shutdown() follows the rename so the wake-on-press pin is still
parked and sensed.
Claude-Session: https://claude.ai/code/session_01B7bFRZ9bBMRr7KPBF8rmzu
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The config-send path copies config.power and then overwrites one field with
the default, unconditionally:
fromRadioScratch.config.payload_variant.power = config.power;
fromRadioScratch.config.payload_variant.power.ls_secs = default_ls_secs;
The note at the end of that state explains why the effective value is sent:
0 means "use the default" and the app needs to know what the device will
actually do. But the write is not conditional on the stored value being 0, so
a configured value is replaced on the way out. Every client, the CLI included,
reads back default_ls_secs (IF_ROUTER(ONE_DAY, 5 * 60)) no matter what is
stored, and a set that took effect looks like a set that was refused.
It also loses the setting. AdminModule's power case stores what it is sent
(src/modules/AdminModule.cpp:967), so a client that reads this config and
writes it back - which is what happens when a user changes any other field in
the same message - stores the default over the configured value.
Observed on an ESP32 node: ls_secs set to 60, read back as 300, and the node
then light slept on the 60 s cadence, so the stored value had taken all along.
Default::getConfiguredOrDefault() keeps the behaviour the note describes, since
0 is exactly the case it falls through on, and changes only the readback of a
non-zero stored value. Note that config.power.ls_secs is initialised to
default_ls_secs in NodeDB::installDefaultConfig() (src/mesh/NodeDB.cpp:1366)
and consumed raw in PowerFSM's lsIdle() (src/PowerFSM.cpp:133), so a stored 0
is not how a default-configured node looks in the first place.
Claude-Session: https://claude.ai/code/session_01GTiifXrVc4YdonmLRCeWhw
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* Add Makerfabs Nomad Terminal variant
* Configure external power detect pins before probing battery gauges
* TFTDisplay: add ILI9488 reset pin and colour inversion options
* Makerfabs Nomad Terminal: add BaseUI env, early power latch, soft-start LCD/GPS rail, GPS wake
* I2C scan: tell FocalTech touch from AHT10 at 0x38 and BMM150 from VEML7700 at 0x10
* Run a BMM150 next to an IMU on the magnetometer thread
* Makerfabs Nomad Terminal: BaseUI touch
* BMM150: add hard/soft-iron calibration, read at 10 Hz
* Makerfabs Nomad Terminal: fix MUI build, restart Wire after MUI touch init
* BMM150: seed calibration from the first valid sample
* Makerfabs Nomad Terminal: combined BaseUI+MUI -tft firmware, SD defines in build flags
* Makerfabs Nomad Terminal: reset touch controller at init, wake touch thread on INT
* BMM150: poll every 200 ms
* Makerfabs Nomad Terminal: scale CW2015 SOC to the on-USB full reading
* BMM150: end an expired calibration when the sample is rejected
* fix WakeKey not responding when pressed during boot
* fix I2C error address 0x48 during scan
* enable GT911 gesture recognition in MUI
* shutdown_variant()
* disable io expander INT during light sleep
* fix BaseUI wakeup
* prevent bootloop in case DIO1 fires
* fix ADS1X15 scan type
* fix power down sequence
* external keyboard support
* fix I2C port 1 define; skip false positive
doLightSleep() arms PMU_IRQ with GPIO_INTR_LOW_LEVEL and, unlike every other
wake pin it arms, never disarms it. The low-level interrupt type and the
wake-enable bit are therefore still set in the GPIO registers once the node
is awake, and they stay set for as long as the chip keeps its GPIO
peripheral.
A panic or software reset does keep it: only a chip-level reset clears those
registers. Measured on a T-Beam S3 Supreme (ESP32-S3), reading
GPIO_PIN40_REG immediately before the first attachInterrupt() of each boot:
0x00000000 after a flash/chip reset, then 0x00002600 (int_type 4 low level,
wakeup_enable 1, int_ena set) on each of three successive CPU-only resets.
With this change the same recipe reads 0x00002100 (int_type 2 negedge,
wakeup_enable 0) on all three.
That leftover level arming is the precondition for the interrupt storm
#12131 fixed for DIO1: the first attachInterrupt() of a boot installs the
GPIO ISR service inside a critical section, and a level condition that is
already true re-enters gpio_intr_service with nothing able to clear it,
which ends in an interrupt WDT panic that only an EN reset recovers from.
On the boards that define PMU_IRQ that first attach is
attachPowerInterrupts(), which is where the panic in #12114 landed.
The storm itself was not reproduced here: PMU_IRQ is open-drain with an
external pull-up, and the pad read 0xff both as found and with no internal
pull on every boot, so the low-level condition was never true across the
attach. A VBUS insert or remove asserting the line in the window between
axpChipInit() clearing the PMU status and attachPowerInterrupts() would make
it true. This closes that window instead of relying on it staying narrow.
gpio_wakeup_disable() on its own leaves int_type behind - the #12131 lesson
- so the pin is also set back to an edge type, matching what the BUTTON_PIN
path above it does.
Claude-Session: https://claude.ai/code/session_01GTiifXrVc4YdonmLRCeWhw
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The 5 h dedup window and the 6 h stationary broadcast floor were paired by
a hand-picked one-hour margin. Round the window to 6 h and derive the floor
from it: one dedup tick (6 min) above the window, which is the exact margin
a stationary node's refresh needs to clear its neighbours' tick-quantised
window regardless of tick phase. A static_assert enforces the pairing.
Role caps (tracker 1 h, lost-and-found 15 min) are unchanged. Saved configs
keep their value, as with the 11 h -> 5 h change in #11606; the new default
applies when traffic management config is first installed.
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Tom <116762865+NomDeTom@users.noreply.github.com>
GCC 12.3.1's newlib no longer exposes the BSD `uint` typedef on nRF52,
breaking CannedMessageModule::keyForCoordinates on the only nRF52 variant
with USE_VIRTUAL_KEYBOARD. Use int, matching the Letter rect fields it
compares against.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
These three variants were referencing the older (broken) library, where other RAKs had already updated to this fixed version. This PR aligns everything on the fixed version.
* Add native Portduino mDNS discovery via Avahi and Bonjour
* Guard native discovery in Portduino WASM builds
* Add linux dependencies in packaging, support "buildroot"
Adds libavahi dependencies for Debian, RPM, Docker and CI.
Moves the libavahi definition to the shared `build_flags` (which is only consumed on Linux targets)
* Fix platformio formatting
* fix(portduino): disable Avahi when daemon unavailable
* fix(portduino): retry native discovery and defer worker logs
* fix(portduino): preserve pending discovery warnings
---------
Co-authored-by: Jake-B <jake-b@users.noreply.github.com>
Co-authored-by: vidplace7 <vidplace7@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Before: an ESP32 with native USB, power saving on and a cable attached
drops off the USB bus within a couple of minutes and does not come back.
The host logs `device descriptor read/64, error -71`, `device not
accepting address`, `unable to enumerate`, and from then on the port is
gone - pressing the button wakes the screen, so the firmware is alive and
responding, but nothing re-triggers enumeration from the host side. Only
removing power restores it. A bench board was measured in that state for
over ten hours.
After: a node whose USB-serial/JTAG console is open by a host does not
light sleep. A node on a battery or a charger, with no host, sleeps
exactly as before.
No shipping chip keeps the USJ pad alive across a light sleep:
`SOC_USB_SERIAL_JTAG_SUPPORT_LIGHT_SLEEP` is undefined for the S3, so IDF
disables the pad for the duration of every one, and the capability is
commented out even on C6/H2. Meshtastic light sleeps in 30 s chunks with a
100 ms awake gap between them, so the device presents itself for ~100 ms
every 30 s, which is far below what enumeration needs. After a few
attempts the host gives up for good.
How: `lsIdle()` is the only entry to light sleep, so the veto goes there,
alongside the existing `doPreflightSleep()` one. The predicate is IDF's
`usb_serial_jtag_is_connected()`, which answers "a host has the port
open" rather than "a cable is in", compiled in only where the console is
really the USJ peripheral - the same `ARDUINO_USB_CDC_ON_BOOT` and
`ARDUINO_USB_MODE` pair SerialConsole.cpp uses, so a TinyUSB build is
untouched and every other target gets a `false`. It is frame-based, so a
host counts as attached for 5 s after the last frame seen; a single
missed frame must not be enough to cost the port. Not
`HWCDC::isPlugged()`, which flaps false while USB is fine (#11864).
The node stays in LS with the CPU awake for as long as the host is there,
rather than advancing to NB, which is the same shape as the preflight
veto: the state is unchanged, only the sleep is skipped.
Claude-Session: https://claude.ai/code/session_01GTiifXrVc4YdonmLRCeWhw
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* Add native Portduino mDNS discovery via Avahi and Bonjour
* Guard native discovery in Portduino WASM builds
* Add linux dependencies in packaging, support "buildroot"
Adds libavahi dependencies for Debian, RPM, Docker and CI.
Moves the libavahi definition to the shared `build_flags` (which is only consumed on Linux targets)
* Fix platformio formatting
* fix(portduino): disable Avahi when daemon unavailable
---------
Co-authored-by: Jake-B <jake-b@users.noreply.github.com>
Co-authored-by: vidplace7 <vidplace7@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Before: the radio-sequence mutex was created alongside the RX readout task,
inside startReceive(). lockRadioSequence() returns false on a null mutex, so the
first RadioSequence to wrap that call took nothing and recorded held == false,
while the task it had just created was already live and free to take the unheld
mutex. On SX126x the exposed window is the tail of SX126xInterface::startReceive()
after the base call - rxArmedContinuous = true, enableInterrupt(isrRxLevel0),
checkRxDoneIrqFlag() - so the first frame of a session could be read out from
under the arm that was still finishing. It is one window per boot, but the guard
documented the opposite invariant, which is what made it hard to see.
After: the mutex exists before any sequence can start, so every sequence in a
readout-task build holds it and the task cannot appear inside an unheld one.
How: xSemaphoreCreateRecursiveMutex() moves to the RadioLibInterface
constructor, under MESHTASTIC_RX_READOUT_TASK, and is freed in the destructor,
since initLoRa() destroys an interface whose init() failed. startRxReadoutTask()
now only checks that it exists. The lockRadioSequence() comment is corrected to
match.
Claude-Session: https://claude.ai/code/session_01TCyfeQuP9f3wggY9eF4GZT
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
`dput` PPA upload operations began showing their status properly after #11941, revealing that many uploads are reaching ~85% and then timing out.
This change increases the timeout for dput publish steps to prevent these failures.
Before: four of the five radio drivers handle any pending notification before
forcing standby, deferring only a TX whose backoff timer has not expired -
SX126x, SX128x, LR11x0 and LR20x0 all open trySetStandby() with
checkNotificationExcept(TRANSMIT_DELAY_COMPLETED). RF95 was never given it, so
on an SX127x board a notification already pending is serviced after
trySetStandby() has stood the chip down and detached the ISR, against a chip
that has left the state the notification describes.
After: RF95 drains like the other four, and a TX still waiting on its backoff
timer is left alone exactly as it is elsewhere.
How: checkNotificationExcept(TRANSMIT_DELAY_COMPLETED) as the first statement of
RF95Interface::trySetStandby(), with the same comment the other drivers carry.
Claude-Session: https://claude.ai/code/session_01TCyfeQuP9f3wggY9eF4GZT
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Upstream dependencies in the newest distros are beginning to require c++20.
This PR bumps all portduino-based targets to c++20, removes the shared c++17 build_flag, and adds it to each variant family where it is needed.
OLED_GR selects Win1253, OLED_RU/OLED_UA Win1251, OLED_PL/OLED_CS
Win1250, no flag Win1252. heltec_mesh_pocket returns to the Win1252
default. t5s3_epaper stays on Win1253 (18pt/24pt exist only there).
Co-authored-by: Jason P <applewiz@mac.com>