mirror of
https://github.com/meshtastic/firmware.git
synced 2026-09-15 15:59:40 -04:00
Node-Bridging
12622
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6fa3df0af3 | node bridge | ||
|
|
46b4fdc799 |
The ESP32 spike is inert again unless a variant opts in, and BLE ingress resets priority
Four findings from the 2026-09-06 feature review, verified against this branch. The spike was not inert on ESP32. [ble_mesh_esp32] documents itself as opt-in and the commit that introduced it is titled "Factor the ESP32 BLE mesh build settings into one opt-in block", but it was referenced with no condition from [esp32s3_base] and [esp32c3_base] - the sections every S3 and C3 variant extends. So HAS_BLE_MESH, BLE_MESH_USE_EXT_ADV and HAS_BLE_GATT_MESH were set on every S3/C3 build, against a NimBLE rebuilt with ext-adv and the observer role, and the #if in NimbleBluetooth.cpp made the legacy advertising branch dead code - the phone advertisement on a stock Heltec V3 was the spike's hand-rolled ext-adv path. The base sections no longer reference it; heltec-v3_blemesh and heltec-ht62-esp32c3-sx1262_blemesh do, the way rak4631_blemesh already did on nRF52. Checked with `pio project config` across all 306 envs: no env carries the flags now without opting in, and the two opt-in envs carry all of them. clearCorruptBondStoreOnce() erased the NimBLE bond database once per device on the first boot of any ESP32 build from this branch, unpairing every phone with no notice. Its own comment says why it exists - NimBLE failing to come up "after flipping between builds with different BLE configs" - which is this feature's hazard and nobody else's. Gated on HAS_BLE_MESH. The NO_PIN branch changed setAuthenticationMode(true, false, false) to (false, false, false), so no ESP32 build from this branch offered to bond in NO_PIN. The reason is the unauthenticated mesh-peer characteristic, so it is gated on HAS_BLE_GATT_MESH and a build without it keeps develop's behaviour. Both BLE ingress paths now reset priority. via_mqtt and tx_after were already cleared as local-only, but priority was left as sent - and unlike want_ack, next_hop and relay_node it is not a field the 16-byte LoRa header carries, so fixPriority() always derives it locally for a LoRa arrival and this bearer is the first that lets a sender choose it. Left alone, a crafted frame with priority MAX outranks ACK, the ceiling fixPriority assigns, and once perhapsRebroadcast copies it into the TX queue replaceLowerPriorityPacket evicts one of ours for it. No PSK needed: the guards run before decryption. Native suites green in Docker: test_ble_mesh 13, test_ble_gatt_mesh 26, test_transport_registry 7. No ESP32 or nRF52 build was run - the ESP32 toolchain rewrites a framework sdkconfig shared with every other checkout on this machine, so that wants a deliberate sitting. |
||
|
|
ca0a39c512 |
Review fixes: the nRF52 phone is the link that uses the phone API, and a free slot is always advertised
With a mesh peer sharing the radio, connect order says nothing about which link is the phone's, but NRF52Bluetooth took the last connect as the phone: fromNum notifies went to Bluefruit's default handle (the newest link), and a mesh peer dropping - or any link dropping before it subscribed - closed the phone's session and wiped its ToRadio state. The phone is now whichever link uses the phone API (toRadio write, fromRadio read, fromNum/logRadio CCCD); notifies and logs address that handle, and only its drop ends the session. Non-mesh builds keep the old behaviour. Bluefruit stops the connectable advertisement on connect and restarts it only when no peripheral link remains, so the second slot was reachable only if that peer connected first, and a peer that dropped could not come back while the other link was up. The mesh layer re-arms the advertisement on every connect and disconnect while a slot is free. Bench: Pixel connected, BLE broadcast off so nothing else could re-arm it, iPad killed and relaunched - connected and subscribed in three seconds. Both platforms: the zero-length disconnect marker is never dropped from a full RX ring (it overwrites the newest chunk of a dead link) so reassembly state cannot leak onto a reused connection handle. nRF52: a departed peer is refused before Bluefruit's blocking notify rather than by fifty timeouts. Both BLE ingress paths clear via_mqtt and tx_after, which only the local stack may set. Native suites test_ble_mesh, test_ble_gatt_mesh and test_transport_registry pass; rak4631_blemesh and heltec-v3 build. |
||
|
|
df1ae63bf3 |
nRF52: mesh-peer GATT service over Bluefruit
The same BLEGattMeshHandler the ESP32 runs, with the Bluefruit half it was missing: an open write/write-without-response/notify characteristic on the mesh-peer UUID, received values queued from Bluefruit's callback task for the main-task pump, subscribed links as notify targets, and the negotiated MTU read live per peer. S140 has one advertising set, so the service UUID rides in the phone advertisement's scan response (instead of the TX power) and the name beside it is shortened. A link becomes a mesh peer when it subscribes to the mesh characteristic; the phone API's disconnect handler skips those links so a peer dropping never closes the phone's session. Bluefruit.begin(2, 1): the phone and one mesh peer at once, on the re-based linker script. Enabled by the BLE_GATT_PEER protocol bit, built by rak4631_blemesh. WisMesh Pocket <-> Pixel 6a monitor, both directions: the phone found the service 3 s after boot and subscribed at chunk 244; its Send test arrived as BLE GATT mesh RX and decoded; a Pocket text reached the phone as rx[gatt] a second after sending. RAM 41.1%, flash 92.0%. |
||
|
|
a4fc823495 |
nRF52: log each BLE mesh advertising burst and how its set terminated
On the S140 the mesh shares advertising set 0 with the phone API, so a frame that never reaches a phone is indistinguishable from one that was never sent. Log the burst (handle, length, dedicated/shared) and the BLE_GAP_EVT_ADV_SET_TERMINATED reason and event count. On a WisMesh Pocket every burst reads 'shared' then 'reason 2 after 3 events': the shared path radiates; the silence was the phone's scan being downgraded. |
||
|
|
361ac318df |
nRF52: define nrf52BluetoothReady so the BLE mesh build links
The readiness flag was declared extern and set in NRF52Bluetooth.cpp but never defined, so every nRF52 env with HAS_BLE_MESH failed to link. With it the rak4631 spike builds (RAM 39.8%, flash 91.4%) and a WisMesh Pocket receives BLE-adv mesh frames from a phone (BLE mesh RX, rssi -36). |
||
|
|
fcc3c05826 |
Prefer 1M PHY on the ESP32-S3/C3 controller so an Apple central cannot crash it
An A16 iPad (iPadOS 26) connecting to any ESP32-S3 node asserts the binary BLE controller in its remote PHY-update handler (BLE assert lld_con.c 3397, esp-idf#15311) about 200 ms after the link forms, before any GATT traffic, and the node bootloops for as long as the iPad keeps reconnecting. Stock develop and the nightly reproduce it with the stock iOS app. Apple's guidance for accessories is to indicate 1M-only PHY preferences; iOS negotiates 2M at the controller level and apps cannot change it. The NimBLE wrapper for that is compiled out of the prebuilt host, so issue HCI LE Set Default PHY (1M/1M) directly once the host has synced. Bench, Heltec V3: 0 of ~170 iPad connects survived before; 9 of 9 survive with this, subscribe, and carry frames. A Pixel requesting 2M was never affected either way, and still negotiates 2M itself. |
||
|
|
c7fa0e26b8 |
Revert "Converge the mesh-peer advertisement on the slot count, and stop speaking for links we never held"
This reverts commit
|
||
|
|
7153c787ab |
Converge the mesh-peer advertisement on the slot count, and stop speaking for links we never held
Review of the previous commit found two problems with the re-arm-on-every-drop change, both about the slot gate being a check-then-act across two tasks. startAdvertising() reads the link count under lock and then makes its ble_gap calls outside it - it has to, because the host task takes the same lock in addLink and is also the task that delivers the HCI completion those calls block on, so holding it across them would deadlock. A CONNECT landing in that window takes the slot after the count said it was free, and the set goes on the air with no room for the peer it attracts: a second mesh central then connects, both host connection slots are mesh peers, and the phone API is locked out until one drops. Now the slot-full branch stops a set it finds running, and the CONNECT-success path asks for a re-arm so that branch is reached promptly. Whichever task wins the race, the state converges on "slot held, set off" instead of staying wrong until the next disconnect. onDisconnect() logged "conn N disconnected (via ... advertisement)" and latched pendingAdvertising for any handle at all, including links this transport never held. With BLE_GATT_PEER off that meant every ordinary phone drop printed a mesh-transport line about itself and armed an advertisement for a service that does not exist. It now returns early for a handle the table does not know. This costs the deferred-start (rc=ENOMEM) retry nothing: with the feature on, onConnect registers every server link whichever advertising set it arrived on, so a drop that could free a connection slot is always a link the table knows. Built for heltec-v3 (pio run, 93 s). Still not flashed - the bench verification in the previous commit's message covers this one too. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
9f5436300c |
Keep the mesh-peer advertisement alive: a CCCD write is not "arrived on instance 2"
On the bench the Pixel's GATT rx sat at zero from launch and across every node rebuild while ble-adv, lora and udp all flowed, and its GATT tx was "accepted by a peer". The peer was the iPad: logcat shows the Pixel's central connecting only to random-address Apple peers and never to a public-address NimBLE device. The V3's connectionless broadcast (instance 1) was on the air; its connectable mesh-peer advertisement (instance 2) was not, so the Pixel's filtered scan had nothing to find. The phone-API set (instance 0) had gone dark with it. Root cause is a conflation in ESP32BLEGattMesh: onSubscribe() called addLink(conn, true), flagging any link that wrote the mesh CCCD as viaMeshAdv. That flag does two jobs - it is the slot count startAdvertising() gates on (BLE_GATT_MESH_MAX_LINKS is 1) and it is the early return in NimbleBluetoothServerCallback::onDisconnect that skips the phone-API session reset and re-arm. Both sets advertise the same public address, so a central that finds instance 2 in a scan can land its CONNECT_IND on instance 0's ADV_IND (the earlier "onSubscribe never fired for a central that connected via the phone-API instance" commit saw exactly that). Such a link, once subscribed, counted against the mesh slot; when a second central (the iPad) took instance 2 the slot was full, and when the first link dropped on a monitor rebuild the early return left instance 0 unarmed too - "peer slots full, not advertising" at LOG_DEBUG was the only trace. Nothing recovered until reboot. - onSubscribe() now calls addLink(conn, false): a CCCD write sets `subscribed` (still the only thing that makes a link a notify target) and leaves `viaMeshAdv` exactly as the instance-2 CONNECT event set it, which is the one meaning it can safely carry. - onDisconnect() re-arms the mesh-peer advertisement for every dropped link and lets startAdvertising() decide, since a phone-API link dropping is what frees the connection slot a deferred start was waiting for. startAdvertising() skips a set that ble_gap_ext_adv_active() says is already running, so the re-arm never interrupts a live advertisement. - The slot-full line is LOG_INFO and names the conn handle holding the slot; an ENOMEM start (every connection slot taken) is LOG_WARN "deferred", and every ext-adv rc carries a hint: 6 = no free connection, 519 = controller activity budget (CONFIG_BT_CTRL_BLE_MAX_ACT). - Subscribe and disconnect lines say which advertisement the link arrived on. BLE_GATT_MESH_MAX_LINKS stays 1: two dual-role phones still race for the single mesh slot by design, and the header now says so and what to do about it (run the second phone PERIPHERAL_ONLY, or raise the slot with MAX_CONNECTIONS/MAX_ACT). The esp32-common.ini note records that the duplicated CONFIG_BT_NIMBLE_MAX_ CONNECTIONS / MAX_ACT keys resolve to [ble_mesh_esp32]'s values - verified in the generated sdkconfig.defaults - and that comment lines are stripped before the custom_sdkconfig hash, so the note itself does not trigger a libs rebuild. Built for heltec-v3 (pio run, 90 s, no IDF-lib recompile). Not yet flashed or verified on the bench. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
ea24b26d52 |
Notify mesh peers per-connection and drop the spike diagnostics
Egress now sends each fragment to a specific subscribed connection with ble_gatts_notify_custom(peer, valueHandle, ...) instead of broadcasting to every subscriber, so the pump's exclude-the-arrival-peer step actually takes effect (a relayed packet is no longer echoed to the peer it came from). The value handle resolves correctly now that setupService() forces the server's GATT start. A link becomes a notify target only when onSubscribe marks it subscribed - the CCCD write is the "this is a mesh peer" signal - so onConnect just tracks the link and a plain phone-API client is never sent mesh frames. Removed the verbose diagnostic logging added while chasing this down and restored the enc-change warning to its plain form. Verified on hardware: the Android client's rx counter advances with the v3's own frames and relayed LoRa traffic (a decoded position from another node, plus opaque channel-50 frames) over BLE GATT. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
668645fdee |
Make the mesh-peer egress actually deliver: resolve the handle and register peers server-side
The transport connected and subscribed but never delivered a frame to the phone. Four things were wrong, found on the bench and fixed here: - Peers were only registered from onSubscribe/onWrite, which never fired for a central that connected through the phone-API advertising instance. Register every inbound link from the server's own onConnect callback (ESP32BLEGattMesh:: onConnect, hooked in NimbleBluetoothServerCallback::onConnect), which fires regardless of advertising instance, and also handle the raw BLE_GAP_EVENT_SUBSCRIBE in our GAP callback. - The characteristic's value handle never resolved (getHandle() == 0xFFFF): the wrapper only resolves handles inside BLEServer::start(), which it triggers from BLEAdvertising::start(). This firmware advertises through the raw ble_gap_ext_adv API and never calls the wrapper's advertising, so force server->start() after registering the service. - Notifications went through the raw NimBLE ble_gatts_notify_custom() with an esp_gatts handle, which returns success but delivers nothing. Send through the wrapper's own BLECharacteristic::notify() - the exact path the phone-API's fromNum uses. Proven on hardware: an Android client (node-kmp monitor) subscribed to the mesh-peer service receives frames the v3 originates and relays - rx counter advanced with a frame from the v3's own node number over BLE GATT. Still spike-quality: verbose diagnostic logs remain (revert), notify() broadcasts to all subscribers rather than excluding the arrival peer (dedup covers it for now), and the C3/nRF52 bring-up is unbuilt. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
55ee523b27 |
spike: add egress diagnostics (onSend/pumpTx/onWrite/onSubscribe) — REVERT later
Temporary LOG_INFO probes used to isolate the mesh-peer egress failure on the bench. They proved: onSend fires (registry OK) and pumpTx runs, but always with peers=0/0, and onSubscribe/onWrite never fire for a central that connected via the phone-API advertising instance. This is what's flashed on the bench v3 (2.8.0.6c1c7fe). Revert before the branch is cleaned up for review. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
6c1c7feba9 |
Stop the node initiating BLE bonding in NO_PIN mode (mesh-peer pairing wall)
An Android central connecting to the unauthenticated mesh-peer GATT service was having its link torn down before it could subscribe. Root cause found on the bench: even in NO_PIN mode the firmware called setAuthenticationMode(bonding=true, ...), so the node advertised the SMP bonding bit and a central "just works"-paired on connect; that pairing failed and the ACL dropped (the phone looped connect -> discover -> drop, never subscribing). Nothing in NO_PIN needs an encrypted link, so offer no bonding at all: setAuthenticationMode(false,false,false). Verified on a heltec-v3: after this change the recurring "BLE encryption change without encrypted link; ignoring" is gone (0 occurrences) and no pairing is attempted. Also logs the enc_change sec_state (bonded/ authenticated/key_size/role) on a failed encryption, which was previously discarded, to tell a failed pairing apart from a stale-bond reconnect. PIN modes (RANDOM_PIN/FIXED_PIN) are unchanged, so the secured phone API keeps its passkey pairing. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
3022a37768 |
Add the BLE-GATT mesh-peer transport: a phone connects to the node as a mesh peer
The SIG-Mesh "GATT proxy" role: the node runs a GATT server on a private service, phones connect to it as centrals and exchange whole mesh frames over a write/notify characteristic, without a phone-API session. Point-to-point where LoRa and the advertisement transport are one-to-many; a broadcast is N notifies. Two halves: - BLEGattMeshHandler (platform-neutral, built into the native suite): fragment framing shared byte-for-byte with the node-kmp client, bounded reassembly (per-peer and per-in-flight caps, expiry), the ingress guards the UDP and advertisement transports already apply (validate-before-relay, drop senderless / self-claimed / impossible-hop packets, strip local-only metadata), a per-peer TX ring, and no-echo-back-to-the-arrival-peer via a small (from,id)->peer table. - ESP32BLEGattMesh (NimBLE): its own connectable advertising set on instance 2 carrying the service UUID, per-connection notifies, an MTU-derived chunk size, and a GAP handler chained ahead of the Arduino wrapper's so the server's connection/MTU/subscription bookkeeping covers these links too. A mesh-peer disconnect is gated out of the PhoneAPI session teardown. Registry-gated on the new BLE_GATT_PEER protocol flag, so it carries outbound packets only while a phone is being served as a peer. The sdkconfig bump that the second connection needs (CONFIG_BT_NIMBLE_MAX_CONNECTIONS=2, CONFIG_BT_CTRL_BLE_MAX_ACT=6) lands here with the service rather than ahead of it. Protobufs pointer bumped for the TRANSPORT_BLE_GATT and BLE_GATT_PEER enums; generated headers regenerated. Proven: native suite 1418/1418 (26 new cases for this transport); heltec-v3 (ESP32-S3) builds and, with WiFi off, brings the service up and advertises on instance 2 with no OOM / NimBLE 519 / crash at the 2-connection config. Not yet proven: a phone connecting as a mesh peer and a frame crossing device to phone (bench-gated); ESP32-C3 and nRF52 are unbuilt. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
d8ea498012 |
Move the MQTT egress tap onto the transport registry
Add a pre-encode fan-out point to MeshTransportBase so a transport can act on the decoded packet (with its now-encrypted copy and the channel index) before the decoded copy is released, alongside the existing post-encode point that UDP and BLE use. A transport opts into exactly one point via its constructor, and the two point at separate registries so they never cross. MQTT registers at the pre-encode point through a thin MQTTTransport adapter, so Router::send() no longer names MQTT directly. Its moduleConfig.mqtt.enabled and isFromUs gate stays at the call site (same point, inside the decoded-tag block) and every MQTT-side gate - via_mqtt loop-prevention, per-channel uplink, DontMqttMeBro, range-test suppression, PKI-vs-channel encryption choice - stays inside MQTT::onSend() verbatim. The old `&& mqtt` null check moves into the adapter's hook. Because the adapter is invisible to the post-encode fan-out, MQTT still fires only for our own originations and never for the relayed / already-encrypted broadcast traffic. LoRa iface->send() and the post-encode UDP/BLE path are untouched. No behaviour change. test_transport_registry gains three cases pinning the new invariant: pre-encode transports receive the encrypted packet, decoded copy and channel index in registration order; the pre- and post-encode points are disjoint; and an empty pre-encode registry is a safe no-op. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KZfYRoJFcYiqUxWQKTLUQR |
||
|
|
0091277731 |
Route the non-LoRa egress taps through a transport registry
Router::send fanned outgoing packets to UDP multicast and BLE mesh through two hardcoded, guard-wrapped taps sitting just after encryption. Replace them with MeshTransportBase, a thin registry modeled on MeshModule: each transport self-registers in its constructor, and callTransports() hands the encrypted packet to every enabled transport in registration order. A new broadcast transport now plugs in without editing the funnel. Behavior is unchanged. UdpMulticastHandler and BLEMeshHandler already exposed bool onSend(const meshtastic_MeshPacket*), so they become overrides with no body edits; the per-tap enabled_protocols check moves verbatim into each transport's isEnabled(), and the old udpHandler/bleMeshHandler null checks are subsumed by "an unconstructed transport never registers". Registration order (UDP then BLE) matches the former tap order. Unlike MeshModule there is no CONTINUE/STOP contract: these are parallel media, so one transport accepting a packet never suppresses another - callTransports ignores the return. MQTT stays a hardcoded tap: it fires earlier, inside the decoded-tag block before p_decoded is released, and needs the decoded copy plus the channel index - a pre-encryption hook point this registry does not yet model. LoRa's iface->send is untouched and remains the mandatory path. test_transport_registry pins the invariant the rewrite could break: every enabled transport is called in registration order, a true return does not suppress a later transport, disabled transports are skipped, and an empty registry is a safe no-op. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KZfYRoJFcYiqUxWQKTLUQR |
||
|
|
4aa73bf4ea |
Regenerate the protobuf headers properly, replacing the hand-edits
TRANSPORT_BLE_ADV and BLE_BROADCAST were hand-written into the generated nanopb headers because nanopb was not available here. They are now produced by the generator, so a future regen extends them rather than silently reverting them. Toolchain: nixpkgs supplies protoc and PyPI the generator, pinned to 0.4.9.1 - the version the checked-in headers name, not the 0.4.9 bin/regen-protos.sh's message says. Two shims under nanopb-0.4.9/generator-bin/ pair them so that script runs unmodified; the directory is already gitignored. Validated before trusting it: a regen against the unmodified submodule changed only the two files carrying the hand-edits and left every other generated file byte-identical. config.pb.h then came back from the real regen identical to what had been written by hand, which is a decent check on both. Two things surfaced that are worth knowing. The submodule pointer is behind the protobufs the checked-in headers were generated from - regenerating at the pinned commit *removes* doc comments that are in the tree - so this bumps it to the commit carrying the enum change. And the remaining churn in mesh.pb.h is hyphens becoming em-dashes in an unrelated comment block, which is drift the submodule bump brings in rather than anything this change does. The bump points at a protobufs commit that exists only locally (spike/ble-mesh-transport). That has to land upstream before this branch is pushable by anyone else. heltec-v3 builds; test_ble_mesh 13/13. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
4c5863c09a |
Factor the ESP32 BLE mesh build settings into one opt-in block
The five sdkconfig options and two build flags were copied between esp32s3 and
esp32c3, and the c3 copy had drifted - it carried three of the five, so the
transport there could have advertised and never received, exactly the failure
the s3 took a full afternoon to diagnose. A variant now opts in by referencing
${ble_mesh_esp32.build_flags} and ${ble_mesh_esp32.custom_sdkconfig}, which
also gives the settings one place to carry the reason each is needed.
Order is load-bearing and documented in the block: esp32_common sets all three
NimBLE options to n, so the reference must come after it.
Verified after the refactor rather than assumed - heltec-v3 and
heltec-ht62-esp32c3-sx1262 both build, and a reflashed Heltec still reports
both halves coming up:
BLE advertising started (extended API, legacy ADV_IND)
BLE mesh scanning started
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
890f12ec27 |
Add native tests for the BLE mesh transport
Thirteen cases over the half of the transport that has no BLE in it: building an advertisement, the ingress guards, the TX ring and the readiness pump. Full suite 1385/1385. Testable because BLEMeshHandler has no platform BLE dependency - ESP32BLEMesh and NRF52BLEMesh do - so native now compiles it via HAS_BLE_MESH=1 on native_base. Nothing runs there: main() instantiates neither platform subclass on portduino, so bleMeshHandler stays null. Adds one seam for it. deliverToRouter called router->enqueueReceivedMessage directly, which meant the ingress guards could only be tested by standing up a live Router; it now goes through a virtual enqueueReceived() that the test overrides to observe what survives. Production always takes the default. The cases are the bugs this transport actually had, or the ones its guards exist to stop: a relayed packet must not be refused (refusing it capped the mesh at one hop), onSend must queue rather than transmit (advertising inline stalled the router for the length of every burst), a spoofed from=0 and an out-of-range hop count must be dropped, claimed PKI authentication must be stripped rather than believed off the wire, and our own advertisement heard by our own scanner must not loop back in. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
d4f9a6a202 |
Make ESP32 BLE mesh actually work: three sdkconfig fixes
Node-to-node confirmed on hardware. A Heltec V3 and a RAK4631 exchange mesh frames over BLE 5 extended advertisements: a packet originated by the Heltec (0xd1d90f21) was seen advertised by the RAK carrying transport=9 (TRANSPORT_BLE_ADV, which deliverToRouter stamps only on BLE receive), and the RAK's region is UNSET so its LoRa RX is disabled - BLE is the only path it could have arrived by. Two further frames from other nodes relayed the same way. Getting there needed three settings, each of which failed differently and none of which is about this transport's own code: 1. CONFIG_BT_NIMBLE_ROLE_OBSERVER=y. Scanning is a NimBLE role and the stock Arduino build compiles it out - ROLE_BROADCASTER=y, OBSERVER not set - so ble_gap_ext_disc returned BLE_HS_ENOTSUP (8). The transport could always advertise and could never receive. Same class of bug as nRF52 needing Bluefruit.begin(1, 1). 2. CONFIG_BT_NIMBLE_EXT_ADV_MAX_SIZE=257. The default 1650 is the chained ceiling, reserved per instance, for a transport capped at one 251-byte PDU. 3. CONFIG_BT_CTRL_BLE_MAX_ACT=4. This was the real blocker. The controller counts advertising sets, scans and connections as "activities" and the default budget is 2 - exactly the stock build's one advertisement plus one connection. BLE mesh needs three: the PhoneAPI advertisement on instance 0, the mesh advertisement on instance 1, and a scan. Both the scan enable and the second ext_adv_configure returned HCI 0x07, Memory Capacity Exceeded, surfacing as NimBLE 519. Diagnosing any of this needed boot logs, which were unobtainable for most of this work: `pio device monitor` cannot run with stdout redirected (miniterm calls termios.tcgetattr and fails on a non-tty), and debug_log_api routes firmware logs to protobuf, silencing the UART. Reading the port with pyserial and pulsing DTR/RTS to reset the board is what finally made the boot visible. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
dacdf0dcec |
Port the PhoneAPI advertisement to ext-adv instance 0; fix the guard the merge broke
Three things, one of which explains the regression I could not account for. THE REGRESSION. The BLE mesh transport worked before Ben's platform layer was merged and stopped afterwards, and it was not device state. His code - and mine after the merge - gates the extended-advertising path on MYNEWT_VAL(BLE_EXT_ADV). That macro resolves from the *prebuilt* esp_nimble_cfg.h in the framework's include tree, which does not reflect custom_sdkconfig, so it reads 0 even when the rebuilt NimBLE library does have extended advertising. The extended path therefore compiled out and the transport silently fell back to the legacy 31-byte branch, which cannot carry a mesh frame at all. The original spike worked precisely because it called ble_gap_ext_adv_* unguarded. Now gated on BLE_MESH_USE_EXT_ADV, a flag this repo sets in build_flags and therefore controls. THE SCAN. ble_gap_ext_disc returning 8 (BLE_HS_ENOTSUP) is not an ext-adv conflict - scanning is a NimBLE *role*, and it is compiled out: framework-arduinoespressif32-libs/esp32s3/sdkconfig has ROLE_BROADCASTER=y and "# CONFIG_BT_NIMBLE_ROLE_OBSERVER is not set", and variants/esp32/esp32-common.ini :286-287,311 sets ROLE_CENTRAL=n, ROLE_OBSERVER=n and EXT_ADV=n outright. So the transport could always advertise and could never receive. This is the same class of bug as nRF52 needing Bluefruit.begin(1, 1) for a central link. Enables CONFIG_BT_NIMBLE_ROLE_OBSERVER for esp32s3. THE PORT. NimbleBluetooth::startAdvertising() now drives ext-adv instance 0 with legacy_pdu=1, so the on-air PDU stays an ordinary ADV_IND and phones without BLE 5 discover the node exactly as before. It reuses the wrapper's own GAP callback rather than replacing it - NimBLE stores the advertising instance's callback on every connection made through it, so SUBSCRIBE, MTU and pairing keep reaching BLEServer and fromNum notifications are unaffected. Reaching that private member uses the explicit-instantiation access idiom, which compiles. The forwarder re-arms instance 0 on a failed connect, where the stock handler would otherwise call the now-ENOTSUP legacy path and leave the node invisible. NOT WORKING YET. With all of the above the Heltec still emits neither a phone advertisement nor a mesh frame. The ext-adv branch is confirmed present in the image, so the remaining fault is below this layer and is not isolated. Do not read this commit as a fix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
ef1d0f571d |
Add a TX-only switch and NimBLE bond recovery; ESP32 ext-adv is the real blocker
Two isolation aids and a conclusion. BLE_MESH_TX_ONLY skips the scan. Useful in its own right for a broadcast-only node, and it is what localised the boot loop: with it set the ESP32 build is stable, without it the node resets every ~3s with "BLE mesh started" as the last line. So the fault is in ble_gap_ext_disc(), not in the pump or the readiness poll. clearCorruptBondStoreOnce() is reinstated from thebentern's branch, which I dropped during the merge as unrelated. It is not - a stored bond blob that crashes NimBLE during populate_db_from_nvs produces exactly the symptom seen here. It did not fix it, but it belongs with this work either way. The conclusion, evidenced rather than guessed: CONFIG_BT_NIMBLE_EXT_ADV=y is incompatible with the PhoneAPI's advertising as this tree does it. - ble_gap_adv_stop returns rc=8, BLE_HS_ENOTSUP: under ext-adv the legacy advertising API the Arduino BLE wrapper calls is simply not there. - On one boot the diagnostic caught nimbleBluetooth->isActive() true while a BLE scan from the host saw no connectable advertisement at all - setup() completing, advertising silently not. - On most boots isActive() is false and no advertisement of any kind appears, so setup() is not completing. - The RAK4631 sitting beside it advertises normally throughout, so this is the ESP32 stack, not the environment or the scanner. Porting the PhoneAPI advertisement onto ext-adv instance 0 is therefore a prerequisite for ESP32 BLE mesh, not a later tidy-up - which is what thebentern's "#if defined(NIMBLE_TWO) || CONFIG_BT_NIMBLE_EXT_ADV" guard change was doing for the NimBLE version his branch targeted, and which I wrongly dismissed as targeting a stack this tree no longer uses. nRF52 is unaffected: it reaches extended advertising through the SoftDevice directly and never touches the legacy API. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
93cdcad712 |
Poll for BLE readiness instead of waiting to be called; ESP32 scan still faults
Two findings from hardware, one fixed and one not. FIXED - the readiness handshake was a race. setBluetoothEnable() brings the BLE stack up, and on ESP32 that happens BEFORE main() constructs the mesh handler. The hook at the end of NimbleBluetooth::setup() therefore ran against a null bleMeshHandler about half the time, and when it lost the coin flip the handler sat in "waiting for Bluetooth ready" forever while the stack was already up. That is exactly what a working run and a dead run looked like on the same binary, which is why this took a diagnostic build to see: BTDIAG setBluetoothEnable(1) memReleased=0 cfgEnabled=1 nimble=0x3fcd4490 active=1 nimble non-null and already active on the first call this handler ever saw. runOnce() now polls platformReady() and calls onBluetoothReady() itself, once, whenever readiness actually arrives - no ordering assumption in either direction. ESP32 reports readiness as "nimbleBluetooth is active" rather than ble_hs_synced(), because the host syncs long before setup() has registered its service; nRF52 exports a flag from NRF52Bluetooth::setup() for the same reason. The push hooks are gone. NOT FIXED - with readiness now arriving reliably, the ESP32 build boot-loops: uptime cycles 1, 2, 1, 2 every ~3s, with "BLE mesh started" as the last line before each reset. So the fault is in what readiness unblocks - most likely ble_gap_ext_disc() starting an extended scan while the PhoneAPI's connectable advertisement is up. That fits the earlier ble_gap_adv_stop rc=8 (ENOTSUP) evidence: under CONFIG_BT_NIMBLE_EXT_ADV the legacy advertising the PhoneAPI uses is not merely noisy, it does not belong in the same stack as the extended path. Porting the PhoneAPI advertisement onto ext-adv instance 0 is now looking like a prerequisite rather than a tidy-up. Not isolated further: platformReady() and startScanning() have not been separated, so the crash could be either. DO NOT flash this to a node you care about. nRF52 is unaffected by the ESP32 fault and the RAK4631 remains healthy. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
ef5fc118ad |
nRF52: re-base the linker RAM origin so BLE mesh can scan
Scanning needs a central link. Bluefruit.begin(1, 1) asks the SoftDevice for one, which raises its RAM requirement past the 0x20004000 origin in nrf52840_s140_v6.ld - that value was measured for the default 1-peripheral, 0-central config. sd_ble_enable() then rejects the RAM base and the node comes up with no Bluetooth at all. Adds nrf52840_s140_v6_blemesh.ld at ORIGIN 0x20006000, the origin this tree shipped before the 0x20004000 measurement and therefore known to satisfy a strictly more generous SoftDevice config than 1-peripheral-plus-1-central. It costs 8 KB of RAM, so it is a separate script selected by a separate env rather than a change to the shared one - nRF52840 boards that do not run BLE mesh keep their RAM. env:rak4631_blemesh selects it and sets BLE_MESH_NRF52_CENTRAL. Verified on a RAK4631: the ELF's RW segment is based at 0x20006000, and the board comes up with Bluetooth working and its serial API responsive. The same build without the re-base left it silent and BLE-less, so the SoftDevice is accepting the new base with the central link enabled. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
3c5fa6d29a |
Gate the nRF52 central role behind its own flag
Bluefruit.begin(1, 1) asks the SoftDevice for a central link, which scanning needs, but it also raises the SoftDevice's RAM requirement above what the linker ORIGIN in nrf52840_s140_v*.ld reserves. sd_ble_enable() then rejects the RAM base and the node comes up with no Bluetooth at all - observed on a RAK4631, which went silent on the serial API and stopped logging entirely until reflashed without it. Gated behind BLE_MESH_NRF52_CENTRAL so the default rak4631 build stays healthy. Turning it on requires re-basing the linker script first; the existing comment on the failure path documents how (rebuild with -DCFG_DEBUG=1, read "SoftDevice's RAM requires: 0x...", raise ORIGIN). Until then nRF52 can transmit but not receive, so ESP32 is the only platform that can currently participate both ways. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
dd9a01c90a |
Merge thebentern's BLE mesh platform layer, fix six defects
Replaces the ESP32-only spike handler with Ben Meadors' architecture from
meshtastic/firmware ble-mesh-working (
|
||
|
|
4ccbe2eed8 |
Cap the advertising payload at 251 bytes, not 254
BLE_HCI_MAX_EXT_ADV_DATA_LEN is 251: the HCI LE Set Extended Advertising Data command spends four of its 255 parameter bytes on handle, operation, fragment preference and length, so an unfragmented payload cannot reach the 254 an AUX_ADV_IND could otherwise hold. The budget was three bytes optimistic, which would have let encodeAdvPayload accept frames the controller then rejects at runtime instead of dropping them at the explicit LOG_WARN. A static_assert now pins the constant to NimBLE's own so it cannot drift again. Usable ciphertext is 251 - 5 (AD wrapper) - 16 (PacketHeader) = 230 bytes, against a DATA_PAYLOAD_LEN of 237. Also corrects two comments that overclaimed. Advertising does not currently buy dupe suppression: perhapsCancelDupe is gated on TRANSPORT_LORA and Router::cancelSending reaches only iface's TX queue, never this handler's ring. It keeps suppression possible later and costs one TX instead of N regardless, which is the actual argument for it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
39999f5818 |
Let a BLE-relayed packet be re-advertised
Copying UdpMulticastHandler's egress guard verbatim capped the BLE mesh at a single hop. A packet that arrived over BLE and comes back through Router::send is a rebroadcast, not a reflection: NextHopRouter::perhapsRebroadcast takes an allocCopy of the received packet, and nothing on the TX path rewrites transport_mechanism - RadioInterface stamps TRANSPORT_LORA in deliverToReceiver, which is only reached on receive. So the copy still said TRANSPORT_BLE_ADV and the guard refused every relay, leaving two nodes able to talk and a three-node chain unable to. Log it and proceed instead. Loop protection is the same as LoRa's and needs no help here: PacketHistory drops a packet seen recently, hop_limit decrements per relay, and onScanReport already ignores frames whose sender is us. Note this leaves BLE without dupe suppression: perhapsCancelDupe is gated on TRANSPORT_LORA, and Router::cancelSending reaches only iface's TX queue, which cannot see BleMeshHandler's ring. Redundant relays are paid for until both are extended. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
1f7d371f55 |
Carry mesh frames over connectionless BLE extended advertisements
Adds BleMeshHandler, a second broadcast transport alongside LoRa, wired the way UdpMulticastHandler already is: ingress via Router::enqueueReceivedMessage, egress as a copy taken in Router::send gated on config.network.enabled_protocols. It is never the only path to the mesh - Router::send still asserts a LoRa iface, so this is an additional copy path, not a replacement. Connectionless advertising rather than GATT, on purpose. FloodingRouter cancels a pending rebroadcast when it overhears another node relaying the same packet, and that cancellation only works on a medium where every neighbour hears every transmission. GATT is point-to-point: N peers means N writes, nobody overhears anybody, nothing cancels. Advertisements restore the overhear property the flood algorithm depends on. Frames go on air in LoRa wire format (PacketHeader + ciphertext) rather than as an encoded MeshPacket the way UDP does it. UDP has a 1500-byte MTU and can afford the proto; a 254-byte advertisement cannot. It also makes a BLE-heard frame byte-identical to a LoRa-heard one, so a future BLE<->LoRa bridge is a memcpy rather than a translation. encodeAdvPayload mirrors RadioInterface::beginSending; the scan path mirrors RadioLibInterface's RX block, including the hop_start == 0 rule that invalidates next_hop/relay_node. Ingress keeps all four of UdpMulticastHandler's guards: spoofed local origin, out-of-range hop_limit/hop_start, the pki_encrypted/public_key reset, and the RSSI/SNR reset - the last differing in that BLE RSSI is a real measurement of this hop, so has_rx_rssi is set rather than cleared. Capped at one 254-byte PDU. Chaining is possible but ble_gap_ext_disc_desc.length_data is a uint8_t, so chained advertisements arrive as several INCOMPLETE reports needing per-advertiser reassembly. The cost is the top of the payload range: 254 - 5 (AD wrapper) - 16 (PacketHeader) leaves 233 bytes against a DATA_PAYLOAD_LEN of 237. Those packets still go out over LoRa. The scanner's filter_duplicates must stay 0: the controller de-duplicates on advertiser address rather than payload, so enabling it would deliver one report per neighbour and then filter away every subsequent mesh frame from that node. Spike scope and known gaps: - Company ID 0xFFFF is the SIG-reserved internal/test identifier. Shipping needs a member company ID or an assigned 16-bit service UUID. - The generated nanopb headers are hand-edited to add TRANSPORT_BLE_ADV and BLE_BROADCAST; nanopb 0.4.9 was not available here. The matching .proto change is on protobufs' spike/ble-mesh-transport branch, so a regen restores rather than reverts it - but the submodule pointer is not bumped yet. - CONFIG_BT_NIMBLE_EXT_ADV is enabled for every esp32c3 board, not a dedicated env. It links cleanly alongside the existing PhoneAPI GATT advertising, but runtime coexistence of legacy and extended advertising is unverified without hardware. - Untested on a device. Built only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
14eaa5587d |
Honor mute when waking the screen for a received message (#11688)
* fix(ui): honor mute when waking the screen for a received message TextMessageModule fired powerFSM.trigger(EVENT_RECEIVED_MSG) for every text packet, gated only by shouldWakeOnReceivedMessage(), which checks external notification, device role and battery level but never the mute flags. A muted channel therefore suppressed the banner and still lit the screen. MessageRenderer::handleNewMessage() only computed mute for MessageType::BROADCAST, so a DM from a muted node produced a banner and a wake. Add isMutedForPacket() in Channels: a DM addressed to us reads the sender's NodeInfoLite mute bit, every other packet reads the mute bit of the channel it arrived on. This is the predicate ExternalNotificationModule already applied to the buzzer, vibra and LED outputs, hoisted so all three call sites share it. Bell and alert messages still break through mute on both paths, unchanged. No protobuf or config change: ChannelSettings.module_settings.is_muted and the NodeInfoLite mute bit already exist and are already settable from the device menu and via AdminMessage.toggle_muted_node. Closes #11674 * fix(ui): let an alert break through mute on the screen wake path In COLOR display mode TextMessageModule skips handleNewMessage(), so powerFSM.trigger(EVENT_RECEIVED_MSG) is the only wake an alert gets. Gating it on mute alone dropped that wake for a bell on a muted channel. Add MeshService::isAlertPayload(): an ASCII BEL in the payload while at least one alert_bell_* output is enabled. The wake gate is now "not muted, or an alert". MessageRenderer uses the same predicate instead of its own inline bell scan, which also lifts that scan's arbitrary 100 byte cap. Rename three test cases. Their names carried exactly 35 characters after the test_ prefix, which matches the Lob API key format and tripped trufflehog in the trunk check gate. |
||
|
|
427ed0f1a0 |
Load optional modules dropped into src/modules/optional/ (#11673)
* Load optional modules dropped into src/modules/optional/ bin/optional-modules.py scans src/modules/optional/ for a directory <Name>/ holding <Name>.h and generates $BUILD_DIR/OptionalModules.h with an include and a setup<Name>() call for each, which Modules.cpp picks up through __has_include. The directory does not exist in a stock checkout, so a stock build generates a header that defines nothing, OPTIONAL_MODULES_SETUP compiles away, and nothing is registered. Sources under the directory are already covered by the default recursive build_src_filter, so dropping a module in needs no platformio.ini edit. * Address review: skip a module directory that is not a usable identifier The directory name becomes a setup<Name>() call, so foo-bar/ would have generated setupfoo-bar() and failed to compile with the error pointing at generated code rather than at the directory. Names that cannot form an identifier are now skipped with a message that names the directory. |
||
|
|
b823c8d7fe |
Let a module hold the screen (#11631)
* Let a module hold the screen Screen::setModalModule() marks one module as owning the screen, honoured at the three places that would otherwise take it away: the carousel advance in runOnce(), the new-message banner in handleNewMessage(), and Cmd::STOP_ALERT_FRAME, which any caller can currently fire to cancel any alert frame regardless of who started it. Only the owning pointer can release it, so a module with a modal state no longer has to patch Screen.cpp to keep an alert from vanishing when a chat message arrives. The default is nullptr and no in-tree caller sets it, so every existing build behaves exactly as before. * Address review: clear pauseBanner even while a module holds the screen START_ALERT_FRAME sets NotificationRenderer::pauseBanner and STOP_ALERT_FRAME is the only thing that clears it, so swallowing the whole command left banners suppressed for good once a module took the screen. Only the setFrames() teardown is now gated on the modal owner. * Take the modal owner as a pointer to const Screen never dereferences it; the pointer is only stored and compared, so const is what the parameter and the member both mean. Fixes the cppcheck constParameterPointer defect on clearModalModule(). * Add isShowingModuleFrame() so a module can claim keys on its own frame Input observers registered by modules run before Screen's, so a module that handles UP/DOWN has to know whether its own frame is the one being looked at, or it takes the key away from the frame that is. moduleFrames is already index-aligned with the frame list for drawModuleFrame(), so the check is a lookup against the current frame. * Address review: match drawModuleFrame's frame selection, trim the comment Mid-transition drawModuleFrame() renders transitionFrameTarget, so comparing only currentFrame reported false while the module's frame was actually on screen and its input observer would have ignored keys. The header comment is back inside the two-line limit. |
||
|
|
3683566f62 |
Don't show new message banner on message screen (#11671)
* message banner * Update MessageRenderer.cpp * Fix message banner suppression race on Portduino |
||
|
|
47db0e3020 |
fix(admin): don't disable BLE on config paths that never reboot (#11651)
Three places took BLE down and left nothing to bring it back. The nRF52 auto-re-advertise bug masked them by restoring advertising ~1s later; with that fixed (#11650) the outage is real, lasting until the next PowerFSM transition - up to screen_on_secs, 10 minutes on a default client. - restore_preferences passed 1000 to reboot(), which takes seconds, arming the reset ~16.7 minutes out instead of the intended ~1s. With BLE disabled for a pending reboot the node was unreachable for that whole window. Use DEFAULT_REBOOT_SECONDS and disable before arming, matching the factory and nodedb reset paths. - mesh_beacon sets shouldReboot=false but was not in the list that spares a variant from the blanket disable, unlike statusmessage. Add it. - MQTT and Serial disable BLE inside their own case, bypassing the transaction check above them. Inside an edit transaction saveChanges() defers the reboot, so BLE went down with no restore - reachable today by importing a device profile containing either module config. Build: heltec-mesh-node-t096. Tests: test_module_config 3/3.v2.8.0.47db0e3 |
||
|
|
b8faaaf54b |
fix(fs): size the files manifest with a malloc probe, not a heap walk (#11667)
heap_caps_get_largest_free_block() walks every TLSF block of every matching heap while holding the allocator lock. On ESP32-S3 boards with PSRAM in the malloc pool, that walk runs long enough during the config handshake that WiFi RX on the other core blocks in wifi_malloc() and the interrupt watchdog reboots the node. Use the bounded malloc() probe (already the non-ESP32 path) on every target instead: TLSF malloc is O(1), so the allocator lock is only held momentarily. Touch the probe through a volatile pointer so LTO cannot elide the malloc()/free() pair. Fixes #11666 |
||
|
|
7dffd66c59 |
fix(radio): recover a chip that lost its state instead of assert-crashing in reconfigure() (#11676)
* fix(lr11x0): recover a chip that lost its state instead of assert-crashing in reconfigure() * fix(radio): extend chip-state-loss recovery to SX126x, SX128x, RF95, and LR20x0 |
||
|
|
52d521426b |
Wio Tracker L2: try-fix battery percentage (#11668)
* try-fix battery percentage * initialize cached_mv * use AnalogBatteryLevel class to calculate percentage level |
||
|
|
7239fe886a |
fix(BaseUI): let a module frame with no menu fall through the SELECT dispatch (#11659)
#11209 added a module-frame branch to the SELECT chain that claims the press for any non-null moduleFrames entry, but its body acts only on the environmental telemetry frame. Every other module frame lands there and the press dies: the branches below it - waypoint among them - are unreachable. #11358 already patched one casualty by excluding the nullptr padding, which restored the node list. Real module frames stayed swallowed, so the waypoint menu #10920 appended to the end of the chain has never opened on BaseUI. Enter the branch only when a module frame actually has a menu, so anything without one falls through to the frames matched after it. Co-authored-by: Ben Meadors <benmmeadors@gmail.com>v2.8.0.7239fe8 |
||
|
|
9625c6bebf |
nrf52840: prevent duplicate I2C switch case for LP5562/MMC5983MA (#11658)
* Initial plan * fix: avoid duplicate I2C switch case for LP5562/MMC5983MA Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> |
||
|
|
2afe097be6 |
chore(deps): update meshtastic/device-ui digest to 9d9b9df (#11653)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> |
||
|
|
36c89fa3a7 |
feat: Support Seeed Wio Tracker L2 (#10909)
* initial commit * enable power save * implement mesh LED * add ADS1115+AW35615 for wio tracker L2 * add ES8311, GT911, AW35615, LP5814 to I2C scanner * update commit references * move variant.cpp to extras * update hw_model * update lovyanGFX * point to device-ui commit * trunk fmt * fix IO expander (have to take from SensorLib for now as long as AudioThread has the limitation to only support SensorLib and the previous IO expander clashes with duplicate names in arduino-audio-driver) * workaround duplicate defined symbol * remove SensorLib; add lightweight Pca9555 class and use unified USE_PCA95X5; add wake button detection * keep TP_INT disabled(OUTPUT) as we use wake button for wakeup * PA off by default, enabled when playing sound; add some delay because typical class-D amps (NS4150 family) spec 20–50ms for the output stage to reach full swing after power-on * refactored AW35615 into new external library * local revert of PR10571 as this PR completely breaks the alert sound * fix detection of ADS1115 * update device-ui commit reference * add synchronisation to IO expander and call toggleDisplay() on wake button press * add battery curve, fix io expander sync * add SPILock, simplify macro usage * update device-ui * fix wakeup from sleep * revert because of #11604 * use new AUDIO_AMP_SETTLE_MS * remove test logs * enable BaseUI * use touch screen * refactor wakekey thread * fix wake button toggle screen on/off * fix battery percentage and plugIn state * Update src/graphics/TFTDisplay.cpp Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * consider to return I2C errors to make coderabbi happy * fix warnings * use Throttle for millis comparison * fix endTransmission in write * make the rabbit happy * spli targets -tft / non-tft * fix compile * revert forced use of Throttle * remove MeshLED * add HW_MODEL --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> |
||
|
|
f8a8d12477 |
fix(ble): stop BLE from coming back up during the pre-reboot window (#11650)
* fix(ble): stop BLE from coming back up during the pre-reboot window Saving a reboot-requiring config over BLE (e.g. screen timeout) made the node disconnect, re-advertise, let the phone reconnect, and then drop it again at the reset. Two causes: nRF52: admin messages from the phone run synchronously on Bluefruit's BLE event task, so the BLE_GAP_EVT_DISCONNECTED caused by shutdown() is only processed after we return - and that handler restarts advertising because restartOnDisconnect(true) was never cleared. Stopping advertising first is a no-op while a connection is live (the SoftDevice isn't advertising), so the deferred event brought it straight back. Clear the restart flag and stop advertising before dropping the link, mirroring nRF54L15's ble_enabled gate. This also closes a main-thread race on the shutdown path where Advertising.stop() could land between connection teardown and Bluefruit's auto-restart within the same event dispatch. PowerFSM (all platforms): darkEnter/onEnter/powerEnter/powerExit/serialExit unconditionally re-enable BLE, so any state transition inside the reboot window - a button press while the banner is up, the screen timeout, USB plug/unplug - turned BLE back on after AdminModule had deliberately torn it down. Route them through a helper that skips the re-enable while rebootAtMsec/shutdownAtMsec is armed; every writer of those deadlines is an imminent restart. * style: trim rationale comments to house 1-2 line limit The full mechanism is in the original commit message and PR description. |
||
|
|
db84bdf3b4 |
Reduce ExternalNotificationModule flash usage (RTTTL + InputBroker) (#10989)
* Generalize RTTTL exclusion into MESHTASTIC_EXCLUDE_RTTTL ExternalNotificationModule already stubbed out RTTTL playback for STM32WL/portduino/ESP32C6 via a raw ARCH/CONFIG_IDF check, but the ringtone config plumbing around it (protobuf message, encode/decode tables, /prefs/ringtone.proto persistence, admin get/set-ringtone handlers) still compiled in even though it can never do anything on those platforms. Introduce MESHTASTIC_EXCLUDE_RTTTL and gate the dead ringtone plumbing behind it too. The flag is set in each architecture's *_base build_flags (stm32_base, esp32c6_base, portduino_base) rather than in the module itself - this matches how every other MESHTASTIC_EXCLUDE_* flag in the tree is set (e.g. stm32_base already sets ten of them directly, and esp32c6_base already excludes PAXCOUNTER for an analogous platform-can't-support-this reason), rather than introducing a new per-architecture C header pattern. Behavior is unchanged on all three platforms; overridable via -D like every other MESHTASTIC_EXCLUDE_* flag. Also guard the two HAS_I2S ringtone-playback call sites with !MESHTASTIC_EXCLUDE_RTTTL alongside HAS_I2S, since rtttlConfig itself is now only declared when RTTTL is not excluded. No current platform defines both HAS_I2S and MESHTASTIC_EXCLUDE_RTTTL simultaneously, so this has no effect today, but prevents a future HAS_I2S platform that also excludes RTTTL from failing to compile. Saves 368 bytes flash / 236 bytes RAM on wio-e5 with no loss to the GPIO on/off notification toggle itself, which does not depend on RTTTL. Signed-off-by: Andrew Yong <me@ndoo.sg> Assisted-by: Claude Sonnet 5 <noreply@anthropic.com> * Skip unused InputBroker observer in ExternalNotificationModule The inputObserver CallbackObserver member was declared unconditionally, even though its only use site was already gated behind MESHTASTIC_EXCLUDE_INPUTBROKER (set for all of stm32 in stm32.ini). Because it's a non-trivial member, the compiler still generated its constructor/destructor as part of ExternalNotificationModule's own lifecycle even when InputBroker is compiled out entirely. Gate the member and its only consumer, handleInputEvent(), behind the same flag as their use site, and match the codebase's dominant !MESHTASTIC_EXCLUDE_X style (used ~330 times) rather than !defined(MESHTASTIC_EXCLUDE_X) (used ~20 times) while touching this flag's other call site. Saves an additional 288 bytes flash on wio-e5, no RAM change, no functional impact since InputBroker was already unused on this platform. Signed-off-by: Andrew Yong <me@ndoo.sg> Assisted-by: Claude Sonnet 5 <noreply@anthropic.com> * fix(native-wasm): exclude RTTTL to unbreak build The browser node builds its own build_flags from arduino_base rather than inheriting portduino_base, so it did not pick up the MESHTASTIC_EXCLUDE_RTTTL flag added to portduino_base. With the inline ARCH_PORTDUINO stub in ExternalNotificationModule.h now replaced by that flag, native-wasm tried to include the unavailable NonBlockingRtttl.h. Set MESHTASTIC_EXCLUDE_RTTTL=1 directly in the native-wasm env alongside its other exclusion flags. Assisted-by: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: Andrew Yong <me@ndoo.sg> --------- Signed-off-by: Andrew Yong <me@ndoo.sg> Co-authored-by: Ben Meadors <benmmeadors@gmail.com> |
||
|
|
7afd270f39 |
Gut beacon send-as-node and consolidate TX onto broadcast_targets (#11646)
* Gut beacon send-as-node and consolidate TX onto broadcast_targets Two MeshBeaconConfig changes, both against fields that never reached a tagged release, so there is no migration for existing nodes. broadcast_send_as_node let a client name a node ID to send beacons AS, rewriting the packet's `from`. Firmware never applied it - the assignment was commented out, so `from` was always the local node and the field was a settable, persisted no-op. It was also unsound as designed: rewriting `from` forges no signature, it only makes isFromUs() false, so perhapsEncode() skips XEdDSA signing and receivers get an unsigned packet attributed to another node. broadcast_on_channel / broadcast_on_region / broadcast_on_preset were a second way to name a beacon destination alongside broadcast_targets, chosen silently on whether broadcast_targets was empty. The comments claimed the two were equivalent; they were not. An inline ChannelSettings carries name and PSK, so broadcast_on_channel could transmit on a channel absent from the node's channel table, which channel_index cannot express. That is dropped deliberately - the channel must exist on the node. Empty broadcast_targets now synthesises one target on the running preset and region over the primary channel, matching what the scalar path produced when left unset, so an otherwise unconfigured node still beacons. The USERPREFS_MESH_BEACON_ON_* keys go with the fields. A preconfigured build that still defines one now fails at compile time with a pointer to the USERPREFS_MESH_BEACON_TARGET_0_* equivalents, rather than silently losing its beacon channel. The replacement names a channel-table slot, so such a build must also provision that channel. MeshBeaconConfig shrinks 324 -> 240 bytes and ModuleConfig 328 -> 244, against the 512-byte MAX_TO_FROM_RADIO_SIZE ceiling that FromRadio sits 2 bytes under. The protobufs submodule points at a branch carrying both proto changes; it needs re-pointing to master once meshtastic/protobufs#1047 and #1048 merge. * Point protobufs submodule at master now that the beacon protos are merged meshtastic/protobufs#1047 and #1048 are in master, so drop the temporary beacon-proto-integration pin. MeshBeaconConfig stays 240 bytes and ModuleConfig 244, unchanged from the integration branch. The bump also picks up master's unrelated additions: the MESHNOLOGY_W12 and MESHPAGER_X2 hardware models, and a ground-speed unit correction in Position. |
||
|
|
78219e09cb |
fix(stm32wl): add TCXO-optional support and fix hardcoded TCXO voltage (#10964)
* stm32wl: consult SX126X_DIO3_TCXO_VOLTAGE instead of hardcoding 1.7V Every STM32WL variant except rak3172 got setTCXOVoltage(1.7) unconditionally, regardless of what the board's hardware actually needs, and rak3172 got no TCXO configuration at all - so a real RAK3172-T (populated TCXO) failed radio init outright. Read SX126X_DIO3_TCXO_VOLTAGE per variant instead. When TCXO_OPTIONAL is also defined, retry once on XTAL if the TCXO attempt fails, mirroring the existing pattern in LR11x0Interface.cpp, LR20x0Interface.cpp, and the SX1262/SX1268 paths in RadioInterface.cpp. Signed-off-by: Andrew Yong <me@ndoo.sg> Assisted-by: Claude Sonnet 5 <noreply@anthropic.com> * stm32wl(rak3172): support both non-T and -T hardware via TCXO-optional RAK3172 is XTAL-only; RAK3172-T has a populated 3.0V TCXO, matching RAK's own reference radio_conf.h. One PlatformIO environment now serves both: tries the TCXO first, falls back to XTAL if not populated. Hardware-verified on a TCXO-equipped board electrically equivalent to RAK3172-T. Genuine non-T hardware not available to re-verify the fallback path; reasoned from RadioLib source instead (see PR description). Signed-off-by: Andrew Yong <me@ndoo.sg> Assisted-by: Claude Sonnet 5 <noreply@anthropic.com> * stm32wl(wio-e5): declare the module's 1.7V TCXO explicitly Matches Seeed's own reference radio driver. wio-e5 previously relied on the hardcoded 1.7V fallback being removed by the preceding commit, which would have broken it - declare the voltage explicitly instead. Hardware-verified via SWD: without this define, the radio interface fails to come up at all (sendtext NAKs with NO_INTERFACE, meaning rIf is null). With it, NO_INTERFACE goes away and the device sends/receives normally. Signed-off-by: Andrew Yong <me@ndoo.sg> Assisted-by: Claude Sonnet 5 <noreply@anthropic.com> * stm32wl(CDEBYTE_E77-MBL): mark TCXO voltage optional, hardware varies by unit EByte changed the E77-MBL hardware in early 2024: units with serial number >= 3202995 have a TCXO, older units have a ceramic crystal oscillator instead. Both ship under the same module name, so probe for the TCXO and fall back to XTAL rather than assuming either. https://github.com/olliw42/mLRS-docu/blob/main/docs/EBYTE_E77_MBL.md Not hardware-tested - no E77-MBL board available this session. Signed-off-by: Andrew Yong <me@ndoo.sg> Assisted-by: Claude Sonnet 5 <noreply@anthropic.com> * stm32wl: trim TCXO comment blocks to repo's 1-2 line guideline Per review feedback on PR #10964 (CodeRabbit nitpicks) - the rak3172 and CDEBYTE_E77-MBL variant.h comments were 4-line blocks, exceeding the repo's comment-length convention. Condensed to one line each, same information and links retained. Signed-off-by: Andrew Yong <me@ndoo.sg> Assisted-by: Claude Sonnet 5 <noreply@anthropic.com> --------- Signed-off-by: Andrew Yong <me@ndoo.sg> |
||
|
|
57d17cfd44 |
fix(stm32wl): recover from littlefs internal corruption instead of hanging (#11230)
LFS_ASSERT (src/platform/stm32wl/littlefs/lfs_util.h) was a plain assert(), which on STM32WL hangs forever with no diagnostic (__wrap___assert_func is while(true);, see main-stm32wl.cpp). STM32_LittleFS::begin() is already designed to treat corruption as recoverable - format and retry, see fsFormat()/NodeDB::saveToDisk() - but that only works if lfs_mount() cleanly returns an error. An internal littlefs consistency check failing (metadata pair/CRC/block-allocator invariants) never returns at all, so a bad flash sector or power loss mid-write could permanently brick a device that would otherwise have recovered via the existing reformat path. nRF52 already hit this and fixed it (LFS_NO_ASSERT + a custom lfs_assert() that reboots into a reformat, see meshtastic/firmware#3818). Port the same approach to STM32WL: LFS_NO_ASSERT routes LFS_ASSERT through a custom lfs_assert() instead of disabling the check outright, and lfs_assert() requests a reformat-on-next-boot via a .noinit SRAM magic value (the same mechanism already used for the DFU bootloader redirect in this file, chosen specifically because backup/TAMP registers don't reliably survive a soft reset in this toolchain) and reboots, rather than trying to reformat littlefs from inside its own possibly-mid-operation callback. Unlike nRF52 (a third-party Adafruit library patched via a -include override so as not to fork it), STM32WL's littlefs copy is already a project-owned vendored file, so lfs_util.h is edited directly. Assisted-by: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: Andrew Yong <me@ndoo.sg> Co-authored-by: Tom <116762865+NomDeTom@users.noreply.github.com> |
||
|
|
467dc44cfa |
Update applyLoraRegion to enable TX on set (#11643)
* Update applyLoraRegion to enable TX on set * Don't enable TX if in HamMode. User must set callsign first * Don't use isHam, use owner.is_licensed |
||
|
|
7e9525ad83 |
feat(baseui): default US to LongTurbo on first region selection (#11637)
Selecting US in the BaseUI region chooser now installs LongTurbo instead of LongFast, but only for out-of-box setup: the outgoing region must be UNSET, so a later switch to US leaves whatever preset the node is running alone. Scoped to the menu on purpose. The US entry in regions[] keeps LongFast as its default preset, so preset repair, admin/phone writes and every other route onto US are unchanged. A build pinning USERPREFS_LORACONFIG_MODEM_PRESET, a preset already moved off the install default, or use_preset=false all outrank it. The decision is lifted into menuHandler::presetForRegionSelection() so it is reachable without a Screen, following toggleNodeMuted(). |
||
|
|
7aa8ad3510 |
fix(t-watch-ultra): build with the esp32s3 flags, not the classic-ESP32 ones (#11619)
* fix(t-watch-ultra): build with the esp32s3 flags, not the classic-ESP32 ones
The env was the only esp32s3 variant extending ${esp32_base.build_flags} (since
#8171). That base adds -D ESP32_FORCE_IRAM_MEMSET -Wl,--wrap=memset
-Wl,--wrap=memcpy, and the wrappers in IramMemcpy.c/IramMemset.c decide whether
the cache is on by reading 0x3FF00040 - DPORT_PRO_CACHE_CTRL_REG on the classic
ESP32, an address the S3 does not map at all (soc.h: DRAM 0x3FC88000-0x3FD00000,
DROM 0x3C000000-0x3E000000, IRAM 0x40370000-0x403E0000, peripherals 0x60000000).
--wrap is link-wide, so every memcpy/memset in the image - including inside the
precompiled WiFi, lwIP and flash driver libraries - branched on that undefined
read. Two long-standing board-specific bugs came from it, both dating to #8171,
which introduced the wrong base and the first workaround in the same commit:
* WPA2 networks associated and completed the 4-way handshake, then never got a
DHCP lease, while open networks worked normally (#11513).
* Direct flash reads returned 0x00 for data that was correct on flash, so NVS
came up empty every boot and dropped BLE bonds (#11530).
Switching the env to esp32s3_base fixes both on hardware: WPA2 gets a lease, and
NVS survives a reboot with the bond intact. The read workaround that #11530
needed - -Wl,--wrap=esp_partition_read, -Wl,--wrap=esp_flash_read and
esp_partition_read_mmap_wrap.c - is therefore removed as well.
The module excludes the env inherited from esp32_base go with it, so the board
now matches every other esp32s3 variant: web server and paxcounter are built
(paxcounter still only runs when enabled in config), and MESHTASTIC_EXCLUDE_AUDIO
was already inert here because AudioModule additionally requires USE_SX1280.
-UMESHTASTIC_EXCLUDE_ACCELEROMETER goes too, having only existed to undo an
inherited -D.
Also guards ESP32_FORCE_IRAM_MEMSET behind CONFIG_IDF_TARGET_ESP32, so a variant
cannot enable the classic-ESP32 probe on another target again.
* Update platformio.ini
added missing ${device-ui_base.custom_sdkconfig}
---------
Co-authored-by: Manuel <71137295+mverch67@users.noreply.github.com>
|