Skip to content

Make examples more regularized and focused, and add contribution guidelines for the examples folder#928

Merged
ianmcorvidae merged 4 commits into
meshtastic:masterfrom
ianmcorvidae:examples
Jun 1, 2026
Merged

Make examples more regularized and focused, and add contribution guidelines for the examples folder#928
ianmcorvidae merged 4 commits into
meshtastic:masterfrom
ianmcorvidae:examples

Conversation

@ianmcorvidae
Copy link
Copy Markdown
Contributor

Includes the example from #739 as part of this.

This should mean a little less anything-goes in the examples folder, hopefully. Probably still more we could do to make that nice, but this is an okay start.

@codecov
Copy link
Copy Markdown

codecov Bot commented Jun 1, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 62.41%. Comparing base (ff26f97) to head (81ae8b6).

Additional details and impacted files
@@           Coverage Diff           @@
##           master     #928   +/-   ##
=======================================
  Coverage   62.41%   62.41%           
=======================================
  Files          25       25           
  Lines        4497     4497           
=======================================
  Hits         2807     2807           
  Misses       1690     1690           
Flag Coverage Δ
unittests 62.41% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ianmcorvidae ianmcorvidae merged commit d1f3552 into meshtastic:master Jun 1, 2026
13 checks passed
@ianmcorvidae ianmcorvidae deleted the examples branch June 1, 2026 00:59
jeremiah-k added a commit to jeremiah-k/mtjk that referenced this pull request Jun 1, 2026
…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>
jeremiah-k added a commit to jeremiah-k/mtjk that referenced this pull request Jun 1, 2026
* Added example script : meshtastic_serial_message_reader.py

* Updates and bug fixes meshtastic_serial_message_reader.py

* Adjusting old deprecation test and exception for non-abstract use of StreamInterface

* Removing superfluous setting of noProto in init()

* Updating test for change of deprecation exception

* Shifting serial interface connection parts from __init__() into connect() method

* Removing unnecessary initialization of self.stream in TCPInterface

* Reorganising connect method calls for when connectNow is false

* Linting adjustment for change to StreamInterface non-abstract use check

* Container: Add initial container for meshtastic-cli

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>

* Filter --reply based on specified channel index 

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.

* fix(ble): handle BLEError with user-friendly messages

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>

* fix(cli): add timeout error handling for serial connections

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>

* Refactor the Meshtastic TCP pub/sub example to ensure proper resource cleanup and clearer exception handling.

* Give TCPInterface reconnect logic on write errors

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

* Fix local node position overwrite by low-precision echoes

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>

* Added textchat.py and replymessage.py example scripts from TODO

* protobufs: v2.7.21

* loosen packaging version requirement

* poetry lock

* Update factory reset to use integer for config reset

* StreamInterface: prevent socket/reader-thread leak on handshake failure 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.

* protobufs: v2.7.24

* Inject options in nanopb .options files into the protobuf files used for code generation

* Add tests of nanopb options injection

* pylint cleanups

* the fuzz

* fix import order

* Update the other factory reset to use integer too

* Document the use of --ch-index along with --reply a little

* pylint fix

* fix a missing paren in a comment

* Fix some leaks/hangs on close: unstarted StreamInterface streams & TCP reader unblock

* pylint strikes again

* pylint/test fixes

* some pre-merge cleanup

* Re-establish the OSError being raised to match former behavior, but still reconnect. TBD if this is quite the right approach.

* Avoid deadlocking on potentially re-entrant _startConfig call, and don't reconnect when _wantExit

* make test more deterministic for reconnect count testing

* Harden a bit, update some sections, add a README section

* A bunch of dependency updates in poetry.lock (to shut up Dependabot)

* tryfix container build issue

* Remove arm/v7 from the container build right now, can't be bothered

* Make examples more regularized and focused, and add contribution guidelines for the examples folder

* Add a BLEError 'kind' field and branch on it instead of string matching

* tests of new BLEError functionality

* More lockfile updates now that dependabot woke up

* One more dependency update

* Filter --reply based on specified channel index 

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.

* Container: Add initial container for meshtastic-cli

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>

* Document the use of --ch-index along with --reply a little

* Add new example scripts and contribution guidelines from upstream meshtastic#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>

* Add bin/inject_nanopb_options.py for nanopb options injection

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>

* Integrate nanopb options injection into bin/regen-protobufs.sh

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>

* Add tests for bin/inject_nanopb_options.py (nanopb options injection)

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>

* Fix CI: regenerate protobufs with nanopb injection, fix reply filter 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.

* Apply code review fixes (2 critical, 4 medium)

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.

* Apply 13 review findings: security, correctness, style

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)

* Fix mypy + pylint failures on review changes

- 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)

* style: reformat code and update CI workflow

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.

* Address review feedback: container CI, examples polish, tests, determinism

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

* Fix duplicate pull_request key, centralize project name constants

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'

* Remove Containerfile.alpine from container build matrix

* Restore alpine matrix entry, skip build outside upstream repo

Only builds Containerfile.alpine when running in meshtastic/python.
Any fork (including mtjk) automatically skips it. One line to remove
when upstreaming.

* ci(container): update alpine build conditional comment

Update the comment in the container build workflow to clarify that the Alpine build is restricted to the upstream repository.

* refactor(ci): restrict all container builds to 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.

---------

Signed-off-by: Olliver Schinagl <oliver@schinagl.nl>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
Co-authored-by: henri <github.shustak@neverbox.com>
Co-authored-by: Travis-L-R <>
Co-authored-by: Olliver Schinagl <oliver@schinagl.nl>
Co-authored-by: Rob <95710162+thatSFguy@users.noreply.github.com>
Co-authored-by: Aleksei Sviridkin <f@lex.la>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: dim5x <dim5x@yahoo.com>
Co-authored-by: Stephen Thorne <stephen@thorne.id.au>
Co-authored-by: Benjamin Babeshkin <skypanther@gmail.com>
Co-authored-by: Aron Tkachuk <arotkac@icloud.com>
Co-authored-by: Ian McEwen <ian@ianmcorvidae.net>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: nightjoker7 <mattdeering7@gmail.com>
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.

2 participants