mirror of
https://github.com/vicliu624/trail-mate.git
synced 2026-09-01 16:48:21 +00:00
Align Meshtastic phone sync and chat UI refresh
This commit is contained in:
@@ -0,0 +1,681 @@
|
||||
# Meshtastic Phone API / BLE Protocol Reference
|
||||
|
||||
## Purpose
|
||||
|
||||
This document summarizes the Meshtastic phone-facing protocol as implemented by the official upstream code currently vendored in this repo:
|
||||
|
||||
- Firmware: `.tmp/firmware`
|
||||
- Apple app: `.tmp/Meshtastic-Apple`
|
||||
- Android app: `.tmp/meshtastic-android`
|
||||
|
||||
The goal is to answer protocol and encoding questions from source-grounded rules instead of relying on local assumptions.
|
||||
|
||||
This document focuses on:
|
||||
|
||||
- `ToRadio` / `FromRadio`
|
||||
- BLE `fromNum` / `fromRadio` interaction
|
||||
- `QueueStatus`
|
||||
- `MeshPacket.id`
|
||||
- `decoded.request_id`
|
||||
- `decoded.reply_id`
|
||||
- `want_ack`
|
||||
- broadcast vs direct-message behavior
|
||||
- how official apps interpret ACK/NAK state
|
||||
|
||||
## Source Anchors
|
||||
|
||||
Primary firmware sources:
|
||||
|
||||
- `.tmp/firmware/src/mesh/PhoneAPI.h`
|
||||
- `.tmp/firmware/src/mesh/PhoneAPI.cpp`
|
||||
- `.tmp/firmware/src/mesh/api/PacketAPI.cpp`
|
||||
- `.tmp/firmware/src/mesh/StreamAPI.cpp`
|
||||
- `.tmp/firmware/src/mesh/Router.cpp`
|
||||
- `.tmp/firmware/src/mesh/ReliableRouter.cpp`
|
||||
- `.tmp/firmware/src/mesh/MeshService.cpp`
|
||||
- `.tmp/firmware/src/mesh/MeshModule.cpp`
|
||||
- `.tmp/firmware/src/modules/RoutingModule.cpp`
|
||||
- `.tmp/firmware/src/mesh/generated/meshtastic/mesh.pb.h`
|
||||
|
||||
Official app sources:
|
||||
|
||||
- `.tmp/Meshtastic-Apple/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift`
|
||||
- `.tmp/Meshtastic-Apple/Meshtastic/Accessory/Accessory Manager/AccessoryManager+FromRadio.swift`
|
||||
- `.tmp/Meshtastic-Apple/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift`
|
||||
- `.tmp/Meshtastic-Apple/Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLEConnection.swift`
|
||||
- `.tmp/Meshtastic-Apple/Meshtastic/Helpers/MeshPackets.swift`
|
||||
- `.tmp/meshtastic-android/app/src/main/java/com/geeksville/mesh/service/PacketHandler.kt`
|
||||
- `.tmp/meshtastic-android/app/src/main/java/com/geeksville/mesh/service/FromRadioPacketHandler.kt`
|
||||
- `.tmp/meshtastic-android/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt`
|
||||
- `.tmp/meshtastic-android/core/model/src/commonMain/kotlin/org/meshtastic/core/model/DataPacket.kt`
|
||||
|
||||
## Big Picture
|
||||
|
||||
Meshtastic exposes a "phone API" over multiple transports:
|
||||
|
||||
- BLE
|
||||
- serial stream
|
||||
- packet/IPC API
|
||||
- HTTP variants
|
||||
|
||||
Across those transports, the logical payloads are the same:
|
||||
|
||||
- phone -> device: `ToRadio`
|
||||
- device -> phone: `FromRadio`
|
||||
|
||||
The transport framing differs, but the application-level meaning does not.
|
||||
|
||||
Important consequence:
|
||||
|
||||
- If we want to know "what the phone is supposed to believe", we must follow `PhoneAPI.cpp` plus the official app code.
|
||||
- If we want to know "what ACK means", we must follow `Router.cpp`, `ReliableRouter.cpp`, `MeshModule.cpp`, and app-side routing handling.
|
||||
|
||||
## Transport Layer Rules
|
||||
|
||||
### Serial / stream transport
|
||||
|
||||
`StreamAPI.cpp` shows the serial framing:
|
||||
|
||||
- start bytes: `0x94 0xC3`
|
||||
- then 16-bit big-endian payload length
|
||||
- then protobuf bytes
|
||||
|
||||
Payload directions:
|
||||
|
||||
- toward device: `ToRadio`
|
||||
- toward client: `FromRadio`
|
||||
|
||||
This is transport framing only. After decoding, the same `PhoneAPI` logic applies.
|
||||
|
||||
### Packet API transport
|
||||
|
||||
`PacketAPI.cpp` wraps the same behavior in queued protobuf packets.
|
||||
|
||||
Important rules:
|
||||
|
||||
- `ToRadio.packet` is passed into `service->handleToRadio(*mp)`
|
||||
- `ToRadio.want_config_id` starts the config state machine
|
||||
- `ToRadio.heartbeat` is handled
|
||||
- outgoing `FromRadio` packets are produced by `getFromRadio()`
|
||||
|
||||
### BLE transport
|
||||
|
||||
Official BLE behavior is split between:
|
||||
|
||||
- firmware BLE implementation
|
||||
- `PhoneAPI`
|
||||
- app BLE client logic
|
||||
|
||||
Upstream firmware exposes:
|
||||
|
||||
- `TORADIO`
|
||||
- `FROMRADIO`
|
||||
- `FROMNUM`
|
||||
- `LOGRADIO`
|
||||
|
||||
Official Apple BLE behavior in `BLEConnection.swift`:
|
||||
|
||||
- phone writes protobuf bytes to `TORADIO`
|
||||
- phone receives `FROMNUM` notification
|
||||
- after `FROMNUM`, phone drains pending packets by repeatedly reading `FROMRADIO`
|
||||
- drain ends when `FROMRADIO` read returns empty payload
|
||||
|
||||
So `FROMNUM` is not the data itself. It is a wakeup/edge signal telling the client that one or more `FromRadio` packets are ready.
|
||||
|
||||
## PhoneAPI State Machine
|
||||
|
||||
`PhoneAPI.cpp` contains the canonical state machine for what the phone receives.
|
||||
|
||||
States are:
|
||||
|
||||
1. `STATE_SEND_NOTHING`
|
||||
2. `STATE_SEND_UIDATA`
|
||||
3. `STATE_SEND_MY_INFO`
|
||||
4. `STATE_SEND_OWN_NODEINFO`
|
||||
5. `STATE_SEND_METADATA`
|
||||
6. `STATE_SEND_CHANNELS`
|
||||
7. `STATE_SEND_CONFIG`
|
||||
8. `STATE_SEND_MODULECONFIG`
|
||||
9. `STATE_SEND_OTHER_NODEINFOS`
|
||||
10. `STATE_SEND_FILEMANIFEST`
|
||||
11. `STATE_SEND_COMPLETE_ID`
|
||||
12. `STATE_SEND_PACKETS`
|
||||
|
||||
Important rule explicitly documented in code:
|
||||
|
||||
- client apps assume this config-send order
|
||||
- upstream comments say: "DO NOT CHANGE IT"
|
||||
|
||||
### Config start
|
||||
|
||||
When the device receives `ToRadio.want_config_id`:
|
||||
|
||||
- `PhoneAPI::handleStartConfig()` is called
|
||||
- the connection is considered active
|
||||
- the device enters the config-send sequence
|
||||
- after config is complete, it sends `FromRadio.config_complete_id`
|
||||
- only then does it move to `STATE_SEND_PACKETS`
|
||||
|
||||
### Special nonces
|
||||
|
||||
`PhoneAPI.h` defines:
|
||||
|
||||
- `SPECIAL_NONCE_ONLY_CONFIG = 69420`
|
||||
- `SPECIAL_NONCE_ONLY_NODES = 69421`
|
||||
|
||||
Meaning:
|
||||
|
||||
- `69420`: send config-related state without full node DB walk
|
||||
- `69421`: focus on node info flow
|
||||
|
||||
Official Apple app uses the same constants in `AccessoryManager.swift`.
|
||||
|
||||
## `ToRadio` Variants
|
||||
|
||||
Source of truth: `PhoneAPI.cpp`, `PacketAPI.cpp`.
|
||||
|
||||
Officially handled variants include:
|
||||
|
||||
- `packet`
|
||||
- `want_config_id`
|
||||
- `disconnect`
|
||||
- `xmodemPacket`
|
||||
- `mqttClientProxyMessage`
|
||||
- `heartbeat`
|
||||
|
||||
### `ToRadio.packet`
|
||||
|
||||
This is the normal way for the app to send a mesh packet through the connected device.
|
||||
|
||||
Flow:
|
||||
|
||||
1. phone builds `MeshPacket`
|
||||
2. wraps in `ToRadio.packet`
|
||||
3. device `PhoneAPI::handleToRadioPacket()`
|
||||
4. device applies local rules and rate limits
|
||||
5. device calls `service->handleToRadio(p)`
|
||||
6. device injects it into mesh routing via `MeshService`
|
||||
|
||||
### `ToRadio.want_config_id`
|
||||
|
||||
Starts config sync.
|
||||
|
||||
The response is not a single packet. It is the whole config state machine ending with:
|
||||
|
||||
- `FromRadio.config_complete_id = same nonce`
|
||||
|
||||
### `ToRadio.heartbeat`
|
||||
|
||||
In `PhoneAPI.cpp`, heartbeat only sets a flag:
|
||||
|
||||
- `heartbeatReceived = true`
|
||||
|
||||
Then the next `getFromRadio()` emits:
|
||||
|
||||
- `FromRadio.queueStatus`
|
||||
|
||||
So on modern firmware, heartbeat is effectively a "please prove you are alive and tell me queue status" request.
|
||||
|
||||
Official Apple app uses this to detect link liveness.
|
||||
|
||||
## `FromRadio` Variants
|
||||
|
||||
`PhoneAPI.cpp` sends these categories:
|
||||
|
||||
- config-related data: `my_info`, `node_info`, `metadata`, `channel`, `config`, `moduleConfig`, `fileInfo`, `config_complete_id`
|
||||
- steady-state data: `packet`, `queueStatus`, `mqttClientProxyMessage`, `clientNotification`, `xmodemPacket`
|
||||
- system events: `rebooted`, `log_record`
|
||||
|
||||
Important distinction:
|
||||
|
||||
- `FromRadio.packet` carries a `MeshPacket`
|
||||
- `FromRadio.queueStatus` is not a mesh packet
|
||||
- `FromRadio.clientNotification` is not a mesh packet
|
||||
|
||||
That distinction matters because official apps treat them differently.
|
||||
|
||||
## `MeshPacket` Field Semantics
|
||||
|
||||
Source of truth: `mesh.pb.h`, `Router.cpp`, `MeshModule.cpp`, app code.
|
||||
|
||||
### `MeshPacket.id`
|
||||
|
||||
This is the packet identifier for the mesh packet itself.
|
||||
|
||||
Important upstream comments say:
|
||||
|
||||
- it is unique per sender for a short time window
|
||||
- used by flooding / ACK / retransmission logic
|
||||
- used by crypto implementation too
|
||||
|
||||
In firmware:
|
||||
|
||||
- if phone did not set `id`, `MeshService::handleToRadio()` generates one
|
||||
- queue-status responses use the packet's `id` as `mesh_packet_id`
|
||||
|
||||
Therefore:
|
||||
|
||||
- app-created packet IDs matter
|
||||
- if app sets `id`, later status signals should correlate back to this same ID
|
||||
|
||||
### `decoded.request_id`
|
||||
|
||||
Upstream protobuf comment:
|
||||
|
||||
- only used in routing or response messages
|
||||
- indicates the original message ID this message is reporting on
|
||||
|
||||
In practice:
|
||||
|
||||
- routing ACK/NAK packets use `decoded.request_id = original_packet.id`
|
||||
- normal responses to a request also use `request_id` to point at the request packet
|
||||
- official apps use `request_id` to correlate a response/ACK with the outbound request
|
||||
|
||||
### `decoded.reply_id`
|
||||
|
||||
Upstream protobuf comment:
|
||||
|
||||
- indicates this message is a reply to a previous message
|
||||
|
||||
This is user/content-level threading, not transport ACK.
|
||||
|
||||
Examples:
|
||||
|
||||
- text reply to a previous message
|
||||
- emoji reaction targeting a prior message
|
||||
|
||||
Do not confuse `reply_id` with ACK state.
|
||||
|
||||
### `want_ack`
|
||||
|
||||
This means the sender wants reliable delivery behavior and an ACK-style confirmation path.
|
||||
|
||||
However, upstream code makes one critical exception:
|
||||
|
||||
- `Router.cpp` forcibly clears `want_ack` on broadcast packets before they go over LoRa
|
||||
|
||||
That means:
|
||||
|
||||
- broadcast over-the-air packets are never true "normal ACKed unicast sends"
|
||||
- any phone/app logic that treats broadcast as awaiting a direct recipient ACK is wrong
|
||||
|
||||
## Broadcast vs Direct Message
|
||||
|
||||
This is the most important rule for current debugging.
|
||||
|
||||
### Direct message
|
||||
|
||||
For non-broadcast packets:
|
||||
|
||||
- `want_ack` can remain set
|
||||
- `ReliableRouter` tracks retransmissions
|
||||
- recipient may send a true routing ACK/NAK
|
||||
- official apps can eventually move message to delivered/error based on routing result
|
||||
|
||||
### Broadcast message
|
||||
|
||||
For broadcast packets:
|
||||
|
||||
- `Router.cpp` clears `want_ack` before air transmission
|
||||
- no normal destination-specific ACK flood is used
|
||||
- reliability is based on rebroadcast observation and implicit acknowledgment logic
|
||||
|
||||
Upstream `ReliableRouter.cpp` behavior:
|
||||
|
||||
- if the original sender sees someone rebroadcast its broadcast packet
|
||||
- firmware generates an implicit ACK internally
|
||||
- this is an optimization for flooding reliability
|
||||
- that ACK is generated on the original sender node and then surfaces to the phone as a local `ROUTING_APP` result tied to the original `request_id`
|
||||
- it should not be rewritten as if it came from the rebroadcaster's node identity
|
||||
|
||||
This implicit ACK is not the same thing as:
|
||||
|
||||
- a direct-message ACK from the intended peer
|
||||
- a conversation-level proof that one specific remote user acknowledged receipt
|
||||
|
||||
Therefore:
|
||||
|
||||
- a broadcast packet must not be surfaced to phone UI as "waiting for DM ACK from peer X"
|
||||
- a rebroadcaster or relay identity must not be mistaken for the final application peer
|
||||
|
||||
## Official ACK / NAK Generation
|
||||
|
||||
### Routing ACK packet format
|
||||
|
||||
`MeshModule::allocAckNak()` builds a packet with:
|
||||
|
||||
- `decoded.portnum = ROUTING_APP`
|
||||
- payload = encoded `Routing`
|
||||
- `decoded.request_id = original packet id`
|
||||
- `to = original sender`
|
||||
|
||||
This is the canonical ACK/NAK message shape.
|
||||
|
||||
### Reply packet format
|
||||
|
||||
`setReplyTo()` in `MeshModule.cpp` sets:
|
||||
|
||||
- `p->to = original sender`
|
||||
- `p->channel = original channel`
|
||||
- `p->want_ack = to.want_ack` except local-phone case
|
||||
- `p->decoded.request_id = original request id`
|
||||
|
||||
So for admin or other request/response flows:
|
||||
|
||||
- an ordinary response packet can also carry `request_id`
|
||||
- apps may use that to match request -> response even when it is not a routing ACK
|
||||
|
||||
### ReliableRouter rules
|
||||
|
||||
`ReliableRouter.cpp` distinguishes:
|
||||
|
||||
- ACK: routing packet with `error_reason == NONE`, or non-routing response carrying `request_id`
|
||||
- NAK: routing packet with non-`NONE` error reason
|
||||
|
||||
Key code:
|
||||
|
||||
- `ackId = ((c && c->error_reason == NONE) || !c) ? p->decoded.request_id : 0`
|
||||
- `nakId = (c && c->error_reason != NONE) ? p->decoded.request_id : 0`
|
||||
|
||||
Meaning:
|
||||
|
||||
- a packet with `request_id` can stop retransmission
|
||||
- for routing packets, the error code decides ACK vs NAK
|
||||
|
||||
## `QueueStatus` Semantics
|
||||
|
||||
This is the second most important rule.
|
||||
|
||||
Source of truth:
|
||||
|
||||
- `MeshService::sendToMesh()`
|
||||
- `MeshService::sendQueueStatusToPhone()`
|
||||
- `PhoneAPI::handleToRadioPacket()`
|
||||
- Android `PacketHandler.handleQueueStatus()`
|
||||
|
||||
### What `QueueStatus` means
|
||||
|
||||
After a phone-originated mesh packet is handed into routing, firmware always tries to send a `QueueStatus` back to the phone:
|
||||
|
||||
- `res` = immediate result of enqueue / local send attempt
|
||||
- `free` = current number of free queue entries
|
||||
- `maxlen` = queue capacity
|
||||
- `mesh_packet_id` = the outbound packet ID this status refers to
|
||||
|
||||
This happens in `MeshService::sendToMesh()`.
|
||||
|
||||
So `QueueStatus` answers:
|
||||
|
||||
- was this packet accepted by the local device/radio path?
|
||||
- what is the local transmit queue state right now?
|
||||
|
||||
It does not answer:
|
||||
|
||||
- whether the remote node received it
|
||||
- whether the remote node ACKed it
|
||||
- whether a routing error happened later
|
||||
|
||||
### Heartbeat `QueueStatus`
|
||||
|
||||
Heartbeat also returns a `QueueStatus`, but that one is only link-liveness / local queue information.
|
||||
|
||||
It is not a send result for a specific message unless `mesh_packet_id` points to one.
|
||||
|
||||
### Official Android interpretation
|
||||
|
||||
`PacketHandler.kt`:
|
||||
|
||||
- when packet is sent to radio, status becomes `ENROUTE`
|
||||
- `handleQueueStatus()` only completes the local "radio accepted it" wait
|
||||
- if `requestId != 0`, Android matches by `mesh_packet_id`
|
||||
|
||||
So Android uses `QueueStatus` to move past the radio-send stage, not to declare final delivery.
|
||||
|
||||
This is the exact reason "queueStatus arrived" is not enough to clear "waiting to be acknowledged".
|
||||
|
||||
## Official App Send-State Interpretation
|
||||
|
||||
### Android
|
||||
|
||||
Relevant code:
|
||||
|
||||
- `PacketHandler.kt`
|
||||
- `MeshDataHandler.kt`
|
||||
- `DataPacket.kt`
|
||||
|
||||
Android status model:
|
||||
|
||||
- `QUEUED`
|
||||
- `ENROUTE`
|
||||
- `DELIVERED`
|
||||
- `ERROR`
|
||||
- `RECEIVED`
|
||||
|
||||
Behavior:
|
||||
|
||||
1. app sends packet -> `ENROUTE`
|
||||
2. firmware returns `QueueStatus(mesh_packet_id=id)` -> local send gate completes
|
||||
3. later, `ROUTING_APP` packet with `request_id=id` drives final status
|
||||
|
||||
`MeshDataHandler.handleRouting()`:
|
||||
|
||||
- decodes `Routing`
|
||||
- calls `handleAckNak(requestId, fromId, routingError, relayNode)`
|
||||
|
||||
Status mapping:
|
||||
|
||||
- ACK from ultimate target or reaction target may become `RECEIVED`
|
||||
- ACK otherwise becomes `DELIVERED`
|
||||
- non-zero routing error becomes `ERROR`
|
||||
|
||||
The essential point:
|
||||
|
||||
- final delivery status comes from `ROUTING_APP`, not `QueueStatus`
|
||||
|
||||
### Apple
|
||||
|
||||
Relevant code:
|
||||
|
||||
- `AccessoryManager.swift`
|
||||
- `MeshPackets.swift`
|
||||
|
||||
Apple receives `FromRadio.packet`, checks `decoded.portnum`, and for `ROUTING_APP` calls:
|
||||
|
||||
- `MeshPackets.shared.routingPacket(packet:connectedNodeNum:)`
|
||||
|
||||
That handler:
|
||||
|
||||
- finds message by `packet.decoded.requestID`
|
||||
- stores `ackError`
|
||||
- if `routingMessage.errorReason == .none`, sets `receivedACK = true`
|
||||
- records `relayNode`, `ackTimestamp`, `ackSNR`
|
||||
|
||||
Apple therefore also treats:
|
||||
|
||||
- routing packet keyed by `requestID`
|
||||
- as the authoritative ACK/NAK path
|
||||
|
||||
Again:
|
||||
|
||||
- `QueueStatus` is not final delivery
|
||||
|
||||
## BLE Read / Notify Contract
|
||||
|
||||
Combining firmware and Apple app:
|
||||
|
||||
1. phone writes `ToRadio` to `TORADIO`
|
||||
2. firmware eventually increments `fromNum`
|
||||
3. firmware notifies `FROMNUM`
|
||||
4. app starts draining `FROMRADIO`
|
||||
5. app keeps reading until empty read
|
||||
|
||||
Important consequences:
|
||||
|
||||
- if firmware queues `FromRadio` data but does not cause the app to drain, status updates can appear delayed
|
||||
- if firmware only wakes the app for some variants and not others, phone-side state may lag
|
||||
- if `QueueStatus` or `ROUTING_APP` packets are generated but not drained, UI stays stale
|
||||
|
||||
## What Must Not Be Misinterpreted
|
||||
|
||||
### Rule 1: `QueueStatus` is not final ACK
|
||||
|
||||
Incorrect:
|
||||
|
||||
- "I saw `QueueStatus` for packet `X`, so message `X` was acknowledged by the peer"
|
||||
|
||||
Correct:
|
||||
|
||||
- "`QueueStatus` means the local radio path accepted or rejected the outbound packet"
|
||||
|
||||
### Rule 2: `reply_id` is not ACK state
|
||||
|
||||
Incorrect:
|
||||
|
||||
- "This packet has `reply_id`, so it acknowledges the earlier packet"
|
||||
|
||||
Correct:
|
||||
|
||||
- `reply_id` is conversation-level reply threading
|
||||
|
||||
### Rule 3: broadcast should not be modeled as direct-message ACK
|
||||
|
||||
Incorrect:
|
||||
|
||||
- "Broadcast packet to `0xFFFFFFFF` should wait for recipient ACK"
|
||||
|
||||
Correct:
|
||||
|
||||
- broadcast ACK-on-air is suppressed by upstream router
|
||||
- reliability uses flooding / rebroadcast observation
|
||||
- relay observations are not direct recipient ACK semantics
|
||||
|
||||
### Rule 4: relay / rebroadcast node is not automatically the logical sender of ACK
|
||||
|
||||
Incorrect:
|
||||
|
||||
- "I saw a routing-related event from short relay `0x11`; therefore node `0x11` is the chat peer who acknowledged"
|
||||
|
||||
Correct:
|
||||
|
||||
- it may be an intermediate relay, rebroadcaster, or broadcast-side routing artifact
|
||||
- interpretation depends on whether the original packet was unicast or broadcast
|
||||
|
||||
## Concrete Rules For Our Integration
|
||||
|
||||
These rules follow upstream behavior and should be treated as protocol constraints.
|
||||
|
||||
### Outbound phone message
|
||||
|
||||
- Preserve `MeshPacket.id` if app/core assigned one.
|
||||
- Use that same ID as the stable correlation key across:
|
||||
- `QueueStatus.mesh_packet_id`
|
||||
- `ROUTING_APP.decoded.request_id`
|
||||
- any response packet carrying `decoded.request_id`
|
||||
|
||||
### Broadcast text send
|
||||
|
||||
- Do not model broadcast text as requiring a direct recipient ACK.
|
||||
- Do not convert relay or rebroadcast observations into peer-delivery ACK for chat UI.
|
||||
- Do not surface a broadcast routing artifact as if it were a DM acknowledgment from a user node.
|
||||
|
||||
### Direct-message text send
|
||||
|
||||
- `QueueStatus` means local acceptance only.
|
||||
- Wait for `ROUTING_APP` or a request-correlated response to decide final state.
|
||||
- A `Routing.Error.NONE` for matching `request_id` is the canonical success signal.
|
||||
|
||||
### Admin / request-response flows
|
||||
|
||||
- Some requests may be effectively confirmed by a real response packet carrying `request_id`.
|
||||
- Official Apple app explicitly treats admin responses as an ACK-equivalent for the admin log entry.
|
||||
|
||||
### BLE transport
|
||||
|
||||
- `FROMNUM` must wake draining of `FROMRADIO`.
|
||||
- All generated `FromRadio` packets that matter to UI state must be drainable in a timely way.
|
||||
|
||||
## Why `from=00000011` Was Suspicious In Our Case
|
||||
|
||||
From upstream rules alone:
|
||||
|
||||
- if the original outbound packet was a broadcast message
|
||||
- and the phone/UI later treated a routing-related event from `0x00000011` as the final peer ACK
|
||||
- that interpretation is wrong
|
||||
|
||||
Because upstream says:
|
||||
|
||||
- broadcasts do not carry normal over-air `want_ack`
|
||||
- their reliability path is based on flooding and implicit observation
|
||||
- relay/rebroadcast evidence is not equivalent to DM recipient acknowledgment
|
||||
|
||||
So if a broadcast text on our branch ended up surfacing:
|
||||
|
||||
- `request_id = original text id`
|
||||
- plus a routing-style success attributed to a relay-like node
|
||||
|
||||
the likely bug is not "Meshtastic protocol says relay `0x11` is the peer ACK sender".
|
||||
|
||||
The likely bug is:
|
||||
|
||||
- our integration mapped a broadcast-side routing observation into a DM-style ACK event for the phone layer
|
||||
|
||||
That conclusion is source-consistent with upstream behavior.
|
||||
|
||||
## Practical Debug Checklist
|
||||
|
||||
When debugging a message stuck on "waiting to be acknowledged", check in this order:
|
||||
|
||||
1. Did the phone send `ToRadio.packet` with a stable `MeshPacket.id`?
|
||||
2. Did firmware emit `QueueStatus.mesh_packet_id == that id`?
|
||||
3. If no, the problem is local enqueue / transport / BLE drain.
|
||||
4. If yes, did a later `FromRadio.packet` arrive with `decoded.request_id == that id`?
|
||||
5. If yes and `portnum == ROUTING_APP`, decode `Routing.error_reason`.
|
||||
6. If the original packet was broadcast, do not interpret relay observations as DM ACK.
|
||||
7. If the original packet was a request expecting content response, also check non-routing response packets carrying `request_id`.
|
||||
|
||||
## Short Reference Table
|
||||
|
||||
`MeshPacket.id`
|
||||
|
||||
- ID of the outbound packet itself
|
||||
- primary correlation key
|
||||
|
||||
`QueueStatus.mesh_packet_id`
|
||||
|
||||
- local enqueue/send result for outbound packet ID
|
||||
- not final remote ACK
|
||||
|
||||
`decoded.request_id`
|
||||
|
||||
- "this packet refers to original packet ID X"
|
||||
- used for routing ACK/NAK and normal responses
|
||||
|
||||
`decoded.reply_id`
|
||||
|
||||
- content/thread reply to previous message
|
||||
- not transport ACK
|
||||
|
||||
`want_ack`
|
||||
|
||||
- reliable-delivery request for unicast path
|
||||
- cleared by router for broadcast over the air
|
||||
|
||||
`ROUTING_APP`
|
||||
|
||||
- canonical ACK/NAK packet family
|
||||
- official apps use it for final delivery state
|
||||
|
||||
## Notes For Future Maintenance
|
||||
|
||||
If upstream changes behavior, re-check at least:
|
||||
|
||||
- `PhoneAPI.cpp`
|
||||
- `Router.cpp`
|
||||
- `ReliableRouter.cpp`
|
||||
- `MeshService.cpp`
|
||||
- Apple `BLEConnection.swift`
|
||||
- Apple `MeshPackets.swift`
|
||||
- Android `PacketHandler.kt`
|
||||
- Android `MeshDataHandler.kt`
|
||||
|
||||
If we change our local adapter behavior, we should compare against this document first, then update the implementation, not the rules.
|
||||
@@ -35,6 +35,13 @@ class ChatService
|
||||
virtual void onIncomingMessage(const ChatMessage& msg, const RxMeta* rx_meta) = 0;
|
||||
};
|
||||
|
||||
class OutgoingTextObserver
|
||||
{
|
||||
public:
|
||||
virtual ~OutgoingTextObserver() = default;
|
||||
virtual void onOutgoingText(const MeshIncomingText& msg) = 0;
|
||||
};
|
||||
|
||||
ChatService(ChatModel& model,
|
||||
IMeshAdapter& adapter,
|
||||
IChatStore& store,
|
||||
@@ -102,6 +109,8 @@ class ChatService
|
||||
void removeIncomingTextObserver(IncomingTextObserver* observer);
|
||||
void addIncomingMessageObserver(IncomingMessageObserver* observer);
|
||||
void removeIncomingMessageObserver(IncomingMessageObserver* observer);
|
||||
void addOutgoingTextObserver(OutgoingTextObserver* observer);
|
||||
void removeOutgoingTextObserver(OutgoingTextObserver* observer);
|
||||
|
||||
/**
|
||||
* @brief Handle send result (ack/timeout)
|
||||
@@ -142,6 +151,7 @@ class ChatService
|
||||
MeshProtocol active_protocol_ = MeshProtocol::Meshtastic;
|
||||
std::vector<IncomingTextObserver*> incoming_text_observers_;
|
||||
std::vector<IncomingMessageObserver*> incoming_message_observers_;
|
||||
std::vector<OutgoingTextObserver*> outgoing_text_observers_;
|
||||
};
|
||||
|
||||
} // namespace chat
|
||||
|
||||
@@ -269,12 +269,24 @@ void MeshtasticPhoneCore::reset()
|
||||
void MeshtasticPhoneCore::onIncomingText(const chat::MeshIncomingText& msg)
|
||||
{
|
||||
packet_queue_.push_back(buildPacketFromText(msg));
|
||||
logDual("[BLE][mtcore] enqueue text packet id=%08lX from=%08lX to=%08lX len=%u\n",
|
||||
static_cast<unsigned long>(packet_queue_.back().id),
|
||||
static_cast<unsigned long>(packet_queue_.back().from),
|
||||
static_cast<unsigned long>(packet_queue_.back().to),
|
||||
static_cast<unsigned>(packet_queue_.back().decoded.payload.size));
|
||||
notifyFromNum(packet_queue_.back().id);
|
||||
}
|
||||
|
||||
void MeshtasticPhoneCore::onIncomingData(const chat::MeshIncomingData& msg)
|
||||
{
|
||||
packet_queue_.push_back(buildPacketFromData(msg));
|
||||
logDual("[BLE][mtcore] enqueue data packet id=%08lX port=%u req=%08lX from=%08lX to=%08lX len=%u\n",
|
||||
static_cast<unsigned long>(packet_queue_.back().id),
|
||||
static_cast<unsigned>(packet_queue_.back().decoded.portnum),
|
||||
static_cast<unsigned long>(packet_queue_.back().decoded.request_id),
|
||||
static_cast<unsigned long>(packet_queue_.back().from),
|
||||
static_cast<unsigned long>(packet_queue_.back().to),
|
||||
static_cast<unsigned>(packet_queue_.back().decoded.payload.size));
|
||||
notifyFromNum(packet_queue_.back().id);
|
||||
}
|
||||
|
||||
@@ -349,16 +361,6 @@ bool MeshtasticPhoneCore::handleToRadioPacket(meshtastic_MeshPacket& packet)
|
||||
packet.from = ctx_.getSelfNodeId();
|
||||
packet.rx_time = nowSeconds();
|
||||
|
||||
const bool is_broadcast = (packet.to == 0 || packet.to == 0xFFFFFFFFUL);
|
||||
if (is_broadcast)
|
||||
{
|
||||
// Meshtastic broadcast messages should not be modeled as ACKed sends.
|
||||
// Some phone clients set want_ack by default, which leaves the UI waiting
|
||||
// for an ACK that will never exist for broadcast traffic.
|
||||
packet.want_ack = false;
|
||||
packet.decoded.want_response = false;
|
||||
}
|
||||
|
||||
const bool admin_for_self =
|
||||
(packet.decoded.portnum == meshtastic_PortNum_ADMIN_APP) &&
|
||||
(packet.to == 0 || packet.to == ctx_.getSelfNodeId());
|
||||
@@ -1054,6 +1056,10 @@ void MeshtasticPhoneCore::enqueueQueueStatus(uint32_t packet_id, bool ok)
|
||||
status.maxlen = kQueueDepthHint;
|
||||
status.mesh_packet_id = packet_id;
|
||||
queue_status_queue_.push_back(status);
|
||||
logDual("[BLE][mtcore] queue status mesh_packet_id=%08lX ok=%u depth=%u\n",
|
||||
static_cast<unsigned long>(packet_id),
|
||||
ok ? 1U : 0U,
|
||||
static_cast<unsigned>(queue_status_queue_.size()));
|
||||
notifyFromNum(packet_id);
|
||||
}
|
||||
|
||||
@@ -1559,7 +1565,20 @@ meshtastic_MeshPacket MeshtasticPhoneCore::buildPacketFromData(const chat::MeshI
|
||||
packet.from = msg.from;
|
||||
packet.to = msg.to;
|
||||
packet.channel = channelIndexFromId(msg.channel);
|
||||
packet.id = (msg.packet_id == 0) ? static_cast<uint32_t>(millis()) : msg.packet_id;
|
||||
if (msg.packet_id != 0)
|
||||
{
|
||||
packet.id = msg.packet_id;
|
||||
}
|
||||
else if (msg.portnum == meshtastic_PortNum_ROUTING_APP && msg.request_id != 0)
|
||||
{
|
||||
// Keep synthetic routing/ack packets tied to the original request ID so
|
||||
// Meshtastic phone clients can correlate them with the pending send.
|
||||
packet.id = msg.request_id;
|
||||
}
|
||||
else
|
||||
{
|
||||
packet.id = static_cast<uint32_t>(millis());
|
||||
}
|
||||
packet.rx_time = (msg.rx_meta.rx_timestamp_s != 0) ? msg.rx_meta.rx_timestamp_s : nowSeconds();
|
||||
packet.rx_snr = msg.rx_meta.snr_db_x10 / 10.0f;
|
||||
packet.rx_rssi = msg.rx_meta.rssi_dbm_x10 / 10;
|
||||
|
||||
@@ -77,6 +77,27 @@ MessageId ChatService::sendTextWithId(ChannelId channel, const std::string& text
|
||||
// Store message
|
||||
store_.append(msg);
|
||||
|
||||
if (queued && msg_id != 0)
|
||||
{
|
||||
MeshIncomingText outgoing{};
|
||||
outgoing.channel = channel;
|
||||
outgoing.from = adapter_.getNodeId();
|
||||
outgoing.to = (peer != 0) ? peer : 0xFFFFFFFFUL;
|
||||
outgoing.msg_id = msg_id;
|
||||
outgoing.timestamp = msg.timestamp;
|
||||
outgoing.text = text;
|
||||
outgoing.hop_limit = 0;
|
||||
outgoing.encrypted = false;
|
||||
|
||||
for (auto* observer : outgoing_text_observers_)
|
||||
{
|
||||
if (observer)
|
||||
{
|
||||
observer->onOutgoingText(outgoing);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return msg.msg_id;
|
||||
}
|
||||
|
||||
@@ -239,6 +260,38 @@ void ChatService::removeIncomingMessageObserver(IncomingMessageObserver* observe
|
||||
}
|
||||
}
|
||||
|
||||
void ChatService::addOutgoingTextObserver(OutgoingTextObserver* observer)
|
||||
{
|
||||
if (!observer)
|
||||
{
|
||||
return;
|
||||
}
|
||||
for (auto* existing : outgoing_text_observers_)
|
||||
{
|
||||
if (existing == observer)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
outgoing_text_observers_.push_back(observer);
|
||||
}
|
||||
|
||||
void ChatService::removeOutgoingTextObserver(OutgoingTextObserver* observer)
|
||||
{
|
||||
if (!observer)
|
||||
{
|
||||
return;
|
||||
}
|
||||
for (auto it = outgoing_text_observers_.begin(); it != outgoing_text_observers_.end(); ++it)
|
||||
{
|
||||
if (*it == observer)
|
||||
{
|
||||
outgoing_text_observers_.erase(it);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ChatService::handleSendResult(MessageId msg_id, bool ok)
|
||||
{
|
||||
if (msg_id == 0) return;
|
||||
|
||||
@@ -5176,8 +5176,9 @@ void Runtime::executeNodeAction()
|
||||
}
|
||||
if (!node->has_public_key)
|
||||
{
|
||||
appendBootLog("verify no pubkey");
|
||||
showTransientPopup("KEY VERIFICATION", "NO PUBLIC KEY");
|
||||
appendBootLog("verify req nodeinfo");
|
||||
const bool requested = mesh->requestNodeInfo(node->node_id, true);
|
||||
showTransientPopup("KEY VERIFICATION", requested ? "REQUESTING NODEINFO" : "NO PUBLIC KEY");
|
||||
return;
|
||||
}
|
||||
const bool ok = mesh->startKeyVerification(node->node_id);
|
||||
@@ -5195,8 +5196,9 @@ void Runtime::executeNodeAction()
|
||||
}
|
||||
if (!node->has_public_key)
|
||||
{
|
||||
appendBootLog("key trust no pubkey");
|
||||
showTransientPopup("TRUST KEY", "NO PUBLIC KEY");
|
||||
appendBootLog("key trust req nodeinfo");
|
||||
const bool requested = mesh->requestNodeInfo(node->node_id, true);
|
||||
showTransientPopup("TRUST KEY", requested ? "REQUESTING NODEINFO" : "NO PUBLIC KEY");
|
||||
return;
|
||||
}
|
||||
const bool trusted = !node->key_manually_verified;
|
||||
|
||||
@@ -83,6 +83,7 @@ class UiController : public IChatUiRuntime
|
||||
void applyConversationListToUi();
|
||||
void updateConversationMetaForMessage(const chat::ChatMessage& msg, bool increment_unread);
|
||||
bool updateConversationViewForIncoming(const chat::ChatMessage& msg);
|
||||
void reloadConversationView();
|
||||
void refreshTeamConversation();
|
||||
void startTeamConversationTimer();
|
||||
void stopTeamConversationTimer();
|
||||
|
||||
@@ -425,6 +425,7 @@ void UiController::onChatEvent(sys::Event* event)
|
||||
if (is_current_conversation)
|
||||
{
|
||||
(void)updateConversationViewForIncoming(*latest);
|
||||
reloadConversationView();
|
||||
service_.markConversationRead(current_conv_);
|
||||
}
|
||||
else
|
||||
@@ -444,13 +445,7 @@ void UiController::onChatEvent(sys::Event* event)
|
||||
const ChatMessage* msg = service_.getMessage(result_event->msg_id);
|
||||
if (!msg || !conversation_->updateMessageStatus(result_event->msg_id, msg->status))
|
||||
{
|
||||
auto messages = service_.getRecentMessages(current_conv_, 50);
|
||||
conversation_->clearMessages();
|
||||
for (const auto& m : messages)
|
||||
{
|
||||
conversation_->addMessage(m);
|
||||
}
|
||||
conversation_->scrollToBottom();
|
||||
reloadConversationView();
|
||||
}
|
||||
}
|
||||
(void)result_event;
|
||||
@@ -912,6 +907,22 @@ bool UiController::updateConversationViewForIncoming(const chat::ChatMessage& ms
|
||||
return true;
|
||||
}
|
||||
|
||||
void UiController::reloadConversationView()
|
||||
{
|
||||
if (!conversation_ || team_conv_active_)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
auto messages = service_.getRecentMessages(current_conv_, 50);
|
||||
conversation_->clearMessages();
|
||||
for (const auto& msg : messages)
|
||||
{
|
||||
conversation_->addMessage(msg);
|
||||
}
|
||||
conversation_->scrollToBottom();
|
||||
}
|
||||
|
||||
bool UiController::isTeamConversation(const chat::ConversationId& conv) const
|
||||
{
|
||||
return isTeamConversationId(conv);
|
||||
|
||||
@@ -2087,7 +2087,8 @@ static void start_selected_node_key_verification()
|
||||
}
|
||||
if (!node->has_public_key)
|
||||
{
|
||||
::ui::SystemNotification::show("No public key yet", 1800);
|
||||
const bool requested = mesh->requestNodeInfo(node->node_id, true);
|
||||
::ui::SystemNotification::show(requested ? "Requesting node info" : "No public key yet", 1800);
|
||||
contacts_focus_to_list();
|
||||
return;
|
||||
}
|
||||
@@ -2100,7 +2101,9 @@ static void start_selected_node_key_verification()
|
||||
static void toggle_selected_node_key_trust()
|
||||
{
|
||||
const auto* node = get_selected_node();
|
||||
if (!node || !g_contacts_state.contact_service)
|
||||
app::IAppFacade& app_ctx = app::appFacade();
|
||||
chat::IMeshAdapter* mesh = app_ctx.getMeshAdapter();
|
||||
if (!node || !g_contacts_state.contact_service || !mesh)
|
||||
{
|
||||
::ui::SystemNotification::show("Key trust unavailable", 1800);
|
||||
contacts_focus_to_list();
|
||||
@@ -2108,7 +2111,8 @@ static void toggle_selected_node_key_trust()
|
||||
}
|
||||
if (!node->has_public_key)
|
||||
{
|
||||
::ui::SystemNotification::show("No public key yet", 1800);
|
||||
const bool requested = mesh->requestNodeInfo(node->node_id, true);
|
||||
::ui::SystemNotification::show(requested ? "Requesting node info" : "No public key yet", 1800);
|
||||
contacts_focus_to_list();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ namespace ble
|
||||
|
||||
class MeshtasticBleService : public BleService,
|
||||
public chat::ChatService::IncomingTextObserver,
|
||||
public chat::ChatService::OutgoingTextObserver,
|
||||
public team::TeamService::IncomingDataObserver,
|
||||
public MeshtasticPhoneTransport,
|
||||
public MeshtasticPhoneHooks
|
||||
@@ -39,6 +40,7 @@ class MeshtasticBleService : public BleService,
|
||||
void update() override;
|
||||
|
||||
void onIncomingText(const chat::MeshIncomingText& msg) override;
|
||||
void onOutgoingText(const chat::MeshIncomingText& msg) override;
|
||||
void onIncomingData(const chat::MeshIncomingData& msg) override;
|
||||
bool isBleConnected() const override;
|
||||
void notifyFromNum(uint32_t value) override;
|
||||
|
||||
+3
@@ -30,6 +30,9 @@ class MeshAdapterRouter : public IMeshAdapter
|
||||
MeshCapabilities getCapabilities() const override;
|
||||
bool sendText(ChannelId channel, const std::string& text,
|
||||
MessageId* out_msg_id, NodeId peer = 0) override;
|
||||
bool sendTextWithId(ChannelId channel, const std::string& text,
|
||||
MessageId forced_msg_id,
|
||||
MessageId* out_msg_id, NodeId peer = 0) override;
|
||||
bool pollIncomingText(MeshIncomingText* out) override;
|
||||
bool sendAppData(ChannelId channel, uint32_t portnum,
|
||||
const uint8_t* payload, size_t len,
|
||||
|
||||
+3
@@ -50,6 +50,9 @@ class MtAdapter : public chat::IMeshAdapter
|
||||
|
||||
bool sendText(ChannelId channel, const std::string& text,
|
||||
MessageId* out_msg_id, NodeId peer = 0) override;
|
||||
bool sendTextWithId(ChannelId channel, const std::string& text,
|
||||
MessageId forced_msg_id,
|
||||
MessageId* out_msg_id, NodeId peer = 0) override;
|
||||
bool pollIncomingText(MeshIncomingText* out) override;
|
||||
bool sendAppData(ChannelId channel, uint32_t portnum,
|
||||
const uint8_t* payload, size_t len,
|
||||
|
||||
@@ -456,6 +456,7 @@ void MeshtasticBleService::start()
|
||||
startAdvertising();
|
||||
|
||||
ctx_.getChatService().addIncomingTextObserver(this);
|
||||
ctx_.getChatService().addOutgoingTextObserver(this);
|
||||
if (auto* team = ctx_.getTeamService())
|
||||
{
|
||||
team->addIncomingDataObserver(this);
|
||||
@@ -467,6 +468,7 @@ void MeshtasticBleService::start()
|
||||
void MeshtasticBleService::stop()
|
||||
{
|
||||
ctx_.getChatService().removeIncomingTextObserver(this);
|
||||
ctx_.getChatService().removeOutgoingTextObserver(this);
|
||||
if (auto* team = ctx_.getTeamService())
|
||||
{
|
||||
team->removeIncomingDataObserver(this);
|
||||
@@ -523,6 +525,12 @@ void MeshtasticBleService::update()
|
||||
device_name_.c_str());
|
||||
refreshBatteryLevel(true);
|
||||
syncMqttProxySettings();
|
||||
if (phone_session_)
|
||||
{
|
||||
// Drain mesh adapter app-data events (including synthetic ROUTING_APP
|
||||
// ACK/NAK results) into the phone session before BLE reads them.
|
||||
phone_session_->pumpIncomingAppData();
|
||||
}
|
||||
handleFromPhone();
|
||||
handleToPhone();
|
||||
}
|
||||
@@ -540,6 +548,23 @@ void MeshtasticBleService::onIncomingText(const chat::MeshIncomingText& msg)
|
||||
}
|
||||
}
|
||||
|
||||
void MeshtasticBleService::onOutgoingText(const chat::MeshIncomingText& msg)
|
||||
{
|
||||
if (phone_session_)
|
||||
{
|
||||
ble_log("local text mirror id=%lu from=%08lX to=%08lX len=%u",
|
||||
static_cast<unsigned long>(msg.msg_id),
|
||||
static_cast<unsigned long>(msg.from),
|
||||
static_cast<unsigned long>(msg.to),
|
||||
static_cast<unsigned>(msg.text.size()));
|
||||
phone_session_->onIncomingText(msg);
|
||||
if (phone_session_->isSendingPackets())
|
||||
{
|
||||
notifyFromNum(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MeshtasticBleService::onIncomingData(const chat::MeshIncomingData& msg)
|
||||
{
|
||||
if (phone_session_)
|
||||
|
||||
@@ -81,6 +81,14 @@ bool MeshAdapterRouter::sendText(ChannelId channel, const std::string& text,
|
||||
return lock.locked() && core_.sendText(channel, text, out_msg_id, peer);
|
||||
}
|
||||
|
||||
bool MeshAdapterRouter::sendTextWithId(ChannelId channel, const std::string& text,
|
||||
MessageId forced_msg_id,
|
||||
MessageId* out_msg_id, NodeId peer)
|
||||
{
|
||||
LockGuard lock(mutex_);
|
||||
return lock.locked() && core_.sendTextWithId(channel, text, forced_msg_id, out_msg_id, peer);
|
||||
}
|
||||
|
||||
bool MeshAdapterRouter::pollIncomingText(MeshIncomingText* out)
|
||||
{
|
||||
LockGuard lock(mutex_);
|
||||
|
||||
@@ -111,6 +111,16 @@ static const char* portName(uint32_t portnum)
|
||||
}
|
||||
}
|
||||
|
||||
void mt_diag_log(const char* fmt, ...)
|
||||
{
|
||||
char buf[192] = {};
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
vsnprintf(buf, sizeof(buf), fmt, args);
|
||||
va_end(args);
|
||||
Serial.print(buf);
|
||||
}
|
||||
|
||||
using chat::meshtastic::computeChannelHash;
|
||||
using chat::meshtastic::expandShortPsk;
|
||||
using chat::meshtastic::hasValidPosition;
|
||||
@@ -280,6 +290,13 @@ MtAdapter::~MtAdapter()
|
||||
|
||||
bool MtAdapter::sendText(ChannelId channel, const std::string& text,
|
||||
MessageId* out_msg_id, NodeId peer)
|
||||
{
|
||||
return sendTextWithId(channel, text, 0, out_msg_id, peer);
|
||||
}
|
||||
|
||||
bool MtAdapter::sendTextWithId(ChannelId channel, const std::string& text,
|
||||
MessageId forced_msg_id,
|
||||
MessageId* out_msg_id, NodeId peer)
|
||||
{
|
||||
if (!ready_ || text.empty() || !config_.tx_enabled)
|
||||
{
|
||||
@@ -296,12 +313,25 @@ bool MtAdapter::sendText(ChannelId channel, const std::string& text,
|
||||
pending.channel = out_channel;
|
||||
pending.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP;
|
||||
pending.text = text;
|
||||
pending.msg_id = next_packet_id_++;
|
||||
pending.msg_id = (forced_msg_id != 0) ? forced_msg_id : next_packet_id_++;
|
||||
if (forced_msg_id != 0 && forced_msg_id >= next_packet_id_)
|
||||
{
|
||||
next_packet_id_ = forced_msg_id + 1;
|
||||
if (next_packet_id_ == 0)
|
||||
{
|
||||
next_packet_id_ = 1;
|
||||
}
|
||||
}
|
||||
pending.dest = (peer != 0) ? peer : 0xFFFFFFFF;
|
||||
pending.retry_count = 0;
|
||||
pending.last_attempt = 0;
|
||||
|
||||
send_queue_.push(pending);
|
||||
mt_diag_log("[MT][TX] queue text id=%08lX dest=%08lX ch=%u len=%u\n",
|
||||
static_cast<unsigned long>(pending.msg_id),
|
||||
static_cast<unsigned long>(pending.dest),
|
||||
static_cast<unsigned>(out_channel),
|
||||
static_cast<unsigned>(text.size()));
|
||||
LORA_LOG("[LORA] queue text ch=%u len=%u id=%lu\n",
|
||||
static_cast<unsigned>(channel),
|
||||
static_cast<unsigned>(text.size()),
|
||||
@@ -451,6 +481,14 @@ bool MtAdapter::sendAppData(ChannelId channel, uint32_t portnum,
|
||||
#endif
|
||||
|
||||
bool ok = (state == RADIOLIB_ERR_NONE);
|
||||
mt_diag_log("[MT][TX] app id=%08lX dest=%08lX port=%u ok=%u air_ack=%u track_ack=%u len=%u\n",
|
||||
static_cast<unsigned long>(msg_id),
|
||||
static_cast<unsigned long>(dest_node),
|
||||
static_cast<unsigned>(portnum),
|
||||
ok ? 1U : 0U,
|
||||
air_want_ack ? 1U : 0U,
|
||||
track_ack ? 1U : 0U,
|
||||
static_cast<unsigned>(wire_size));
|
||||
LORA_LOG("[LORA] TX app port=%u len=%u want_resp=%u air_ack=%u track_ack=%u ok=%d\n",
|
||||
(unsigned)portnum,
|
||||
(unsigned)wire_size,
|
||||
@@ -1390,22 +1428,33 @@ void MtAdapter::processReceivedPacket(const uint8_t* data, size_t size)
|
||||
rx_meta.sf = radio_sf_;
|
||||
rx_meta.cr = radio_cr_;
|
||||
|
||||
mt_diag_log("[MT][RX] from=%08lX to=%08lX id=%08lX flags=0x%02X ch=%u next=%u relay=%u len=%u\n",
|
||||
static_cast<unsigned long>(header.from),
|
||||
static_cast<unsigned long>(header.to),
|
||||
static_cast<unsigned long>(header.id),
|
||||
static_cast<unsigned>(header.flags),
|
||||
static_cast<unsigned>(header.channel),
|
||||
static_cast<unsigned>(header.next_hop),
|
||||
static_cast<unsigned>(header.relay_node),
|
||||
static_cast<unsigned>(payload_size));
|
||||
|
||||
if (header.from == node_id_)
|
||||
{
|
||||
auto pending_it = pending_ack_ms_.find(header.id);
|
||||
if (header.to == 0xFFFFFFFF && pending_it != pending_ack_ms_.end())
|
||||
if (header.to == kBroadcastNodeId && pending_it != pending_ack_ms_.end())
|
||||
{
|
||||
ChannelId channel_id =
|
||||
(header.channel == secondary_channel_hash_) ? ChannelId::SECONDARY : ChannelId::PRIMARY;
|
||||
uint32_t ack_from = (header.relay_node != 0) ? header.relay_node : node_id_;
|
||||
LORA_LOG("[LORA] RX implicit ack via rebroadcast req=%08lX relay=%08lX\n",
|
||||
(unsigned long)header.id,
|
||||
(unsigned long)ack_from);
|
||||
mt_diag_log("[MT][IMPLICIT_ACK] observed self-broadcast id=%08lX relay=%08lX next=%08lX ch=%u\n",
|
||||
static_cast<unsigned long>(header.id),
|
||||
static_cast<unsigned long>(header.relay_node),
|
||||
static_cast<unsigned long>(header.next_hop),
|
||||
static_cast<unsigned>(header.channel));
|
||||
pending_ack_ms_.erase(pending_it);
|
||||
pending_ack_dest_.erase(header.id);
|
||||
emitRoutingResultToPhone(header.id,
|
||||
meshtastic_Routing_Error_NONE,
|
||||
ack_from,
|
||||
node_id_,
|
||||
node_id_,
|
||||
channel_id,
|
||||
header.channel,
|
||||
@@ -1413,7 +1462,11 @@ void MtAdapter::processReceivedPacket(const uint8_t* data, size_t size)
|
||||
return;
|
||||
}
|
||||
|
||||
LORA_LOG("[LORA] RX self drop id=%08lX\n", (unsigned long)header.id);
|
||||
LORA_LOG("[LORA] RX self drop id=%08lX to=%08lX relay=%08lX ch=%u\n",
|
||||
static_cast<unsigned long>(header.id),
|
||||
static_cast<unsigned long>(header.to),
|
||||
static_cast<unsigned long>(header.relay_node),
|
||||
static_cast<unsigned>(header.channel));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1779,6 +1832,11 @@ void MtAdapter::processReceivedPacket(const uint8_t* data, size_t size)
|
||||
}
|
||||
pending_ack_ms_.erase(decoded.request_id);
|
||||
pending_ack_dest_.erase(decoded.request_id);
|
||||
mt_diag_log("[MT][ACK] req=%08lX from=%08lX reason=%u ok=%u\n",
|
||||
static_cast<unsigned long>(decoded.request_id),
|
||||
static_cast<unsigned long>(header.from),
|
||||
static_cast<unsigned>(routing.error_reason),
|
||||
ok ? 1U : 0U);
|
||||
LORA_LOG("[LORA] RX ack reason=%u (%s)\n",
|
||||
static_cast<unsigned>(routing.error_reason),
|
||||
routingErrorName(routing.error_reason));
|
||||
@@ -2030,8 +2088,15 @@ void MtAdapter::processSendQueue()
|
||||
{
|
||||
if (now - it->second >= ACK_TIMEOUT_MS)
|
||||
{
|
||||
LORA_LOG("[LORA] RX ack timeout req=%08lX\n",
|
||||
(unsigned long)it->first);
|
||||
const auto dest_it = pending_ack_dest_.find(it->first);
|
||||
const uint32_t dest = (dest_it != pending_ack_dest_.end()) ? dest_it->second : 0;
|
||||
mt_diag_log("[MT][ACK_TIMEOUT] req=%08lX dest=%08lX age_ms=%lu\n",
|
||||
static_cast<unsigned long>(it->first),
|
||||
static_cast<unsigned long>(dest),
|
||||
static_cast<unsigned long>(now - it->second));
|
||||
LORA_LOG("[LORA] RX ack timeout req=%08lX dest=%08lX\n",
|
||||
static_cast<unsigned long>(it->first),
|
||||
static_cast<unsigned long>(dest));
|
||||
pending_ack_dest_.erase(it->first);
|
||||
emitRoutingResultToPhone(it->first,
|
||||
meshtastic_Routing_Error_MAX_RETRANSMIT,
|
||||
@@ -3713,6 +3778,12 @@ void MtAdapter::emitRoutingResultToPhone(uint32_t request_id,
|
||||
return;
|
||||
}
|
||||
|
||||
mt_diag_log("[MT][ACK->BLE] req=%08lX from=%08lX to=%08lX reason=%u\n",
|
||||
static_cast<unsigned long>(request_id),
|
||||
static_cast<unsigned long>(from),
|
||||
static_cast<unsigned long>(to),
|
||||
static_cast<unsigned>(reason));
|
||||
|
||||
meshtastic_Routing routing = meshtastic_Routing_init_default;
|
||||
routing.which_variant = meshtastic_Routing_error_reason_tag;
|
||||
routing.error_reason = reason;
|
||||
|
||||
@@ -406,12 +406,18 @@ void wakeScreenSaver()
|
||||
return;
|
||||
}
|
||||
|
||||
bool was_sleeping = false;
|
||||
if (s_activity_mutex != nullptr)
|
||||
{
|
||||
if (xSemaphoreTake(s_activity_mutex, pdMS_TO_TICKS(10)) == pdTRUE)
|
||||
{
|
||||
was_sleeping = s_screen_sleeping;
|
||||
// Treat the wake gesture as real activity so the background sleep
|
||||
// task doesn't immediately force the panel back off before the
|
||||
// 3-second saver window expires.
|
||||
s_last_user_activity_time = millis();
|
||||
s_screen_saver_active = true;
|
||||
s_screen_sleeping = true;
|
||||
s_screen_sleeping = false;
|
||||
xSemaphoreGive(s_activity_mutex);
|
||||
}
|
||||
}
|
||||
@@ -420,8 +426,20 @@ void wakeScreenSaver()
|
||||
lv_obj_clear_flag(s_screen_saver_layer, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_move_foreground(s_screen_saver_layer);
|
||||
lv_refr_now(nullptr);
|
||||
s_saved_screen_brightness = board.getBrightness();
|
||||
board.setBrightness(s_saved_screen_brightness);
|
||||
if (was_sleeping)
|
||||
{
|
||||
board.exitScreenSleep();
|
||||
board.setBrightness(s_saved_screen_brightness);
|
||||
if (board.hasKeyboard())
|
||||
{
|
||||
board.keyboardSetBrightness(s_saved_keyboard_brightness);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
s_saved_screen_brightness = board.getBrightness();
|
||||
board.setBrightness(s_saved_screen_brightness);
|
||||
}
|
||||
|
||||
if (s_screen_saver_timer == nullptr)
|
||||
{
|
||||
@@ -520,6 +538,7 @@ void updateUserActivity()
|
||||
{
|
||||
bool woke_from_sleep = false;
|
||||
bool hide_saver = false;
|
||||
bool restore_sleep_state = false;
|
||||
if (s_activity_mutex != nullptr)
|
||||
{
|
||||
if (xSemaphoreTake(s_activity_mutex, pdMS_TO_TICKS(10)) == pdTRUE)
|
||||
@@ -533,12 +552,8 @@ void updateUserActivity()
|
||||
if (s_screen_sleeping)
|
||||
{
|
||||
s_screen_sleeping = false;
|
||||
board.setBrightness(s_saved_screen_brightness);
|
||||
if (board.hasKeyboard())
|
||||
{
|
||||
board.keyboardSetBrightness(s_saved_keyboard_brightness);
|
||||
}
|
||||
woke_from_sleep = true;
|
||||
restore_sleep_state = true;
|
||||
}
|
||||
xSemaphoreGive(s_activity_mutex);
|
||||
}
|
||||
@@ -547,6 +562,15 @@ void updateUserActivity()
|
||||
{
|
||||
hide_screen_saver_layer();
|
||||
}
|
||||
if (restore_sleep_state)
|
||||
{
|
||||
board.exitScreenSleep();
|
||||
board.setBrightness(s_saved_screen_brightness);
|
||||
if (board.hasKeyboard())
|
||||
{
|
||||
board.keyboardSetBrightness(s_saved_keyboard_brightness);
|
||||
}
|
||||
}
|
||||
if (woke_from_sleep)
|
||||
{
|
||||
notifyWakeFromSleep();
|
||||
|
||||
@@ -27,6 +27,7 @@ class MeshtasticPhoneCore;
|
||||
|
||||
class MeshtasticBleService final : public BleService,
|
||||
public chat::ChatService::IncomingTextObserver,
|
||||
public chat::ChatService::OutgoingTextObserver,
|
||||
public MeshtasticPhoneTransport,
|
||||
public MeshtasticPhoneHooks
|
||||
{
|
||||
@@ -38,6 +39,7 @@ class MeshtasticBleService final : public BleService,
|
||||
void stop() override;
|
||||
void update() override;
|
||||
void onIncomingText(const chat::MeshIncomingText& msg) override;
|
||||
void onOutgoingText(const chat::MeshIncomingText& msg) override;
|
||||
bool handleToRadio(const uint8_t* data, size_t len);
|
||||
bool popToPhone(MeshtasticBleFrame* out);
|
||||
void handleConnectEvent(uint16_t conn_handle);
|
||||
|
||||
@@ -273,6 +273,7 @@ void MeshtasticBleService::start()
|
||||
log_radio_.begin();
|
||||
|
||||
ctx_.getChatService().addIncomingTextObserver(this);
|
||||
ctx_.getChatService().addOutgoingTextObserver(this);
|
||||
startAdvertising(service_);
|
||||
active_ = true;
|
||||
pending_passkey_.store(0);
|
||||
@@ -282,6 +283,7 @@ void MeshtasticBleService::start()
|
||||
void MeshtasticBleService::stop()
|
||||
{
|
||||
ctx_.getChatService().removeIncomingTextObserver(this);
|
||||
ctx_.getChatService().removeOutgoingTextObserver(this);
|
||||
disconnectAll();
|
||||
Bluefruit.Advertising.stop();
|
||||
if (phone_session_)
|
||||
@@ -325,6 +327,19 @@ void MeshtasticBleService::onIncomingText(const chat::MeshIncomingText& msg)
|
||||
}
|
||||
}
|
||||
|
||||
void MeshtasticBleService::onOutgoingText(const chat::MeshIncomingText& msg)
|
||||
{
|
||||
if (phone_session_)
|
||||
{
|
||||
Serial2.printf("[BLE][nrf52][mt] local text mirror id=%08lX from=%08lX to=%08lX len=%u\n",
|
||||
static_cast<unsigned long>(msg.msg_id),
|
||||
static_cast<unsigned long>(msg.from),
|
||||
static_cast<unsigned long>(msg.to),
|
||||
static_cast<unsigned>(msg.text.size()));
|
||||
phone_session_->onIncomingText(msg);
|
||||
}
|
||||
}
|
||||
|
||||
bool MeshtasticBleService::handleToRadio(const uint8_t* data, size_t len)
|
||||
{
|
||||
Serial2.printf("[BLE][nrf52][mt] handleToRadio len=%u connected=%u\n",
|
||||
|
||||
+26
-6
@@ -554,7 +554,7 @@ bool MeshtasticRadioAdapter::sendTextWithId(::chat::ChannelId channel, const std
|
||||
const uint8_t channel_hash = ::chat::meshtastic::computeChannelHash(channelNameFor(config_, out_channel),
|
||||
key,
|
||||
key_len);
|
||||
const bool track_ack = (dest != kBroadcastNode);
|
||||
const bool track_ack = true;
|
||||
const bool air_want_ack = shouldSetAirWantAck(dest, track_ack);
|
||||
|
||||
uint8_t wire[384] = {};
|
||||
@@ -587,7 +587,7 @@ bool MeshtasticRadioAdapter::sendTextWithId(::chat::ChannelId channel, const std
|
||||
std::memcpy(mqtt_data.payload.bytes, text.data(), mqtt_data.payload.size);
|
||||
}
|
||||
|
||||
if (!transmitPreparedWire(wire, wire_size, out_channel, &mqtt_data, true, true, 0, true))
|
||||
if (!transmitPreparedWire(wire, wire_size, out_channel, &mqtt_data, track_ack, true, 0, true))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -665,7 +665,7 @@ bool MeshtasticRadioAdapter::sendAppData(::chat::ChannelId channel, uint32_t por
|
||||
const uint8_t* wire_payload = data_pb;
|
||||
size_t wire_payload_len = data_pb_size;
|
||||
bool use_pki = false;
|
||||
bool track_ack = want_ack && !is_broadcast;
|
||||
bool track_ack = want_ack;
|
||||
if (wire_dest != kBroadcastNode && pki_enabled_)
|
||||
{
|
||||
if (!pki_ready_ || !allowPkiForPortnum(portnum) || !hasPkiKey(wire_dest))
|
||||
@@ -1056,6 +1056,11 @@ void MeshtasticRadioAdapter::handleRawPacket(const uint8_t* data, size_t size)
|
||||
const auto pending_it = pending_retransmits_.find(pendingKey(header.from, header.id));
|
||||
if (is_broadcast && pending_it != pending_retransmits_.end())
|
||||
{
|
||||
logMeshtasticRx("[gat562][mt] implicit-ack observed self-broadcast id=%08lX relay=%u next=%u ch=%u\n",
|
||||
static_cast<unsigned long>(header.id),
|
||||
static_cast<unsigned>(header.relay_node),
|
||||
static_cast<unsigned>(header.next_hop),
|
||||
static_cast<unsigned>(header.channel));
|
||||
::chat::RxMeta implicit_rx{};
|
||||
implicit_rx.rx_timestamp_ms = millis();
|
||||
implicit_rx.rx_timestamp_s = nowSeconds();
|
||||
@@ -1064,12 +1069,10 @@ void MeshtasticRadioAdapter::handleRawPacket(const uint8_t* data, size_t size)
|
||||
implicit_rx.channel_hash = header.channel;
|
||||
implicit_rx.next_hop = header.next_hop;
|
||||
implicit_rx.relay_node = header.relay_node;
|
||||
const ::chat::NodeId ack_from =
|
||||
(header.relay_node != 0) ? static_cast<::chat::NodeId>(header.relay_node) : node_id_;
|
||||
pending_retransmits_.erase(pending_it);
|
||||
emitRoutingResult(header.id,
|
||||
meshtastic_Routing_Error_NONE,
|
||||
ack_from,
|
||||
node_id_,
|
||||
node_id_,
|
||||
channel,
|
||||
header.channel,
|
||||
@@ -1465,6 +1468,15 @@ void MeshtasticRadioAdapter::processSendQueue()
|
||||
auto* header = reinterpret_cast<::chat::meshtastic::PacketHeaderWire*>(pending.wire.data());
|
||||
if (pending.retries_left == 0)
|
||||
{
|
||||
if (pending.observe_only)
|
||||
{
|
||||
logMeshtasticRx("[gat562][mt] observe timeout id=%08lX dest=%08lX ch=%u local=%u want_ack=%u\n",
|
||||
static_cast<unsigned long>(pending.packet_id),
|
||||
static_cast<unsigned long>(pending.dest),
|
||||
static_cast<unsigned>(pending.channel),
|
||||
pending.local_origin ? 1U : 0U,
|
||||
pending.want_ack ? 1U : 0U);
|
||||
}
|
||||
if (pending.local_origin && pending.want_ack)
|
||||
{
|
||||
emitRoutingResult(pending.packet_id,
|
||||
@@ -2243,6 +2255,14 @@ void MeshtasticRadioAdapter::queuePendingRetransmit(const ::chat::meshtastic::Pa
|
||||
: (pending.want_ack ? kDefaultAckRetries : kDefaultNextHopRetries);
|
||||
pending.next_tx_ms = millis() + kRetransmitIntervalMs;
|
||||
}
|
||||
logMeshtasticRx("[gat562][mt] watch pending id=%08lX from=%08lX dest=%08lX observe=%u local=%u want_ack=%u next=%lu\n",
|
||||
static_cast<unsigned long>(pending.packet_id),
|
||||
static_cast<unsigned long>(pending.original_from),
|
||||
static_cast<unsigned long>(pending.dest),
|
||||
pending.observe_only ? 1U : 0U,
|
||||
pending.local_origin ? 1U : 0U,
|
||||
pending.want_ack ? 1U : 0U,
|
||||
static_cast<unsigned long>(pending.next_tx_ms));
|
||||
pending_retransmits_[pendingKey(header.from, header.id)] = std::move(pending);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user