From 9aa9d5ea03192e62dffb725b5f193501da2e6456 Mon Sep 17 00:00:00 2001 From: Kernel Panic Date: Tue, 7 Jul 2026 17:44:09 +0200 Subject: [PATCH 1/2] Add chunked PhoneAPI transport payloads --- protobufs | 2 +- src/mesh/PhoneAPI.cpp | 322 +++++++++++++++++++++++- src/mesh/generated/meshtastic/mesh.pb.h | 93 +++---- 3 files changed, 370 insertions(+), 47 deletions(-) diff --git a/protobufs b/protobufs index ba16bfc731a..569a4b3ac29 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit ba16bfc731ab7e23f6be5a8e73358b1973e73822 +Subproject commit 569a4b3ac2977b0d9611a2bc0bccf970d6d07378 diff --git a/src/mesh/PhoneAPI.cpp b/src/mesh/PhoneAPI.cpp index cf58c23b43f..24e0b3276c0 100644 --- a/src/mesh/PhoneAPI.cpp +++ b/src/mesh/PhoneAPI.cpp @@ -44,11 +44,320 @@ namespace { constexpr uint8_t FILES_MANIFEST_LEVELS = 3; constexpr size_t FILES_MANIFEST_MAX_COUNT = 64; +constexpr size_t MAX_CHUNKED_TORADIO_SLOTS = 2; +constexpr size_t MAX_CHUNKED_FROMRADIO_SLOTS = 2; +constexpr size_t MAX_CHUNKED_FROMRADIO_PACKET_SIZE = 20; +constexpr uint16_t CHUNKED_FROMRADIO_FIRST_BYTES = 12; +constexpr uint16_t CHUNKED_FROMRADIO_NEXT_BYTES = 15; +constexpr uint32_t CHUNKED_TORADIO_TIMEOUT_MS = 30000; + +// Keep chunk state deliberately small: this is sized for the normal serial/BLE +// client footprint, not for many simultaneous chunking clients. Extra clients +// fail closed and stale ToRadio transfers are reclaimed after the timeout below. + +struct ChunkedToRadioSlot { + PhoneAPI *who = nullptr; + uint16_t totalSize = 0; + uint16_t bytesUsed = 0; + uint32_t updatedMillis = 0; + uint8_t bytes[MAX_TO_FROM_RADIO_SIZE] = {0}; +}; + +struct ChunkedFromRadioSlot { + PhoneAPI *who = nullptr; + bool enabled = false; + uint16_t totalSize = 0; + uint16_t bytesUsed = 0; + uint32_t updatedMillis = 0; + uint8_t bytes[MAX_TO_FROM_RADIO_SIZE] = {0}; +}; + +static ChunkedToRadioSlot g_chunkedToRadioSlots[MAX_CHUNKED_TORADIO_SLOTS]; +static concurrency::Lock g_chunkedToRadioMutex; +static ChunkedFromRadioSlot g_chunkedFromRadioSlots[MAX_CHUNKED_FROMRADIO_SLOTS]; +static concurrency::Lock g_chunkedFromRadioMutex; void releaseFilesManifest(std::vector &filesManifest) { std::vector().swap(filesManifest); } + +static void clearChunkedToRadioSlot_LH(ChunkedToRadioSlot &slot) +{ + slot.who = nullptr; + slot.totalSize = 0; + slot.bytesUsed = 0; + slot.updatedMillis = 0; + memset(slot.bytes, 0, sizeof(slot.bytes)); +} + +static void clearChunkedToRadioSlots(PhoneAPI *api) +{ + concurrency::LockGuard guard(&g_chunkedToRadioMutex); + for (auto &slot : g_chunkedToRadioSlots) { + if (slot.who == api) { + clearChunkedToRadioSlot_LH(slot); + } + } +} + +static void clearChunkedFromRadioPayload_LH(ChunkedFromRadioSlot &slot) +{ + slot.totalSize = 0; + slot.bytesUsed = 0; + slot.updatedMillis = millis(); + memset(slot.bytes, 0, sizeof(slot.bytes)); +} + +static void clearChunkedFromRadioSlot_LH(ChunkedFromRadioSlot &slot) +{ + slot.who = nullptr; + slot.enabled = false; + clearChunkedFromRadioPayload_LH(slot); +} + +static void clearChunkedFromRadioSlot(PhoneAPI *api) +{ + concurrency::LockGuard guard(&g_chunkedFromRadioMutex); + for (auto &slot : g_chunkedFromRadioSlots) { + if (slot.who == api) { + clearChunkedFromRadioSlot_LH(slot); + } + } +} + +static ChunkedFromRadioSlot *findChunkedFromRadioSlot_LH(PhoneAPI *api) +{ + for (auto &slot : g_chunkedFromRadioSlots) { + if (slot.who == api) { + if (!api->isConnected()) { + clearChunkedFromRadioSlot_LH(slot); + return nullptr; + } + return &slot; + } + } + + return nullptr; +} + +static ChunkedFromRadioSlot *findOrAllocChunkedFromRadioSlot_LH(PhoneAPI *api) +{ + if (auto *existing = findChunkedFromRadioSlot_LH(api)) { + return existing; + } + + for (auto &slot : g_chunkedFromRadioSlots) { + if (slot.who == nullptr) { + slot.who = api; + slot.updatedMillis = millis(); + return &slot; + } + } + + return nullptr; +} + +static void noteChunkedApiSupport(PhoneAPI *api) +{ + concurrency::LockGuard guard(&g_chunkedFromRadioMutex); + auto *slot = findOrAllocChunkedFromRadioSlot_LH(api); + if (!slot) { + LOG_WARN("No ChunkedPayload capability slot available"); + return; + } + slot->enabled = true; + slot->updatedMillis = millis(); +} + +static bool hasChunkedApiSupport_LH(PhoneAPI *api) +{ + auto *slot = findChunkedFromRadioSlot_LH(api); + return slot && slot->enabled; +} + +static ChunkedToRadioSlot *findChunkedToRadioSlot_LH(PhoneAPI *api) +{ + const uint32_t now = millis(); + for (auto &slot : g_chunkedToRadioSlots) { + if (slot.who != nullptr && now - slot.updatedMillis > CHUNKED_TORADIO_TIMEOUT_MS) { + clearChunkedToRadioSlot_LH(slot); + } + } + + for (auto &slot : g_chunkedToRadioSlots) { + if (slot.who == api) { + return &slot; + } + } + + return nullptr; +} + +static ChunkedToRadioSlot *allocChunkedToRadioSlot_LH(PhoneAPI *api, uint16_t totalSize) +{ + const uint32_t now = millis(); + if (auto *existing = findChunkedToRadioSlot_LH(api)) { + clearChunkedToRadioSlot_LH(*existing); + existing->who = api; + existing->totalSize = totalSize; + existing->updatedMillis = millis(); + return existing; + } + + for (auto &slot : g_chunkedToRadioSlots) { + if (slot.who == nullptr) { + slot.who = api; + slot.totalSize = totalSize; + slot.updatedMillis = now; + return &slot; + } + } + + for (auto &slot : g_chunkedToRadioSlots) { + if (slot.who == api) { + clearChunkedToRadioSlot_LH(slot); + slot.who = api; + slot.totalSize = totalSize; + slot.updatedMillis = now; + return &slot; + } + } + + return nullptr; +} + +static bool handleChunkedToRadio(PhoneAPI *api, const meshtastic_ChunkedPayload &chunk) +{ + if (chunk.payload_chunk.size == 0) { + LOG_WARN("Invalid ChunkedPayload empty chunk total=%u", chunk.payload_size); + return false; + } + + uint8_t reassembled[MAX_TO_FROM_RADIO_SIZE]; + uint16_t reassembledSize = 0; + bool complete = false; + + { + concurrency::LockGuard guard(&g_chunkedToRadioMutex); + ChunkedToRadioSlot *slot = nullptr; + if (chunk.payload_size != 0) { + if (chunk.payload_size > MAX_TO_FROM_RADIO_SIZE || chunk.payload_size < chunk.payload_chunk.size) { + LOG_WARN("Invalid ChunkedPayload total=%u chunk=%u", chunk.payload_size, chunk.payload_chunk.size); + return false; + } + slot = allocChunkedToRadioSlot_LH(api, chunk.payload_size); + } else { + slot = findChunkedToRadioSlot_LH(api); + } + + if (!slot) { + LOG_WARN("No active ChunkedPayload transfer"); + return false; + } + + if (slot->bytesUsed + chunk.payload_chunk.size > slot->totalSize) { + LOG_WARN("ChunkedPayload too large used=%u add=%u total=%u", slot->bytesUsed, chunk.payload_chunk.size, slot->totalSize); + clearChunkedToRadioSlot_LH(*slot); + return false; + } + + memcpy(slot->bytes + slot->bytesUsed, chunk.payload_chunk.bytes, chunk.payload_chunk.size); + slot->bytesUsed += chunk.payload_chunk.size; + slot->updatedMillis = millis(); + LOG_DEBUG("ChunkedPayload bytes=%u used=%u total=%u", chunk.payload_chunk.size, slot->bytesUsed, slot->totalSize); + + if (slot->bytesUsed == slot->totalSize) { + reassembledSize = slot->bytesUsed; + memcpy(reassembled, slot->bytes, reassembledSize); + clearChunkedToRadioSlot_LH(*slot); + complete = true; + } + } + + if (!complete) { + return false; + } + + LOG_INFO("ChunkedPayload complete bytes=%u", reassembledSize); + noteChunkedApiSupport(api); + return api->handleToRadio(reassembled, reassembledSize); +} + +static size_t encodeChunkedFromRadio_LH(ChunkedFromRadioSlot &slot, uint8_t *buf) +{ + if (slot.totalSize == 0 || slot.bytesUsed >= slot.totalSize) { + return 0; + } + + const bool firstChunk = slot.bytesUsed == 0; + uint16_t chunkSize = firstChunk ? CHUNKED_FROMRADIO_FIRST_BYTES : CHUNKED_FROMRADIO_NEXT_BYTES; + const uint16_t remaining = slot.totalSize - slot.bytesUsed; + if (chunkSize > remaining) { + chunkSize = remaining; + } + + meshtastic_FromRadio chunked = {}; + chunked.which_payload_variant = meshtastic_FromRadio_chunked_payload_tag; + chunked.chunked_payload.payload_size = firstChunk ? slot.totalSize : 0; + chunked.chunked_payload.payload_chunk.size = chunkSize; + memcpy(chunked.chunked_payload.payload_chunk.bytes, slot.bytes + slot.bytesUsed, chunkSize); + + const size_t numbytes = pb_encode_to_bytes(buf, meshtastic_FromRadio_size, &meshtastic_FromRadio_msg, &chunked); + if (numbytes == 0 || numbytes > MAX_CHUNKED_FROMRADIO_PACKET_SIZE) { + LOG_ERROR("Chunked FromRadio encode failed size=%u chunk=%u first=%d", numbytes, chunkSize, firstChunk); + clearChunkedFromRadioPayload_LH(slot); + return 0; + } + + slot.bytesUsed += chunkSize; + slot.updatedMillis = millis(); + LOG_DEBUG("Chunked FromRadio bytes=%u used=%u total=%u encoded=%u", chunkSize, slot.bytesUsed, slot.totalSize, numbytes); + if (slot.bytesUsed == slot.totalSize) { + clearChunkedFromRadioPayload_LH(slot); + } + + return numbytes; +} + +static size_t getPendingChunkedFromRadio(PhoneAPI *api, uint8_t *buf) +{ + concurrency::LockGuard guard(&g_chunkedFromRadioMutex); + auto *slot = findChunkedFromRadioSlot_LH(api); + if (!slot || slot->totalSize == 0) { + return 0; + } + return encodeChunkedFromRadio_LH(*slot, buf); +} + +static size_t maybeEncodeChunkedFromRadio(PhoneAPI *api, uint8_t *buf, const uint8_t *fromRadioBytes, size_t fromRadioSize) +{ + if (fromRadioSize <= MAX_CHUNKED_FROMRADIO_PACKET_SIZE) { + return fromRadioSize; + } + + concurrency::LockGuard guard(&g_chunkedFromRadioMutex); + if (!hasChunkedApiSupport_LH(api)) { + return fromRadioSize; + } + + auto *slot = findOrAllocChunkedFromRadioSlot_LH(api); + if (!slot) { + LOG_WARN("No Chunked FromRadio slot available, sending unchunked bytes=%u", fromRadioSize); + return fromRadioSize; + } + + if (fromRadioSize > sizeof(slot->bytes)) { + LOG_ERROR("FromRadio too large to chunk bytes=%u max=%u", fromRadioSize, sizeof(slot->bytes)); + return 0; + } + + memcpy(slot->bytes, fromRadioBytes, fromRadioSize); + slot->totalSize = fromRadioSize; + slot->bytesUsed = 0; + slot->updatedMillis = millis(); + return encodeChunkedFromRadio_LH(*slot, buf); +} } // namespace // Flag to indicate a heartbeat was received and we should send queue status @@ -357,6 +666,8 @@ void PhoneAPI::handleStartConfig() void PhoneAPI::close() { LOG_DEBUG("PhoneAPI::close()"); + clearChunkedToRadioSlots(this); + clearChunkedFromRadioSlot(this); if (service->api_state == service->STATE_BLE && api_type == TYPE_BLE) service->api_state = service->STATE_DISCONNECTED; else if (service->api_state == service->STATE_WIFI && api_type == TYPE_WIFI) @@ -519,6 +830,9 @@ bool PhoneAPI::handleToRadio(const uint8_t *buf, size_t bufLength) heartbeatReceived = true; } break; + case meshtastic_ToRadio_chunked_payload_tag: + handleChunkedToRadio(this, toRadioScratch.chunked_payload); + break; default: // Ignore nop messages break; @@ -552,6 +866,10 @@ bool PhoneAPI::handleToRadio(const uint8_t *buf, size_t bufLength) size_t PhoneAPI::getFromRadio(uint8_t *buf) { + if (size_t chunkedBytes = getPendingChunkedFromRadio(this, buf)) { + return chunkedBytes; + } + // Respond to heartbeat by sending queue status if (heartbeatReceived) { memset(&fromRadioScratch, 0, sizeof(fromRadioScratch)); @@ -560,7 +878,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf) heartbeatReceived = false; size_t numbytes = pb_encode_to_bytes(buf, meshtastic_FromRadio_size, &meshtastic_FromRadio_msg, &fromRadioScratch); LOG_DEBUG("FromRadio=STATE_SEND_QUEUE_STATUS, numbytes=%u", numbytes); - return numbytes; + return maybeEncodeChunkedFromRadio(this, buf, buf, numbytes); } if (!available()) { @@ -1084,7 +1402,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf) // VERY IMPORTANT to not print debug messages while writing to fromRadioScratch - because we use that same buffer // for logging (when we are encapsulating with protobufs) - return numbytes; + return maybeEncodeChunkedFromRadio(this, buf, buf, numbytes); } LOG_DEBUG("No FromRadio packet available"); diff --git a/src/mesh/generated/meshtastic/mesh.pb.h b/src/mesh/generated/meshtastic/mesh.pb.h index f2b0d6dc1fc..2b33ec52549 100644 --- a/src/mesh/generated/meshtastic/mesh.pb.h +++ b/src/mesh/generated/meshtastic/mesh.pb.h @@ -1438,6 +1438,31 @@ typedef struct _meshtastic_LoRaRegionPresetMap { meshtastic_LoRaRegionPresets region_groups[38]; } meshtastic_LoRaRegionPresetMap; +/* A heartbeat message is sent to the node from the client to keep the connection alive. + This is currently only needed to keep serial connections alive, but can be used by any PhoneAPI. */ +typedef struct _meshtastic_Heartbeat { + /* The nonce of the heartbeat message */ + uint32_t nonce; +} meshtastic_Heartbeat; + +/* RemoteHardwarePins associated with a node */ +typedef struct _meshtastic_NodeRemoteHardwarePin { + /* The node_num exposing the available gpio pin */ + uint32_t node_num; + /* The the available gpio pin for usage with RemoteHardware module */ + bool has_pin; + meshtastic_RemoteHardwarePin pin; +} meshtastic_NodeRemoteHardwarePin; + +typedef PB_BYTES_ARRAY_T(228) meshtastic_ChunkedPayload_payload_chunk_t; +typedef struct _meshtastic_ChunkedPayload { + /* The total size of the reassembled payload. This is sent only on the + first chunk of an ordered transfer; later chunks omit it. */ + uint16_t payload_size; + /* The binary data of the current chunk */ + meshtastic_ChunkedPayload_payload_chunk_t payload_chunk; +} meshtastic_ChunkedPayload; + /* Packets from the radio to the phone will appear on the fromRadio characteristic. It will support READ and NOTIFY. When a new packet arrives the device will BLE notify? It will sit in that descriptor until consumed by the phone, @@ -1500,16 +1525,12 @@ typedef struct _meshtastic_FromRadio { illegal region+preset combination. A region that does not appear in any group carries no constraint info and should not be restricted. */ meshtastic_LoRaRegionPresetMap region_presets; + /* Chunked client API payload. Only sent to clients that have opted in to + chunked API transport support. */ + meshtastic_ChunkedPayload chunked_payload; }; } meshtastic_FromRadio; -/* A heartbeat message is sent to the node from the client to keep the connection alive. - This is currently only needed to keep serial connections alive, but can be used by any PhoneAPI. */ -typedef struct _meshtastic_Heartbeat { - /* The nonce of the heartbeat message */ - uint32_t nonce; -} meshtastic_Heartbeat; - /* Packets/commands to the radio will be written (reliably) to the toRadio characteristic. Once the write completes the phone can assume it is handled. */ typedef struct _meshtastic_ToRadio { @@ -1535,30 +1556,12 @@ typedef struct _meshtastic_ToRadio { meshtastic_MqttClientProxyMessage mqttClientProxyMessage; /* Heartbeat message (used to keep the device connection awake on serial) */ meshtastic_Heartbeat heartbeat; + /* Chunked client API payload. The reassembled payload is a serialized + ToRadio message and is handled through the normal PhoneAPI path. */ + meshtastic_ChunkedPayload chunked_payload; }; } meshtastic_ToRadio; -/* RemoteHardwarePins associated with a node */ -typedef struct _meshtastic_NodeRemoteHardwarePin { - /* The node_num exposing the available gpio pin */ - uint32_t node_num; - /* The the available gpio pin for usage with RemoteHardware module */ - bool has_pin; - meshtastic_RemoteHardwarePin pin; -} meshtastic_NodeRemoteHardwarePin; - -typedef PB_BYTES_ARRAY_T(228) meshtastic_ChunkedPayload_payload_chunk_t; -typedef struct _meshtastic_ChunkedPayload { - /* The ID of the entire payload */ - uint32_t payload_id; - /* The total number of chunks in the payload */ - uint16_t chunk_count; - /* The current chunk index in the total */ - uint16_t chunk_index; - /* The binary data of the current chunk */ - meshtastic_ChunkedPayload_payload_chunk_t payload_chunk; -} meshtastic_ChunkedPayload; - /* Wrapper message for broken repeated oneof support */ typedef struct _meshtastic_resend_chunks { pb_callback_t chunks; @@ -1743,7 +1746,7 @@ extern "C" { #define meshtastic_LoRaRegionPresetMap_init_default {0, {meshtastic_LoRaPresetGroup_init_default, meshtastic_LoRaPresetGroup_init_default, meshtastic_LoRaPresetGroup_init_default, meshtastic_LoRaPresetGroup_init_default, meshtastic_LoRaPresetGroup_init_default, meshtastic_LoRaPresetGroup_init_default, meshtastic_LoRaPresetGroup_init_default, meshtastic_LoRaPresetGroup_init_default}, 0, {meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default}} #define meshtastic_Heartbeat_init_default {0} #define meshtastic_NodeRemoteHardwarePin_init_default {0, false, meshtastic_RemoteHardwarePin_init_default} -#define meshtastic_ChunkedPayload_init_default {0, 0, 0, {0, {0}}} +#define meshtastic_ChunkedPayload_init_default {0, {0, {0}}} #define meshtastic_resend_chunks_init_default {{{NULL}, NULL}} #define meshtastic_ChunkedPayloadResponse_init_default {0, 0, {0}} #define meshtastic_Position_init_zero {false, 0, false, 0, false, 0, 0, _meshtastic_Position_LocSource_MIN, _meshtastic_Position_AltSource_MIN, 0, 0, false, 0, false, 0, 0, 0, 0, 0, false, 0, false, 0, 0, 0, 0, 0, 0, 0, 0} @@ -1782,7 +1785,7 @@ extern "C" { #define meshtastic_LoRaRegionPresetMap_init_zero {0, {meshtastic_LoRaPresetGroup_init_zero, meshtastic_LoRaPresetGroup_init_zero, meshtastic_LoRaPresetGroup_init_zero, meshtastic_LoRaPresetGroup_init_zero, meshtastic_LoRaPresetGroup_init_zero, meshtastic_LoRaPresetGroup_init_zero, meshtastic_LoRaPresetGroup_init_zero, meshtastic_LoRaPresetGroup_init_zero}, 0, {meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero}} #define meshtastic_Heartbeat_init_zero {0} #define meshtastic_NodeRemoteHardwarePin_init_zero {0, false, meshtastic_RemoteHardwarePin_init_zero} -#define meshtastic_ChunkedPayload_init_zero {0, 0, 0, {0, {0}}} +#define meshtastic_ChunkedPayload_init_zero {0, {0, {0}}} #define meshtastic_resend_chunks_init_zero {{{NULL}, NULL}} #define meshtastic_ChunkedPayloadResponse_init_zero {0, 0, {0}} @@ -1986,6 +1989,11 @@ extern "C" { #define meshtastic_LoRaRegionPresets_group_index_tag 2 #define meshtastic_LoRaRegionPresetMap_groups_tag 1 #define meshtastic_LoRaRegionPresetMap_region_groups_tag 2 +#define meshtastic_Heartbeat_nonce_tag 1 +#define meshtastic_NodeRemoteHardwarePin_node_num_tag 1 +#define meshtastic_NodeRemoteHardwarePin_pin_tag 2 +#define meshtastic_ChunkedPayload_payload_size_tag 1 +#define meshtastic_ChunkedPayload_payload_chunk_tag 2 #define meshtastic_FromRadio_id_tag 1 #define meshtastic_FromRadio_packet_tag 2 #define meshtastic_FromRadio_my_info_tag 3 @@ -2005,19 +2013,14 @@ extern "C" { #define meshtastic_FromRadio_deviceuiConfig_tag 17 #define meshtastic_FromRadio_lockdown_status_tag 18 #define meshtastic_FromRadio_region_presets_tag 19 -#define meshtastic_Heartbeat_nonce_tag 1 +#define meshtastic_FromRadio_chunked_payload_tag 20 #define meshtastic_ToRadio_packet_tag 1 #define meshtastic_ToRadio_want_config_id_tag 3 #define meshtastic_ToRadio_disconnect_tag 4 #define meshtastic_ToRadio_xmodemPacket_tag 5 #define meshtastic_ToRadio_mqttClientProxyMessage_tag 6 #define meshtastic_ToRadio_heartbeat_tag 7 -#define meshtastic_NodeRemoteHardwarePin_node_num_tag 1 -#define meshtastic_NodeRemoteHardwarePin_pin_tag 2 -#define meshtastic_ChunkedPayload_payload_id_tag 1 -#define meshtastic_ChunkedPayload_chunk_count_tag 2 -#define meshtastic_ChunkedPayload_chunk_index_tag 3 -#define meshtastic_ChunkedPayload_payload_chunk_tag 4 +#define meshtastic_ToRadio_chunked_payload_tag 8 #define meshtastic_resend_chunks_chunks_tag 1 #define meshtastic_ChunkedPayloadResponse_payload_id_tag 1 #define meshtastic_ChunkedPayloadResponse_request_transfer_tag 2 @@ -2264,7 +2267,8 @@ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,fileInfo,fileInfo), 15) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,clientNotification,clientNotification), 16) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,deviceuiConfig,deviceuiConfig), 17) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,lockdown_status,lockdown_status), 18) \ -X(a, STATIC, ONEOF, MESSAGE, (payload_variant,region_presets,region_presets), 19) +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,region_presets,region_presets), 19) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,chunked_payload,chunked_payload), 20) #define meshtastic_FromRadio_CALLBACK NULL #define meshtastic_FromRadio_DEFAULT NULL #define meshtastic_FromRadio_payload_variant_packet_MSGTYPE meshtastic_MeshPacket @@ -2283,6 +2287,7 @@ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,region_presets,region_preset #define meshtastic_FromRadio_payload_variant_deviceuiConfig_MSGTYPE meshtastic_DeviceUIConfig #define meshtastic_FromRadio_payload_variant_lockdown_status_MSGTYPE meshtastic_LockdownStatus #define meshtastic_FromRadio_payload_variant_region_presets_MSGTYPE meshtastic_LoRaRegionPresetMap +#define meshtastic_FromRadio_payload_variant_chunked_payload_MSGTYPE meshtastic_ChunkedPayload #define meshtastic_LockdownStatus_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, UENUM, state, 1) \ @@ -2354,13 +2359,15 @@ X(a, STATIC, ONEOF, UINT32, (payload_variant,want_config_id,want_config_i X(a, STATIC, ONEOF, BOOL, (payload_variant,disconnect,disconnect), 4) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,xmodemPacket,xmodemPacket), 5) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,mqttClientProxyMessage,mqttClientProxyMessage), 6) \ -X(a, STATIC, ONEOF, MESSAGE, (payload_variant,heartbeat,heartbeat), 7) +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,heartbeat,heartbeat), 7) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,chunked_payload,chunked_payload), 8) #define meshtastic_ToRadio_CALLBACK NULL #define meshtastic_ToRadio_DEFAULT NULL #define meshtastic_ToRadio_payload_variant_packet_MSGTYPE meshtastic_MeshPacket #define meshtastic_ToRadio_payload_variant_xmodemPacket_MSGTYPE meshtastic_XModem #define meshtastic_ToRadio_payload_variant_mqttClientProxyMessage_MSGTYPE meshtastic_MqttClientProxyMessage #define meshtastic_ToRadio_payload_variant_heartbeat_MSGTYPE meshtastic_Heartbeat +#define meshtastic_ToRadio_payload_variant_chunked_payload_MSGTYPE meshtastic_ChunkedPayload #define meshtastic_Compressed_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, UENUM, portnum, 1) \ @@ -2435,10 +2442,8 @@ X(a, STATIC, OPTIONAL, MESSAGE, pin, 2) #define meshtastic_NodeRemoteHardwarePin_pin_MSGTYPE meshtastic_RemoteHardwarePin #define meshtastic_ChunkedPayload_FIELDLIST(X, a) \ -X(a, STATIC, SINGULAR, UINT32, payload_id, 1) \ -X(a, STATIC, SINGULAR, UINT32, chunk_count, 2) \ -X(a, STATIC, SINGULAR, UINT32, chunk_index, 3) \ -X(a, STATIC, SINGULAR, BYTES, payload_chunk, 4) +X(a, STATIC, SINGULAR, UINT32, payload_size, 1) \ +X(a, STATIC, SINGULAR, BYTES, payload_chunk, 2) #define meshtastic_ChunkedPayload_CALLBACK NULL #define meshtastic_ChunkedPayload_DEFAULT NULL @@ -2542,7 +2547,7 @@ extern const pb_msgdesc_t meshtastic_ChunkedPayloadResponse_msg; /* meshtastic_ChunkedPayloadResponse_size depends on runtime parameters */ #define MESHTASTIC_MESHTASTIC_MESH_PB_H_MAX_SIZE meshtastic_FromRadio_size #define meshtastic_BoundingBox_size 20 -#define meshtastic_ChunkedPayload_size 245 +#define meshtastic_ChunkedPayload_size 235 #define meshtastic_ClientNotification_size 482 #define meshtastic_Compressed_size 239 #define meshtastic_Data_size 335 From 1ae91229d7e16a39ff8dfcc2d0c32fced5493973 Mon Sep 17 00:00:00 2001 From: Kernel Panic Date: Tue, 7 Jul 2026 21:07:04 +0200 Subject: [PATCH 2/2] Use Throttle for chunk timeout --- src/mesh/PhoneAPI.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/mesh/PhoneAPI.cpp b/src/mesh/PhoneAPI.cpp index 24e0b3276c0..a9f362f61e7 100644 --- a/src/mesh/PhoneAPI.cpp +++ b/src/mesh/PhoneAPI.cpp @@ -178,9 +178,8 @@ static bool hasChunkedApiSupport_LH(PhoneAPI *api) static ChunkedToRadioSlot *findChunkedToRadioSlot_LH(PhoneAPI *api) { - const uint32_t now = millis(); for (auto &slot : g_chunkedToRadioSlots) { - if (slot.who != nullptr && now - slot.updatedMillis > CHUNKED_TORADIO_TIMEOUT_MS) { + if (slot.who != nullptr && !Throttle::isWithinTimespanMs(slot.updatedMillis, CHUNKED_TORADIO_TIMEOUT_MS)) { clearChunkedToRadioSlot_LH(slot); } }