diff --git a/docs/MESHTASTIC_PHONE_PROTOCOL_REFERENCE.md b/docs/MESHTASTIC_PHONE_PROTOCOL_REFERENCE.md new file mode 100644 index 00000000..5388833a --- /dev/null +++ b/docs/MESHTASTIC_PHONE_PROTOCOL_REFERENCE.md @@ -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. diff --git a/modules/core_chat/include/chat/usecase/chat_service.h b/modules/core_chat/include/chat/usecase/chat_service.h index 5cb05da9..5d12c2e4 100644 --- a/modules/core_chat/include/chat/usecase/chat_service.h +++ b/modules/core_chat/include/chat/usecase/chat_service.h @@ -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 incoming_text_observers_; std::vector incoming_message_observers_; + std::vector outgoing_text_observers_; }; } // namespace chat diff --git a/modules/core_chat/src/ble/meshtastic_phone_core.cpp b/modules/core_chat/src/ble/meshtastic_phone_core.cpp index 9b0f982d..add88957 100644 --- a/modules/core_chat/src/ble/meshtastic_phone_core.cpp +++ b/modules/core_chat/src/ble/meshtastic_phone_core.cpp @@ -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(packet_queue_.back().id), + static_cast(packet_queue_.back().from), + static_cast(packet_queue_.back().to), + static_cast(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(packet_queue_.back().id), + static_cast(packet_queue_.back().decoded.portnum), + static_cast(packet_queue_.back().decoded.request_id), + static_cast(packet_queue_.back().from), + static_cast(packet_queue_.back().to), + static_cast(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(packet_id), + ok ? 1U : 0U, + static_cast(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(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(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; diff --git a/modules/core_chat/src/usecase/chat_service.cpp b/modules/core_chat/src/usecase/chat_service.cpp index c5121616..04400287 100644 --- a/modules/core_chat/src/usecase/chat_service.cpp +++ b/modules/core_chat/src/usecase/chat_service.cpp @@ -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; diff --git a/modules/ui_mono_128x64/src/runtime.cpp b/modules/ui_mono_128x64/src/runtime.cpp index e685aef6..5f552740 100644 --- a/modules/ui_mono_128x64/src/runtime.cpp +++ b/modules/ui_mono_128x64/src/runtime.cpp @@ -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; diff --git a/modules/ui_shared/include/ui/screens/chat/chat_ui_controller.h b/modules/ui_shared/include/ui/screens/chat/chat_ui_controller.h index fafa6522..29d5784e 100644 --- a/modules/ui_shared/include/ui/screens/chat/chat_ui_controller.h +++ b/modules/ui_shared/include/ui/screens/chat/chat_ui_controller.h @@ -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(); diff --git a/modules/ui_shared/src/ui/screens/chat/chat_ui_controller.cpp b/modules/ui_shared/src/ui/screens/chat/chat_ui_controller.cpp index 4d7c5660..89bbac38 100644 --- a/modules/ui_shared/src/ui/screens/chat/chat_ui_controller.cpp +++ b/modules/ui_shared/src/ui/screens/chat/chat_ui_controller.cpp @@ -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); diff --git a/modules/ui_shared/src/ui/screens/contacts/contacts_page_components.cpp b/modules/ui_shared/src/ui/screens/contacts/contacts_page_components.cpp index 21efc36f..e0e309e8 100644 --- a/modules/ui_shared/src/ui/screens/contacts/contacts_page_components.cpp +++ b/modules/ui_shared/src/ui/screens/contacts/contacts_page_components.cpp @@ -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; } diff --git a/platform/esp/arduino_common/include/ble/meshtastic_ble.h b/platform/esp/arduino_common/include/ble/meshtastic_ble.h index 46fcdd05..39e8c431 100644 --- a/platform/esp/arduino_common/include/ble/meshtastic_ble.h +++ b/platform/esp/arduino_common/include/ble/meshtastic_ble.h @@ -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; diff --git a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/mesh_adapter_router.h b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/mesh_adapter_router.h index 47aa0d7f..89af1db5 100644 --- a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/mesh_adapter_router.h +++ b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/mesh_adapter_router.h @@ -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, diff --git a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/meshtastic/mt_adapter.h b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/meshtastic/mt_adapter.h index 19d11173..98a7a192 100644 --- a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/meshtastic/mt_adapter.h +++ b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/meshtastic/mt_adapter.h @@ -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, diff --git a/platform/esp/arduino_common/src/ble/meshtastic_ble.cpp b/platform/esp/arduino_common/src/ble/meshtastic_ble.cpp index 8cc1874a..dfffa1b6 100644 --- a/platform/esp/arduino_common/src/ble/meshtastic_ble.cpp +++ b/platform/esp/arduino_common/src/ble/meshtastic_ble.cpp @@ -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(msg.msg_id), + static_cast(msg.from), + static_cast(msg.to), + static_cast(msg.text.size())); + phone_session_->onIncomingText(msg); + if (phone_session_->isSendingPackets()) + { + notifyFromNum(0); + } + } +} + void MeshtasticBleService::onIncomingData(const chat::MeshIncomingData& msg) { if (phone_session_) diff --git a/platform/esp/arduino_common/src/chat/infra/mesh_adapter_router.cpp b/platform/esp/arduino_common/src/chat/infra/mesh_adapter_router.cpp index aa9d5664..afe306cb 100644 --- a/platform/esp/arduino_common/src/chat/infra/mesh_adapter_router.cpp +++ b/platform/esp/arduino_common/src/chat/infra/mesh_adapter_router.cpp @@ -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_); diff --git a/platform/esp/arduino_common/src/chat/infra/meshtastic/mt_adapter.cpp b/platform/esp/arduino_common/src/chat/infra/meshtastic/mt_adapter.cpp index f9b955b8..11f5f382 100644 --- a/platform/esp/arduino_common/src/chat/infra/meshtastic/mt_adapter.cpp +++ b/platform/esp/arduino_common/src/chat/infra/meshtastic/mt_adapter.cpp @@ -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(pending.msg_id), + static_cast(pending.dest), + static_cast(out_channel), + static_cast(text.size())); LORA_LOG("[LORA] queue text ch=%u len=%u id=%lu\n", static_cast(channel), static_cast(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(msg_id), + static_cast(dest_node), + static_cast(portnum), + ok ? 1U : 0U, + air_want_ack ? 1U : 0U, + track_ack ? 1U : 0U, + static_cast(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(header.from), + static_cast(header.to), + static_cast(header.id), + static_cast(header.flags), + static_cast(header.channel), + static_cast(header.next_hop), + static_cast(header.relay_node), + static_cast(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(header.id), + static_cast(header.relay_node), + static_cast(header.next_hop), + static_cast(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(header.id), + static_cast(header.to), + static_cast(header.relay_node), + static_cast(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(decoded.request_id), + static_cast(header.from), + static_cast(routing.error_reason), + ok ? 1U : 0U); LORA_LOG("[LORA] RX ack reason=%u (%s)\n", static_cast(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(it->first), + static_cast(dest), + static_cast(now - it->second)); + LORA_LOG("[LORA] RX ack timeout req=%08lX dest=%08lX\n", + static_cast(it->first), + static_cast(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(request_id), + static_cast(from), + static_cast(to), + static_cast(reason)); + meshtastic_Routing routing = meshtastic_Routing_init_default; routing.which_variant = meshtastic_Routing_error_reason_tag; routing.error_reason = reason; diff --git a/platform/esp/arduino_common/src/screen_sleep.cpp b/platform/esp/arduino_common/src/screen_sleep.cpp index 315386a1..ac5ec27b 100644 --- a/platform/esp/arduino_common/src/screen_sleep.cpp +++ b/platform/esp/arduino_common/src/screen_sleep.cpp @@ -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(); diff --git a/platform/nrf52/arduino_common/include/ble/meshtastic_ble.h b/platform/nrf52/arduino_common/include/ble/meshtastic_ble.h index 761c96c8..d9617cf9 100644 --- a/platform/nrf52/arduino_common/include/ble/meshtastic_ble.h +++ b/platform/nrf52/arduino_common/include/ble/meshtastic_ble.h @@ -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); diff --git a/platform/nrf52/arduino_common/src/ble/meshtastic_ble.cpp b/platform/nrf52/arduino_common/src/ble/meshtastic_ble.cpp index 958ca7e1..a2ea6a8b 100644 --- a/platform/nrf52/arduino_common/src/ble/meshtastic_ble.cpp +++ b/platform/nrf52/arduino_common/src/ble/meshtastic_ble.cpp @@ -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(msg.msg_id), + static_cast(msg.from), + static_cast(msg.to), + static_cast(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", diff --git a/platform/nrf52/arduino_common/src/chat/infra/meshtastic/meshtastic_radio_adapter.cpp b/platform/nrf52/arduino_common/src/chat/infra/meshtastic/meshtastic_radio_adapter.cpp index b6041e12..fa9018fe 100644 --- a/platform/nrf52/arduino_common/src/chat/infra/meshtastic/meshtastic_radio_adapter.cpp +++ b/platform/nrf52/arduino_common/src/chat/infra/meshtastic/meshtastic_radio_adapter.cpp @@ -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(header.id), + static_cast(header.relay_node), + static_cast(header.next_hop), + static_cast(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(pending.packet_id), + static_cast(pending.dest), + static_cast(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(pending.packet_id), + static_cast(pending.original_from), + static_cast(pending.dest), + pending.observe_only ? 1U : 0U, + pending.local_origin ? 1U : 0U, + pending.want_ack ? 1U : 0U, + static_cast(pending.next_tx_ms)); pending_retransmits_[pendingKey(header.from, header.id)] = std::move(pending); }