Align Meshtastic phone sync and chat UI refresh

This commit is contained in:
liu weikai
2026-03-27 22:32:51 +08:00
parent df76526b5d
commit e0076d707b
18 changed files with 1003 additions and 49 deletions
+681
View File
@@ -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;
+6 -4
View File
@@ -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;
@@ -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,
@@ -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",
@@ -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);
}