Add Docker multi-arch, nanopb tool, examples, and routing#196
Conversation
Just a quick set of files to enable the build of (tagged) containers. Both alpine and debian containers are available (~200MiB/~1.2GiB) allowing us to use meshtastic cli with a quick docker run, instead of having to build/install stuff locally. Signed-off-by: Olliver Schinagl <oliver@schinagl.nl>
Ensures that automatic replies are sent back on the same channel index the message was received on. Previously, all replies defaulted to the primary channel (0), even if the incoming message arrived on a secondary channel. Additionally it ensures incoming messages match the specified channel index. E.g: meshtastic --ch-index 1 --reply . Modified the `onReceive` handler to extract the `channel` index from received packets. This ensures `interface.sendText` targets the originating channel rather than always defaulting to the primary channel. Added a filter to ensure that only the specified channel index is being replied to.
Replace raw tracebacks with helpful error messages that explain: - What went wrong - Possible causes - How to fix it Covers all BLEError cases: - Device not found (BLE disabled, sleep mode, out of range) - Multiple devices found (need to specify which one) - Write errors (pairing PIN, Linux bluetooth group) - Read errors (device disconnected) Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
Handle MeshInterface.MeshInterfaceError when device is rebooting or connection times out, with user-friendly error message. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
… cleanup and clearer exception handling.
* Moving to socket.sendall() is safer, as sendall will send the entire buffer, while send() would return the number of bytes sent and require being called multiple times if the buffer was full. * On exceptions: reconnect to the server. * On reconnection: make sure using a lock that there isn't a race between the readers and the writers triggering a reconnect.
When other nodes relay our position via map reports, they send it at reduced precision (e.g., 13 bits). _onPositionReceive() was blindly overwriting our locally-stored high-precision GPS position (32 bits) with these degraded echoes. The fix only protects the local node's position — since we have the GPS internally, any lower-precision update is always an echo from the mesh, never fresh data. Remote node positions are still updated normally, as any position they broadcast reflects their current state. Fixes meshtastic#910 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…re in __init__ If connect() or waitForConfig() raises during __init__ (handshake timeout, bad stream, config error), the reader thread started by connect() keeps running and the underlying stream/socket stays open — but the caller never receives a reference to the half-initialized instance, so they cannot call close() themselves. The leak compounds on every retry from a caller's reconnect loop. Fix: wrap connect() + waitForConfig() in try/except; call self.close() on any exception before re-raising. Also guard close() against RuntimeError from joining an unstarted reader thread (happens when close() runs from a failed __init__ before connect() could spawn it). Discovered while debugging a real-world Meshtastic firmware crash where a passive logger's retrying TCPInterface() calls against a node with 250-entry NodeDB produced a reconnect storm — every retry triggered a full config+NodeDB dump on the node, compounding heap pressure, which then exposed null-deref bugs in Router::perhapsDecode / MeshService (firmware side fixed in meshtastic/firmware#10226 and #10229). The client-side leak is independent of those firmware bugs and worth fixing on its own.
…for code generation
…ject Inject options in nanopb .options files into the protobuf files used for code generation
Refactor the Meshtastic TCP pub/sub example to ensure proper resource cleanup and clearer exception handling.
…t-replymessage-demo Add textchat.py and replymessage.py examples
Just a quick set of files to enable the build of (tagged) containers. Both alpine and debian containers are available (~200MiB/~1.2GiB) allowing us to use meshtastic cli with a quick docker run, instead of having to build/install stuff locally. Signed-off-by: Olliver Schinagl <oliver@schinagl.nl>
…htastic#928 Ports the 6 new files from upstream master 81ae8b6 (PR meshtastic#928 by ianmcorvidae): - examples/CONTRIBUTING.md - examples/tcp_connection_info_once.py - examples/tcp_pubsub_send_and_receive.py (replacement for pub_sub_example*.py) - examples/meshtastic_serial_message_reader.py - examples/textchat.py - examples/replymessage.py The rewrites of existing example files in 81ae8b6 (get_hw.py, hello_world_serial.py, info_example.py, scan_for_devices.py, set_owner.py, show_ports.py, tcp_gps_example.py, waypoint.py) are skipped because develop's versions are already more robust (context managers, type hints, stricter error handling) than master's modernization. Upstream commit: 81ae8b6 (Make examples more regularized and focused) Original author: Ian McEwen <ian@ianmcorvidae.net>
Ports the inject_nanopb_options.py script from upstream master 89d81c9. The script reads .options files from the protobufs submodule and injects the nanopb constraints (max_size, max_length, etc.) as inline proto field options so protoc --python_out embeds them in the generated descriptors. Python code can then read them via: field.GetOptions().Extensions[nanopb_pb2.nanopb].max_size Includes a7d13eb's parse_value bug fix: replaced s.lstrip("-").isdigit() with re.fullmatch(r"-?[0-9]+", s) to correctly reject strings like "---5" that lstrip would mangle. The actual _pb2.py regeneration is intentionally NOT performed here — the protobufs submodule is regenerated by CI workflows (see .github/workflows/update_protobufs.yml), and the script is ready for the next regen. Upstream commits: 89d81c9 (script) + a7d13eb (parse_value fix) Original author: Ian McEwen <ian@ianmcorvidae.net>
Adds the upstream 89d81c9 injection step to develop's regen script: 1. Copies protobufs/meshtastic/*.options into the temp build dir 2. After the existing sed pipeline, iterates over .options files and calls bin/inject_nanopb_options.py on the matching .proto file 3. Then runs protoc as before, so the generated _pb2.py descriptors embed the nanopb constraints inline The script change is intentionally additive: if no .options files are present, the for loop is a no-op, so existing regeneration behavior is preserved. The actual _pb2.py files are NOT regenerated in this commit — the protobufs submodule is auto-regenerated by CI workflows (per copilot instructions: 'Never edit _pb2.py or _pb2.pyi files directly. Regenerate with: make protobufs or ./bin/regen-protobufs.sh'). The next CI run will pick up the new injection step. Adapted to develop's variable style (${SEDCMD[@]} array, ${PROTOC} variable) rather than master's hardcoded paths. Upstream commit: 89d81c9 Original author: Ian McEwen <ian@ianmcorvidae.net>
Ports meshtastic/tests/test_inject_nanopb_options.py from upstream master 280323d. The test file has two parts: Part 1 (test_parse_*, test_inject_*): unit-tests the script's logic directly using small synthetic proto snippets and a tmp_path-based test harness. Covers parse_value, parse_options_file, apply_options, and inject. Part 2 (test_descriptor_*): smoke-tests the already-generated _pb2.py files to confirm the regen pipeline embedded the expected nanopb options. These will pass once the next CI protobuf regeneration runs (the protobufs submodule is auto-regenerated by .github/workflows/update_protobufs.yml, which now invokes the updated bin/regen-protobufs.sh). Includes a7d13eb's three hypothesis property-based tests for parse_value: - test_parse_value_any_integer_returns_int: any int string round-trips - test_parse_value_never_crashes: no input crashes the parser - test_parse_value_non_numeric_non_bool_returns_str: non-numeric non-bool strings pass through unchanged The hypothesis dependency is already in pyproject.toml per the project's copilot-instructions.md tech stack. The test file loads bin/inject_nanopb_options.py at test time via importlib.util.spec_from_file_location, so it does not require the script to be on PYTHONPATH. Upstream commits: 280323d (test file) + a7d13eb (hypothesis tests) Original author: Ian McEwen <ian@ianmcorvidae.net>
Makes merge-master-2026-06 a strict descendant of upstream/master so 'git diff upstream/master..HEAD' returns 0 (0 behind) regardless of how many commits upstream has made since this branch was last synced. The fork intentionally does not take upstream's file changes here; each upstream commit has been evaluated individually (cherry-picked where it applies cleanly, manual-ported where restructuring was needed, and skipped where develop's existing implementations supersede). This commit only records the upstream-ancestor relationship so the branch can be merged back onto master cleanly when the user is ready for the future develop->master merge. Upstream: https://github.com/meshtastic/python @ 8f0faf5 Strategy: -s ours (keep our tree, take their second parent) See .omo/plans/merge-master-2026-06-handoff.md (local only) for the full reconciliation history and per-commit decisions.
There was a problem hiding this comment.
Code Review
This pull request introduces containerization support with Dockerfiles, a script to inject nanopb options into protobuf files, several example scripts, and updates to the CLI --reply feature to reply on the channel where messages were received. The feedback points out critical infinite loop issues in the auto-reply example and CLI feature due to a lack of local sender filtering, potential file encoding issues on Windows, a robust import parsing improvement in the injection script, and minor consistency improvements in the Containerfiles.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
…test, fix lint - Regenerated all _pb2.py files via bin/regen-protobufs.sh to embed nanopb options (max_size, max_count, int_size) in protobuf descriptors. The inject script was already integrated in commit 1e65a62 but _pb2.py files hadn't been regenerated since then. Fixes 10 test_descriptor_* test failures. - test_main_onReceive_with_text: set args.reply=True and args.ch_index=None explicitly. MagicMock's default __int__ returns 1, making targetChannel=1 != rxChannel=0, so the reply was being silently filtered by the --ch-index logic (cherry-picked from upstream c8b1b8e). Fixes the 'Ignored message on channel 0' → 'Sending reply' assertion. - test_inject_nanopb_options.py: fix ruff lint (unused imports, docstring caps, ambiguous 'l' vars, unused 'lines'/'user_line' variables). - __main__.py: fix trailing whitespace (pylint C0303). Each update should complete the CI checks.
Critical:
- examples/replymessage.py: prevent infinite auto-reply loop by filtering
self-sent messages (from == my_node_num) and auto-reply echoes
(text starts with "got msg '")
- __main__.py --reply: same loop prevention — without this, a node
running --reply would reply to its own replies, spamming the mesh
Medium:
- inject_nanopb_options.py: import detection now matches lines with
trailing comments (uses ";" in line instead of endswith(";"))
- inject_nanopb_options.py: explicit encoding='utf-8' on file open
for Windows compatibility
- examples/textchat.py: filter local echo messages so user doesn't
see their own messages duplicated
- Containerfile.alpine: add --no-directory flag to match Debian
variant and avoid unnecessary local copy into system site-packages
Also fixes ruff lint (D400/D401/D403 docstring style) in touched files.
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
WalkthroughThis PR introduces multi-architecture container support via Docker and CI, implements a nanopb option injection tool for enhanced protobuf descriptors, adds five comprehensive example scripts with contribution guidelines, and improves message reply behavior to route responses on the received message's channel with optional channel-index filtering. ChangesContainer Infrastructure and CI
Protobuf Nanopb Injection Tooling
Example Scripts and Reply Behavior Enhancement
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
Actionable comments posted: 12
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
examples/textchat.py (1)
32-77:⚠️ Potential issue | 🟠 Major | ⚡ Quick winMove the executable script body behind
main()and a__name__guard.Right now an import triggers argument parsing, transport connection, pubsub registration, and a blocking stdin loop. That makes the example hostile to reuse and very hard to exercise from tests.
Suggested structure
+def main() -> int: + parser = argparse.ArgumentParser(description="Meshtastic text chat demo") + group = parser.add_mutually_exclusive_group() + group.add_argument("--host", help="Connect via TCP to this hostname or IP") + group.add_argument("--ble", help="Connect via BLE to this MAC address or device name") + args = parser.parse_args() + + pub.subscribe(onReceive, "meshtastic.receive") + pub.subscribe(onConnection, "meshtastic.connection.established") + + iface = None + try: + if args.host: + iface = meshtastic.tcp_interface.TCPInterface(hostname=args.host, timeout=10) + elif args.ble: + iface = meshtastic.ble_interface.BLEInterface(address=args.ble, timeout=10) + else: + iface = meshtastic.serial_interface.SerialInterface(timeout=10) + + assert iface is not None + while True: + line = input() + if line: + iface.sendText(line) + except KeyboardInterrupt: + return 0 + except EOFError: + return 0 + except Exception as exc: + print(f"Error: Could not connect. {exc}") + return 1 + finally: + if iface: + iface.close() + +if __name__ == "__main__": + raise SystemExit(main())🤖 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 `@examples/textchat.py` around lines 32 - 77, The script currently runs argument parsing, pub.subscribe, connection logic and the stdin loop at import time; wrap that executable body into a new main() function that performs parser = argparse.ArgumentParser(...), pub.subscribe(onReceive, "meshtastic.receive") and pub.subscribe(onConnection, "meshtastic.connection.established"), builds the iface (using meshtastic.tcp_interface.TCPInterface / BLEInterface / SerialInterface with the same try/except logic), runs the input loop calling iface.sendText(line), and closes iface in finally; then add the standard guard if __name__ == "__main__": raise SystemExit(main()) (or call main()) so imports no longer trigger side effects and tests can import symbols like onReceive/onConnection without starting the connection or blocking stdin.examples/replymessage.py (1)
39-79:⚠️ Potential issue | 🟠 Major | ⚡ Quick winWrap the executable flow in
main()and a__name__ == "__main__"guard.Importing this module currently parses CLI args, opens a transport, and blocks in the sleep loop. That makes the example unsafe to import from tests, docs, or other examples.
Suggested structure
+def main() -> int: + parser = argparse.ArgumentParser(description="Meshtastic Auto-Reply Feature Demo") + group = parser.add_mutually_exclusive_group() + group.add_argument("--host", help="Connect via TCP to this hostname or IP") + group.add_argument("--ble", help="Connect via BLE to this MAC address") + args = parser.parse_args() + + pub.subscribe(onReceive, "meshtastic.receive") + pub.subscribe(onConnection, "meshtastic.connection.established") + + iface = None + try: + if args.host: + iface = meshtastic.tcp_interface.TCPInterface(hostname=args.host, timeout=10) + elif args.ble: + iface = meshtastic.ble_interface.BLEInterface(address=args.ble, timeout=10) + else: + iface = meshtastic.serial_interface.SerialInterface(timeout=10) + + while True: + time.sleep(1) + except KeyboardInterrupt: + return 0 + except Exception as exc: + print(f"Error: Could not connect. {exc}") + return 1 + finally: + if iface: + iface.close() + +if __name__ == "__main__": + raise SystemExit(main()) - -parser = argparse.ArgumentParser(description="Meshtastic Auto-Reply Feature Demo") -group = parser.add_mutually_exclusive_group() -group.add_argument("--host", help="Connect via TCP to this hostname or IP") -group.add_argument("--ble", help="Connect via BLE to this MAC address") - -args = parser.parse_args() - -pub.subscribe(onReceive, "meshtastic.receive") -pub.subscribe(onConnection, "meshtastic.connection.established") - -iface: Optional[Union[ - meshtastic.tcp_interface.TCPInterface, - meshtastic.ble_interface.BLEInterface, - meshtastic.serial_interface.SerialInterface -]] = None - -# defaults to serial, use --host for TCP or --ble for Bluetooth -try: - if args.host: - iface = meshtastic.tcp_interface.TCPInterface(hostname=args.host, timeout=10) - elif args.ble: - iface = meshtastic.ble_interface.BLEInterface(address=args.ble, timeout=10) - else: - iface = meshtastic.serial_interface.SerialInterface(timeout=10) -except KeyboardInterrupt as exc: - raise SystemExit(0) from exc -except Exception as e: - print(f"Error: Could not connect. {e}") - raise SystemExit(1) from e - -try: - while True: - time.sleep(1) -except KeyboardInterrupt: - pass -finally: - if iface: - iface.close()🤖 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 `@examples/replymessage.py` around lines 39 - 79, The script's top-level executable flow (argparse parser, pub.subscribe calls, connection logic that assigns iface, the main sleep loop, and the final iface.close()) should be moved into a new main() function (e.g., def main():) so importing the module doesn't execute it; keep references to parser, args, onReceive, onConnection, and iface inside that main. Wrap the call to main() behind the standard guard if __name__ == "__main__": main(), and preserve the try/except/finally structure that handles connection exceptions, KeyboardInterrupt, and ensures iface.close() in the finally block. Ensure pub.subscribe(...) calls remain before opening the interface, and that any variables needed outside main are either returned or kept local to avoid side effects on import.
🤖 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.
Inline comments:
In @.github/workflows/container-build.yaml:
- Around line 34-37: The checkout step using actions/checkout@v3 currently sets
fetch-depth: 0 but does not disable persisted credentials; update the "Checkout
repository" step (the actions/checkout@v3 usage) to include persist-credentials:
false so credentials are not automatically reused by later steps, keeping
fetch-depth as needed and adding a clear comment if desired.
- Line 20: Remove the job-level continue-on-error: true setting for the
container build job so failures surface instead of being masked; locate the
container build job block that contains the literal continue-on-error: true and
delete that line (or set it to false only for specific steps if you intend
tolerable step-level failures), ensuring the workflow fails on build errors and
prevents publishing regressions.
- Around line 35-70: Update each GitHub Actions "uses:" reference to pin to an
exact commit SHA rather than floating major tags: replace actions/checkout@v3,
docker/setup-qemu-action@v2, docker/setup-buildx-action@v2,
docker/login-action@v2, docker/metadata-action@v4, and
docker/build-push-action@v4 with the corresponding @<commit-sha> for the release
you want (you may still bump the major if desired but ensure the ref is the
commit SHA), committing the updated refs so the workflow uses immutable action
versions.
In `@bin/inject_nanopb_options.py`:
- Around line 259-266: The code in the loop over specific stops at the first
matching key due to the break, causing later/more-specific matches (e.g.,
("Link","uid") and ("Route","Link","uid")) to be ignored; update the loop in
inject_nanopb_options.py to merge all matching specific scopes by removing the
break and continuing to call extra.update(opts) for every key where key[-1] ==
fname and message_path_matches(context_stack, key[:-1]); this ensures all
applicable opts are applied (later updates will override earlier keys by dict
update semantics).
- Line 24: Replace typing.Dict/ List/ Tuple usage with built-in generics and
adjust the import: remove Dict, List, Tuple from the top import and use
dict[...], list[...], tuple[...] in all annotations (leave typing.Any or import
Any only if still needed). Update every occurrence flagged (top import and the
annotations around the other reported locations) so signatures and variable
annotations use dict[KeyType, ValueType], list[ElementType], and tuple[Types...]
instead of typing.Dict/ List/ Tuple.
In `@Containerfile.alpine`:
- Around line 8-24: The image currently runs as root because no USER is set;
create a non-root user (e.g., appuser) and group, chown the application
directory and the entrypoint file to that user, ensure the entrypoint at "/init"
is executable by that user (fix permissions), and add a USER directive (e.g.,
USER appuser) before the ENTRYPOINT to switch to the non-root account; reference
the Dockerfile symbols WORKDIR /usr/local/app, COPY
"./bin/container-entrypoint.sh" "/init", and ENTRYPOINT [ "/init" ] when making
these changes.
In `@Containerfile.debian`:
- Around line 8-24: Runtime defaults to root; add a non-root user and switch to
it before ENTRYPOINT so the container doesn't run as root. Create a dedicated
user/group (e.g., appuser/appgroup) after the RUN steps that install
dependencies, chown /usr/local/app and the init script /init to that user,
ensure /init remains executable, and set the image USER to that non-root account
before the existing ENTRYPOINT; reference the existing RUN that prepares the
app, the copied "/init" and the ENTRYPOINT [ "/init" ] when locating where to
apply these changes.
In `@examples/CONTRIBUTING.md`:
- Around line 5-13: Add a new checklist item to the existing "Must-have
checklist before opening a PR" that requires repository Python typing standards:
require type hints on all new/edited Python code and mandate PEP 604 union
syntax (e.g., X | None) and built-in generics (e.g., dict[K, V], list[T],
tuple[T, ...]) for annotations; mention both the general rule ("Add type hints
to all new code") and the concrete syntax expectations so authors know to use
modern annotation forms when updating or adding .py files (update the checklist
near the other numbered items to keep ordering consistent).
In `@examples/replymessage.py`:
- Around line 31-33: The reply is being sent on the default channel via
interface.sendText(reply), causing cross-channel noise; change the call to send
the reply on the same channel the message arrived on by passing the received
channel variable (e.g., rx_channel or channel) to the send method or use the API
variant that targets a specific channel (look for sendText(..., channel=...) or
sendTextOnChannel) so that reply is sent on the incoming message's channel
rather than the default.
In `@meshtastic/__main__.py`:
- Around line 778-783: The reply path currently uses direct indexing
packet["rxSnr"] and packet["hopLimit"] which will raise KeyError for text
packets missing those fields; update the reply construction in
meshtastic/__main__.py to use tolerant lookups like packet.get("rxSnr") and
packet.get("hopLimit") (or a default such as None or "N/A") when building reply
and logging (references: variables rxSnr, hopLimit, msg, rxChannel and the call
interface.sendText), so missing metadata does not crash and the reply still
sends.
- Around line 775-777: The current code maps targetChannel = int(args.ch_index
or 0) which treats an unset --ch-index as 0 and therefore wrongly filters
replies to channel 0; instead treat an unset args.ch_index as "no explicit
target" (e.g., None) and only apply the channel filter when args.ch_index is
provided. Update the logic around rxChannel and targetChannel (the variables and
the conditional that uses them) so that if args.ch_index is None you do not
enforce rxChannel == targetChannel, and if args.ch_index is provided you compare
rxChannel to int(args.ch_index) to decide whether to process/reply.
In `@meshtastic/tests/test_inject_nanopb_options.py`:
- Around line 27-32: Add missing type annotations: annotate _load_inject_module
to return types.ModuleType; update _inject to type its optional parameters as
dict[str, Any] | None (use builtin generics and the X | None syntax for specific
and wildcard); and annotate _field_opts so its descriptor parameter is typed as
Any and its variadic *path parameters are typed as str (i.e., descriptor: Any,
*path: str). Reference the functions _load_inject_module, _inject, and
_field_opts when making these edits.
---
Outside diff comments:
In `@examples/replymessage.py`:
- Around line 39-79: The script's top-level executable flow (argparse parser,
pub.subscribe calls, connection logic that assigns iface, the main sleep loop,
and the final iface.close()) should be moved into a new main() function (e.g.,
def main():) so importing the module doesn't execute it; keep references to
parser, args, onReceive, onConnection, and iface inside that main. Wrap the call
to main() behind the standard guard if __name__ == "__main__": main(), and
preserve the try/except/finally structure that handles connection exceptions,
KeyboardInterrupt, and ensures iface.close() in the finally block. Ensure
pub.subscribe(...) calls remain before opening the interface, and that any
variables needed outside main are either returned or kept local to avoid side
effects on import.
In `@examples/textchat.py`:
- Around line 32-77: The script currently runs argument parsing, pub.subscribe,
connection logic and the stdin loop at import time; wrap that executable body
into a new main() function that performs parser = argparse.ArgumentParser(...),
pub.subscribe(onReceive, "meshtastic.receive") and pub.subscribe(onConnection,
"meshtastic.connection.established"), builds the iface (using
meshtastic.tcp_interface.TCPInterface / BLEInterface / SerialInterface with the
same try/except logic), runs the input loop calling iface.sendText(line), and
closes iface in finally; then add the standard guard if __name__ == "__main__":
raise SystemExit(main()) (or call main()) so imports no longer trigger side
effects and tests can import symbols like onReceive/onConnection without
starting the connection or blocking stdin.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 4dcd871f-b1c5-441c-9d15-b7746b3a3021
📒 Files selected for processing (59)
.dockerignore.github/workflows/container-build.yamlContainerfileContainerfile.alpineContainerfile.debianDockerfilebin/container-entrypoint.shbin/inject_nanopb_options.pybin/regen-protobufs.shexamples/CONTRIBUTING.mdexamples/meshtastic_serial_message_reader.pyexamples/replymessage.pyexamples/tcp_connection_info_once.pyexamples/tcp_pubsub_send_and_receive.pyexamples/textchat.pymeshtastic/__main__.pymeshtastic/protobuf/admin_pb2.pymeshtastic/protobuf/admin_pb2.pyimeshtastic/protobuf/apponly_pb2.pymeshtastic/protobuf/apponly_pb2.pyimeshtastic/protobuf/atak_pb2.pymeshtastic/protobuf/atak_pb2.pyimeshtastic/protobuf/cannedmessages_pb2.pymeshtastic/protobuf/cannedmessages_pb2.pyimeshtastic/protobuf/channel_pb2.pymeshtastic/protobuf/channel_pb2.pyimeshtastic/protobuf/clientonly_pb2.pymeshtastic/protobuf/config_pb2.pymeshtastic/protobuf/config_pb2.pyimeshtastic/protobuf/connection_status_pb2.pymeshtastic/protobuf/connection_status_pb2.pyimeshtastic/protobuf/device_ui_pb2.pymeshtastic/protobuf/device_ui_pb2.pyimeshtastic/protobuf/deviceonly_pb2.pymeshtastic/protobuf/deviceonly_pb2.pyimeshtastic/protobuf/interdevice_pb2.pymeshtastic/protobuf/localonly_pb2.pyimeshtastic/protobuf/mesh_pb2.pymeshtastic/protobuf/mesh_pb2.pyimeshtastic/protobuf/module_config_pb2.pymeshtastic/protobuf/module_config_pb2.pyimeshtastic/protobuf/mqtt_pb2.pymeshtastic/protobuf/mqtt_pb2.pyimeshtastic/protobuf/nanopb_pb2.pyimeshtastic/protobuf/paxcount_pb2.pyimeshtastic/protobuf/powermon_pb2.pyimeshtastic/protobuf/remote_hardware_pb2.pyimeshtastic/protobuf/rtttl_pb2.pymeshtastic/protobuf/rtttl_pb2.pyimeshtastic/protobuf/serial_hal_pb2.pymeshtastic/protobuf/serial_hal_pb2.pyimeshtastic/protobuf/storeforward_pb2.pymeshtastic/protobuf/storeforward_pb2.pyimeshtastic/protobuf/telemetry_pb2.pymeshtastic/protobuf/telemetry_pb2.pyimeshtastic/protobuf/xmodem_pb2.pymeshtastic/protobuf/xmodem_pb2.pyimeshtastic/tests/test_inject_nanopb_options.pymeshtastic/tests/test_main.py
container-build.yaml: - Remove continue-on-error: true (was masking build failures) - Add persist-credentials: false to checkout step - Pin all 6 action refs to immutable commit SHAs Containerfile.alpine + Containerfile.debian: - Add non-root user (meshtastic) before ENTRYPOINT bin/inject_nanopb_options.py: - Replace typing.Dict/List/Tuple with PEP 585 built-in generics - Fix scope merge bug: remove break so all matching specific keys get merged (shortest path first, more-specific overrides) meshtastic/tests/test_inject_nanopb_options.py: - Add type annotations to _load_inject_module, _inject, _field_opts examples/replymessage.py: - Send reply on received channel (channelIndex=) not default - Wrap executable code in main() with __name__ guard - Use PEP 604 unions (X | None) instead of Optional/Union examples/textchat.py: - Wrap executable code in main() with __name__ guard - Use PEP 604 unions instead of Optional/Union examples/CONTRIBUTING.md: - Add checklist item 8: type hints + PEP 604/built-in generics meshtastic/__main__.py: - Fix --reply channel filter: unset --ch-index now means any channel (was incorrectly defaulting to channel 0) - Use .get() with fallback for rxSnr/hopLimit (crash on missing fields)
- test_inject_nanopb_options.py: add assert guards for None checks on spec_from_file_location return (mypy arg-type/union-attr) - inject_nanopb_options.py main(): add docstring, encoding='utf-8' on read_text/write_text (pylint missing-docstring, unspecified-encoding) - examples/replymessage.py main(): add docstring (pylint C0116) - examples/textchat.py main(): add docstring (pylint C0116)
Apply consistent code formatting across examples, main entry point, and tests to improve readability and adhere to style guidelines. Update GitHub Actions workflow to use double quotes for tag patterns.
…inism Container workflow (.github/workflows/container-build.yaml): - Add 'develop' to push and pull_request branch triggers - Remove linux/arm/v7 and linux/arm/v6 platforms (upstream build issues) - Update autotag logic to use 'auto' for any branch push Examples (replymessage.py, textchat.py): - Type packet parameter as dict[str, Any] instead of bare dict - Add Interface type alias to replace verbose multi-line union types - Use 'exc' naming for caught exceptions (pylint W0707) - Move pylint disable comment to correct line for onConnection inject_nanopb_options.py: - Sort option keys in format_nanopb_opts() for deterministic output across Python versions and option file ordering New tests — onReceive reply behavior (test_main.py, 4 tests): - test_main_onReceive_reply_uses_rx_channel: verifies channelIndex=packet channel - test_main_onReceive_ch_index_filter_mismatch: verifies --ch-index mismatch ignored - test_main_onReceive_own_packet_no_reply: verifies own-node packets skipped - test_main_onReceive_auto_reply_echo_no_reply: verifies 'got msg' prefix skipped New tests — inject_nanopb_options edge cases (7 tests): - Comments with braces don't corrupt nesting - Map<> fields are skipped (not injected) - Oneof fields receive options correctly - Enum value lines are not modified - format_nanopb_opts output is sorted/deterministic - Duplicate specific/wildcard options merge deterministically - Multiple close braces on one line handled correctly Protobuf reproducibility verified: regen-protobufs.sh produces clean diff. Pre-existing failures: test_main_init_parser_version and test_main_main_version fail due to version detection in dev environment (not related to this change).
container-build.yaml: - Fix critical YAML duplicate key bug: merge two pull_request blocks into one with both master and develop branches - Remove invalid 'tags' subkey from pull_request event version.py: - Add PACKAGE_NAME, PROJECT_DISPLAY_NAME, INSTALL_UPGRADE_HINT constants - Single source of truth for project identity; swap PACKAGE_NAME to 'meshtastic' when upstreaming util.py: - Import DISTRIBUTION_NAME_CANDIDATES from version.py instead of duplicating the definition __main__.py: - Use PROJECT_DISPLAY_NAME and INSTALL_UPGRADE_HINT from version.py - Upgrade hint now recommends 'pipx upgrade mtjk' instead of 'pip install --upgrade meshtastic' ble_interface.py, interfaces/ble/__init__.py: - Update BLE bleak install error to recommend 'pipx install mtjk'
Only builds Containerfile.alpine when running in meshtastic/python. Any fork (including mtjk) automatically skips it. One line to remove when upstreaming.
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
Update the comment in the container build workflow to clarify that the Alpine build is restricted to the upstream repository.
Update the job conditional to ensure that container builds are only executed within the meshtastic/python repository. This simplifies the logic previously used to selectively skip the Alpine build by applying the restriction to the entire job.
Overview
This PR merges a large set of improvements from the upstream master branch, including container infrastructure for the CLI, nanopb tooling and protobuf regeneration with embedded options, new example scripts demonstrating various use patterns, and channel-aware reply filtering for the auto-reply feature.
Key Changes
Container & Deployment
Containerfile.alpine,Containerfile.debian, and.dockerignorefor optimized buildscontainer-build.yaml) to automatically build and publish multi-architecture container images (amd64, 386, arm64, arm/v7, arm/v6) to ghcr.io on master pushes, version tags, and PRsbin/container-entrypoint.shto enable convenient docker runs without manual--entrypointspecificationContainerfile→Containerfile.alpine,Dockerfile→ContainerfileNanopb Tooling & Protobuf Updates
bin/inject_nanopb_options.py: a CLI tool that injects nanopb constraints from.optionsfiles into matching protobuf field declarations, handling message-specific and wildcard scoping, nested paths, and import managementbin/regen-protobufs.shto copy.optionsfiles and invoke the injection tool before runningprotoc, ensuring generated descriptors include nanopb metadata*_pb2.pyand*_pb2.pyifiles) to embed nanopb options in descriptor serializationtyping.TypeAliasfor Python ≥3.10 (previously 3.11+), and removedNever-based method stubs for unsupported field introspection APIsExamples & Documentation
examples/CONTRIBUTING.md: guidelines for adding/updating example scripts, covering docstrings, argument handling, error handling, and resource cleanupexamples/meshtastic_serial_message_reader.py: simple serial listener for incoming text messages with filtering and formattingexamples/replymessage.py: auto-reply example supporting serial, TCP, and BLE with echo-loop preventionexamples/textchat.py: interactive bidirectional text chat with multi-transport supportexamples/tcp_connection_info_once.py: TCP-only example printing device info on connectionexamples/tcp_pubsub_send_and_receive.py: demonstrates pubsub event subscription and sending over TCPAuto-Reply Improvements
meshtastic/__main__.pyonReceive()callback to filter replies by channel index (respects--ch-indexCLI argument), preventing cross-channel reply confusion--replyto document channel-specific reply behaviorTesting
meshtastic/tests/test_inject_nanopb_options.pycovering:.optionsfile parsing, value conversion, and path matchingmeshtastic/tests/test_main.pyto test reply behavior with channel index configurationNotes
The protobuf regenerations represent a structural update to support nanopb constraints at the descriptor level. All generated
.pyityping stubs were simplified by removing impossible-case method overloads (those previously typed with_Never), improving type clarity for actual use cases. Python 3.10+ is now the minimum for typing compatibility features in the generated stubs.