Skip to content

Add chunked PhoneAPI transport payloads - #10928

Open
kernel-oops wants to merge 9 commits into
meshtastic:developfrom
kernel-oops:garmin-ble-chunked-phoneapi
Open

Add chunked PhoneAPI transport payloads#10928
kernel-oops wants to merge 9 commits into
meshtastic:developfrom
kernel-oops:garmin-ble-chunked-phoneapi

Conversation

@kernel-oops

@kernel-oops kernel-oops commented Jul 7, 2026

Copy link
Copy Markdown

Summary

This adds a compact chunked PhoneAPI transport so clients with a 20-byte GATT payload limit can exchange complete Meshtastic client API protobufs without ATT long reads or writes.

The immediate use case is Garmin Connect IQ BLE. Garmin limits application writes to 20 bytes and does not provide ATT long reads, while normal ToRadio and FromRadio messages can be substantially larger.

Depends on the corresponding protobuf schema PR: meshtastic/protobufs#980

What changed

  • Adds a distinct ClientApiChunkedPayload transport wrapper with uint32 payload_size = 1 and bytes payload_chunk = 2.
  • Adds ToRadio.chunked_payload = 8 and FromRadio.chunked_payload = 20.
  • Keeps the existing four-field ChunkedPayload and ChunkedPayloadResponse messages unchanged for compatibility with their current users.
  • Reassembles incoming chunked ToRadio messages before passing the original serialized message to the existing PhoneAPI handler.
  • Emits chunked FromRadio messages only after that client has successfully completed a chunked ToRadio transfer. Legacy clients retain the existing unchunked behavior.
  • Tracks inbound and outbound state for two simultaneous PhoneAPI clients in one lock-protected static table.
  • Reclaims incomplete incoming transfers after 30 seconds and clears all state when a client disconnects.
  • Regenerates the nanopb mesh.pb.cpp and mesh.pb.h sources for the schema change.

Wire format

The transport assumes ordered, reliable delivery on one client connection. This matches Garmin's serialized BLE writes-with-response and reads, and avoids spending bytes on transfer IDs and chunk indices in every packet.

Only the first chunk carries the total serialized payload size. Every chunk carries the next payload bytes.

For a 20-byte GATT payload:

  • ToRadio first chunk: up to 13 payload bytes.
  • ToRadio continuation: up to 16 payload bytes.
  • FromRadio first chunk: up to 12 payload bytes.
  • FromRadio continuation: up to 15 payload bytes.

The FromRadio encoder writes the fixed canonical protobuf wire representation directly. Boundary tests compare it byte-for-byte with nanopb output.

Safety and compatibility

  • The existing ChunkedPayload wire schema remains unchanged.
  • Chunking is negotiated per connection; clients that do not opt in continue receiving normal FromRadio messages.
  • Incoming first chunks are rejected when the declared size exceeds MAX_TO_FROM_RADIO_SIZE or is smaller than the bytes already supplied.
  • Continuations are rejected and their state cleared if they would exceed the declared total.
  • Empty chunks, continuations without an active transfer, and clients beyond the bounded two-slot table fail closed.
  • Reassembled messages are copied out of shared state and passed to handleToRadio() only after releasing the mutex.
  • Outbound messages are size-checked before entering the fixed 512-byte buffer.
  • Pending outbound chunks are drained before another normal FromRadio message is generated.

This is intentionally not a retransmission protocol. It relies on the ordered, reliable behavior of the underlying client connection.

Flash footprint

Measured on nrf52_promicro_diy_tcxo against the same develop base:

Build Flash RAM Warm-store headroom
develop 800,816 B 102,612 B 2,000 B
Initial integrated chunk transport 802,816 B 104,724 B 0 B
Final optimized chunk transport 801,584 B 104,708 B 1,232 B

The final patch adds 768 bytes of flash and 2,096 bytes of RAM relative to develop. Consolidating the client tables and locks, removing redundant state and chunk-only log strings, avoiding unnecessary buffer clearing, and directly encoding the small outbound wrapper recovered 1,232 bytes of flash from the initial integrated implementation.

The target's nominal flash limit is 815,104 bytes, but its reserved WarmNodeStore region starts earlier at 0xEA000. The final image ends at 0xE9B30, 1,232 bytes below that effective boundary, and passes the warm-region guard.

No unrelated Pro Micro functionality or target configuration was removed.

Validation

  • Built nrf52_promicro_diy_tcxo; warm-region, ISR, and LTO guards passed.
  • Built heltec-v4 from the PR branch: 2,314,411 bytes flash and 103,760 bytes RAM.
  • Regenerated nanopb output and verified it is reproducible.
  • Compared direct outbound encoding byte-for-byte with nanopb at total payload sizes 21, 127, 128, 511, and 512 bytes; every wrapper was at most 20 bytes and reconstructed the original payload exactly.
  • Verified the pre-existing ChunkedPayload serialization vector remains 080110021803220104.
  • Ported the same transport to current master, built and flashed a Heltec V4 app image, and completed a live BLE round trip with a Garmin D2 Mach 2.
  • Live validation covered chunked FromRadio messages including 116-byte node info and a 171-byte MQTT proxy message, plus a chunked 34-byte ToRadio message; all BLE writes completed successfully and config transfer completed without errors.
  • Ran git diff --check.

The remaining hardware test gap is simultaneous use by two physical PhoneAPI clients; the bounded two-client paths have been reviewed and tested at the serialization/state-machine level.

Summary by CodeRabbit

  • New Features

    • Added automatic chunked transport for large ToRadio/FromRadio protobuf payloads, including outbound chunk sequencing and inbound reassembly.
    • Enabled chunked messaging for clients that advertise chunked support, with proper per-connection state management.
  • Bug Fixes

    • Improved validation and recovery for incomplete, invalid, oversized, or timed-out fragments to avoid processing corrupted data.
  • Chores

    • Updated referenced protobuf sources and generated artifacts.

@CLAassistant

CLAassistant commented Jul 7, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 84fdbf9a-901c-41c7-a0f6-79a2b823f72e

📥 Commits

Reviewing files that changed from the base of the PR and between f73a6cc and 8b7adff.

📒 Files selected for processing (1)
  • protobufs

📝 Walkthrough

Walkthrough

This PR updates the protobufs submodule and adds chunked transport for oversized ToRadio and FromRadio protobuf payloads, including inbound reassembly, outbound fragmentation, timeout handling, and connection cleanup.

Changes

Chunked Payload Support

Layer / File(s) Summary
Chunked payload infrastructure and helpers
src/mesh/PhoneAPI.cpp, protobufs
Adds synchronized per-connection chunk state, inbound reassembly, outbound fragment encoding, stale-state timeout handling, and updates the protobufs submodule reference.
Inbound chunk handling and connection cleanup
src/mesh/PhoneAPI.cpp
Dispatches chunked ToRadio payloads to reassembly and clears chunk state when connections close.
Outbound chunked delivery paths
src/mesh/PhoneAPI.cpp
Returns pending fragments first and applies chunked encoding to queue-status and normal FromRadio responses.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant PhoneAPI
  participant ChunkedClient
  Client->>PhoneAPI: send chunked ToRadio payload
  PhoneAPI->>ChunkedClient: allocate or locate reassembly slot
  PhoneAPI->>ChunkedClient: append chunk bytes
  alt payload complete
    PhoneAPI->>PhoneAPI: forward reassembled bytes to handleToRadio
  else invalid or overflow
    PhoneAPI->>ChunkedClient: clear slot
  end
  Client->>PhoneAPI: call getFromRadio
  PhoneAPI->>ChunkedClient: check pending outbound fragment
  alt fragment pending
    PhoneAPI-->>Client: return fragment
  else no fragment pending
    PhoneAPI->>PhoneAPI: encode FromRadio payload
    PhoneAPI-->>Client: return original or chunked payload
  end
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: adding chunked PhoneAPI transport payloads.
Description check ✅ Passed The description is detailed and covers the change, compatibility, wire format, and validation, though it omits the template attestations.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

@kernel-oops, Welcome to Meshtastic!

Thanks for opening your first pull request. We really appreciate it.

We discuss work as a team in discord, please join us in the #firmware channel.
There's a big backlog of patches at the moment. If you have time,
please help us with some code review and testing of other PRs!

Welcome to the team 😄

@github-actions github-actions Bot added first-contribution enhancement New feature or request labels Jul 7, 2026
@kernel-oops
kernel-oops marked this pull request as ready for review July 7, 2026 18:25
@kernel-oops

kernel-oops commented Jul 7, 2026

Copy link
Copy Markdown
Author

Local validation after rebasing onto current develop:

  • Resolved the protobuf submodule conflict by updating to kernel-oops/protobufs@90d0af5 from Add compact chunked client API payload protobufs#980.
  • ./.venv/bin/pio has a stale shebang in this checkout, so I ran PlatformIO via the venv Python instead.
  • Command: .venv/bin/python .venv/bin/pio run -e heltec-v4
  • Result: heltec-v4 SUCCESS (00:37:57.748, including first-run tool/dependency downloads).

The protobuf PR is now ready for review as well: meshtastic/protobufs#980.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/mesh/PhoneAPI.cpp (1)

179-186: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use Throttle for the stale-slot timeout instead of raw millis() arithmetic.

This stale-transfer expiry is a "did N ms pass since X" check and should go through the Throttle helper.

♻️ Proposed change
-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);
-        }
-    }
+static ChunkedToRadioSlot *findChunkedToRadioSlot_LH(PhoneAPI *api)
+{
+    for (auto &slot : g_chunkedToRadioSlots) {
+        if (slot.who != nullptr && !Throttle::isWithinTimespanMs(slot.updatedMillis, CHUNKED_TORADIO_TIMEOUT_MS)) {
+            clearChunkedToRadioSlot_LH(slot);
+        }
+    }

As per coding guidelines: "Use Throttle for time-based rate limiting instead of raw millis() arithmetic; prefer Throttle::isWithinTimespanMs(lastMs, intervalMs) ... for 'did N ms pass since X' checks."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mesh/PhoneAPI.cpp` around lines 179 - 186, The stale-slot expiry check in
findChunkedToRadioSlot_LH is using raw millis() subtraction to decide whether
CHUNKED_TORADIO_TIMEOUT_MS has elapsed. Replace that logic with the Throttle
helper, using the existing slot timestamp field and
Throttle::isWithinTimespanMs(...) for the “has N ms passed since X” check before
calling clearChunkedToRadioSlot_LH. Keep the behavior the same, but route the
timeout decision through Throttle to match the time-based rate-limiting
guideline.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/mesh/PhoneAPI.cpp`:
- Around line 179-186: The stale-slot expiry check in findChunkedToRadioSlot_LH
is using raw millis() subtraction to decide whether CHUNKED_TORADIO_TIMEOUT_MS
has elapsed. Replace that logic with the Throttle helper, using the existing
slot timestamp field and Throttle::isWithinTimespanMs(...) for the “has N ms
passed since X” check before calling clearChunkedToRadioSlot_LH. Keep the
behavior the same, but route the timeout decision through Throttle to match the
time-based rate-limiting guideline.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8ccf108a-0720-4d93-a686-99de0a8e10ee

📥 Commits

Reviewing files that changed from the base of the PR and between d16ae2b and f619b73.

⛔ Files ignored due to path filters (1)
  • src/mesh/generated/meshtastic/mesh.pb.h is excluded by !**/generated/**, !src/mesh/generated/**
📒 Files selected for processing (2)
  • protobufs
  • src/mesh/PhoneAPI.cpp

@kernel-oops

Copy link
Copy Markdown
Author

Flash footprint update after optimizing only the chunked PhoneAPI implementation:

nrf52_promicro_diy_tcxo build Flash RAM Headroom before reserved warm-store region
Current develop baseline 800,816 B 102,612 B 2,000 B
Initial integrated chunk transport 802,816 B 104,724 B 0 B
Final optimized chunk transport 801,584 B 104,708 B 1,232 B

The optimization saves 1,232 bytes of flash compared with the initial integrated implementation. The final feature cost relative to the same develop baseline is 768 bytes of flash and 2,096 bytes of RAM.

The board definition's nominal flash limit is 815,104 bytes, which would suggest 13,520 bytes remain. The meaningful constraint on this target is tighter, however: the 12 KiB WarmNodeStore reservation starts at 0xEA000. The final image ends at 0xE9B30, so it is 1,232 bytes under the effective protected boundary and the nRF warm-region guard passes.

The savings came from combining inbound/outbound per-client state and locks, removing redundant state and buffer clearing, removing chunk-only log strings, and replacing nanopb encoding of the small outbound wrapper with a fixed canonical wire encoder. Boundary tests at 21, 127, 128, 511, and 512-byte payload totals matched nanopb byte-for-byte and reconstructed the original payload exactly.

Protocol behavior is unchanged: two clients remain supported, inbound timeout and bounds checks remain, outbound chunking is still opt-in per client, and legacy clients still receive unchunked messages. No unrelated Pro Micro functionality or target configuration was removed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request first-contribution

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants