Skip to content

Add Docker multi-arch, nanopb tool, examples, and routing#196

Merged
jeremiah-k merged 82 commits into
developfrom
merge-master-2026-06
Jun 1, 2026
Merged

Add Docker multi-arch, nanopb tool, examples, and routing#196
jeremiah-k merged 82 commits into
developfrom
merge-master-2026-06

Conversation

@jeremiah-k

@jeremiah-k jeremiah-k commented Jun 1, 2026

Copy link
Copy Markdown
Owner

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

  • Added Docker/Containerfile infrastructure: Containerfile.alpine, Containerfile.debian, and .dockerignore for optimized builds
  • Added GitHub Actions workflow (container-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 PRs
  • Added bin/container-entrypoint.sh to enable convenient docker runs without manual --entrypoint specification
  • Created symbolic links: ContainerfileContainerfile.alpine, DockerfileContainerfile

Nanopb Tooling & Protobuf Updates

  • Added bin/inject_nanopb_options.py: a CLI tool that injects nanopb constraints from .options files into matching protobuf field declarations, handling message-specific and wildcard scoping, nested paths, and import management
  • Updated bin/regen-protobufs.sh to copy .options files and invoke the injection tool before running protoc, ensuring generated descriptors include nanopb metadata
  • Regenerated all protobuf Python modules (*_pb2.py and *_pb2.pyi files) to embed nanopb options in descriptor serialization
  • Updated typing stubs across all protobuf modules to use typing.TypeAlias for Python ≥3.10 (previously 3.11+), and removed Never-based method stubs for unsupported field introspection APIs

Examples & Documentation

  • Added examples/CONTRIBUTING.md: guidelines for adding/updating example scripts, covering docstrings, argument handling, error handling, and resource cleanup
  • Added examples/meshtastic_serial_message_reader.py: simple serial listener for incoming text messages with filtering and formatting
  • Added examples/replymessage.py: auto-reply example supporting serial, TCP, and BLE with echo-loop prevention
  • Added examples/textchat.py: interactive bidirectional text chat with multi-transport support
  • Added examples/tcp_connection_info_once.py: TCP-only example printing device info on connection
  • Added examples/tcp_pubsub_send_and_receive.py: demonstrates pubsub event subscription and sending over TCP

Auto-Reply Improvements

  • Enhanced meshtastic/__main__.py onReceive() callback to filter replies by channel index (respects --ch-index CLI argument), preventing cross-channel reply confusion
  • Added safeguards to prevent replying to the device's own messages and to avoid echo loops by ignoring replies starting with "got msg '"
  • Updated CLI help text for --reply to document channel-specific reply behavior

Testing

  • Added comprehensive test suite meshtastic/tests/test_inject_nanopb_options.py covering:
    • Parser unit tests for .options file parsing, value conversion, and path matching
    • Injection tests validating correct field option embedding, import insertion, and nested message handling
    • Integration tests verifying nanopb options appear in generated protobuf descriptors
  • Updated meshtastic/tests/test_main.py to test reply behavior with channel index configuration

Notes

The protobuf regenerations represent a structural update to support nanopb constraints at the descriptor level. All generated .pyi typing 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.

henri and others added 30 commits February 19, 2025 17:48
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>
 * 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.
…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
oliv3r and others added 7 commits June 1, 2026 09:31
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.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Comment thread examples/replymessage.py Outdated
Comment thread meshtastic/__main__.py Outdated
Comment thread bin/inject_nanopb_options.py
Comment thread bin/inject_nanopb_options.py Outdated
Comment thread examples/textchat.py Outdated
Comment thread Containerfile.alpine Outdated
@codecov

codecov Bot commented Jun 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

📢 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.
@jeremiah-k jeremiah-k changed the title @sourcery-ai title @coderabbitai Jun 1, 2026
@jeremiah-k

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown
✅ Actions performed

Full review triggered.

@coderabbitai coderabbitai Bot changed the title @coderabbitai Add Docker multi-arch, nanopb tool, examples, and routing Jun 1, 2026
@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The head commit changed during the review from 57c51ca to bdfe27e.

Walkthrough

This 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.

Changes

Container Infrastructure and CI

Layer / File(s) Summary
Docker build files and entrypoint
Containerfile, Containerfile.alpine, Containerfile.debian, Dockerfile, bin/container-entrypoint.sh
Containerfile and Dockerfile use symlinks; Alpine and Debian variants install dependencies via Poetry in temporary virtualenvs; custom entrypoint script enables flexible command execution with meshtastic binary fallback.
Docker build CI workflow and ignore rules
.dockerignore, .github/workflows/container-build.yaml
.dockerignore uses allowlist-based ignore pattern; workflow builds and publishes multi-platform images (amd64, 386, arm64, arm/v7, arm/v6) to ghcr.io on master pushes, version tags, and PRs using Buildx with QEMU support.

Protobuf Nanopb Injection Tooling

Layer / File(s) Summary
Nanopb option injection script and core logic
bin/inject_nanopb_options.py
New CLI tool reads .options files, parses wildcard and message-path-specific constraints, tracks nested scopes, injects nanopb field annotations via regex-based matching, formats options into proto syntax, and conditionally inserts the nanopb import after existing imports or after syntax declaration.
Protobuf regeneration script integration
bin/regen-protobufs.sh
Updated to copy .options files into the temporary build directory and invoke the injection script on matching .proto files before running protoc.
Comprehensive nanopb injection tests
meshtastic/tests/test_inject_nanopb_options.py
Extensive unit tests for parse_value, parse_options_file, message_path_matches, and format_nanopb_opts; integration tests verify nanopb options in generated descriptors for multiple message/field paths.
Regenerated protobuf descriptor files
meshtastic/protobuf/*_pb2.py, meshtastic/protobuf/*_pb2.pyi
All generated files updated with nanopb imports, regenerated serialized descriptors, revised typing imports (Python ≥3.10 for TypeAlias), and removed _Never-typed HasField/WhichOneof unreachable stubs.

Example Scripts and Reply Behavior Enhancement

Layer / File(s) Summary
Example scripts demonstrating Meshtastic usage
examples/meshtastic_serial_message_reader.py, examples/replymessage.py, examples/tcp_connection_info_once.py, examples/tcp_pubsub_send_and_receive.py, examples/textchat.py
Five new example scripts covering serial/TCP/BLE transports, pubsub patterns, passive monitoring, auto-reply, interactive chat, and connection info retrieval; each includes proper error handling, resource cleanup via finally blocks, and optional transport selection.
Example contribution guidelines
examples/CONTRIBUTING.md
Documentation guide for adding/updating examples: defines PR checklist (learning goals, docstrings, error handling, argument parsing), transport scope guidance, resource cleanup requirements, and naming conventions.
Message reply channel routing and filtering
meshtastic/__main__.py
Updated onReceive() to route replies on the received message's channel, adds self-message and echo-loop detection, implements optional --ch-index filtering, and expands --reply help text to document routing behavior. Updated test setup to initialize args.reply and args.ch_index.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • jeremiah-k/mtjk#114: Also modifies bin/regen-protobufs.sh for protobuf build logic changes.
  • jeremiah-k/mtjk#84: Updates nanopb/protoc regeneration machinery that this PR builds upon for constraint injection.
  • jeremiah-k/mtjk#115: Adds workflow that invokes the updated bin/regen-protobufs.sh script.

Poem

🐰 Containerize with Alpine cheer,
Inject constraints without fear,
Five examples light the way,
Replies find their rightful bay,
Protobuf's future, bright and clear!

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch merge-master-2026-06

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 win

Move 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 win

Wrap 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

📥 Commits

Reviewing files that changed from the base of the PR and between 2388333 and 922be32.

📒 Files selected for processing (59)
  • .dockerignore
  • .github/workflows/container-build.yaml
  • Containerfile
  • Containerfile.alpine
  • Containerfile.debian
  • Dockerfile
  • bin/container-entrypoint.sh
  • bin/inject_nanopb_options.py
  • bin/regen-protobufs.sh
  • examples/CONTRIBUTING.md
  • examples/meshtastic_serial_message_reader.py
  • examples/replymessage.py
  • examples/tcp_connection_info_once.py
  • examples/tcp_pubsub_send_and_receive.py
  • examples/textchat.py
  • meshtastic/__main__.py
  • meshtastic/protobuf/admin_pb2.py
  • meshtastic/protobuf/admin_pb2.pyi
  • meshtastic/protobuf/apponly_pb2.py
  • meshtastic/protobuf/apponly_pb2.pyi
  • meshtastic/protobuf/atak_pb2.py
  • meshtastic/protobuf/atak_pb2.pyi
  • meshtastic/protobuf/cannedmessages_pb2.py
  • meshtastic/protobuf/cannedmessages_pb2.pyi
  • meshtastic/protobuf/channel_pb2.py
  • meshtastic/protobuf/channel_pb2.pyi
  • meshtastic/protobuf/clientonly_pb2.py
  • meshtastic/protobuf/config_pb2.py
  • meshtastic/protobuf/config_pb2.pyi
  • meshtastic/protobuf/connection_status_pb2.py
  • meshtastic/protobuf/connection_status_pb2.pyi
  • meshtastic/protobuf/device_ui_pb2.py
  • meshtastic/protobuf/device_ui_pb2.pyi
  • meshtastic/protobuf/deviceonly_pb2.py
  • meshtastic/protobuf/deviceonly_pb2.pyi
  • meshtastic/protobuf/interdevice_pb2.py
  • meshtastic/protobuf/localonly_pb2.pyi
  • meshtastic/protobuf/mesh_pb2.py
  • meshtastic/protobuf/mesh_pb2.pyi
  • meshtastic/protobuf/module_config_pb2.py
  • meshtastic/protobuf/module_config_pb2.pyi
  • meshtastic/protobuf/mqtt_pb2.py
  • meshtastic/protobuf/mqtt_pb2.pyi
  • meshtastic/protobuf/nanopb_pb2.pyi
  • meshtastic/protobuf/paxcount_pb2.pyi
  • meshtastic/protobuf/powermon_pb2.pyi
  • meshtastic/protobuf/remote_hardware_pb2.pyi
  • meshtastic/protobuf/rtttl_pb2.py
  • meshtastic/protobuf/rtttl_pb2.pyi
  • meshtastic/protobuf/serial_hal_pb2.py
  • meshtastic/protobuf/serial_hal_pb2.pyi
  • meshtastic/protobuf/storeforward_pb2.py
  • meshtastic/protobuf/storeforward_pb2.pyi
  • meshtastic/protobuf/telemetry_pb2.py
  • meshtastic/protobuf/telemetry_pb2.pyi
  • meshtastic/protobuf/xmodem_pb2.py
  • meshtastic/protobuf/xmodem_pb2.pyi
  • meshtastic/tests/test_inject_nanopb_options.py
  • meshtastic/tests/test_main.py

Comment thread .github/workflows/container-build.yaml Outdated
Comment thread .github/workflows/container-build.yaml
Comment thread .github/workflows/container-build.yaml Outdated
Comment thread bin/inject_nanopb_options.py Outdated
Comment thread bin/inject_nanopb_options.py Outdated
Comment thread examples/CONTRIBUTING.md
Comment thread examples/replymessage.py Outdated
Comment thread meshtastic/__main__.py Outdated
Comment thread meshtastic/__main__.py Outdated
Comment thread meshtastic/tests/test_inject_nanopb_options.py Outdated
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.
@jeremiah-k

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown
✅ Actions performed

Full 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.
@jeremiah-k jeremiah-k merged commit 9d5d9c9 into develop Jun 1, 2026
20 checks passed
@jeremiah-k jeremiah-k deleted the merge-master-2026-06 branch June 1, 2026 17:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.