Compare commits

..
372 Commits
Author SHA1 Message Date
vidplace7 efb6d1c0e9 protobufs v2.8.0 2026-09-02 13:55:22 -04:00
Ian McEwen 0539a9600f Merge pull request #961 from anton-vinogradov/ota-update-skip-node-db
Skip node DB fetch during --ota-update / --reboot-ota
2026-08-12 12:04:41 -07:00
Ian McEwen 445e17ddc0 Merge pull request #971 from ianmcorvidae/improve-update-protobufs
Improve update_protobufs workflow
2026-08-11 13:30:35 -07:00
Ian McEwen 33840e2cee Fix review comments/zizmor lint 2026-08-11 11:47:18 -07:00
Ian McEwen e45e59ad39 Improve update_protobufs workflow to use release versions, put version in commit, and allow specifying a version, as well as allowing dispatch from other repositories. 2026-08-11 11:32:44 -07:00
Ian McEwen ae710478fb update pyarrow for python 3.14 specifically, separately 2026-07-22 12:46:53 -07:00
Ian McEwen 3b084d7df2 Install with extras and dev/powermon in validate to stop build jobs sooner if there's issues in those 2026-07-22 12:18:42 -07:00
Ian McEwen 3f62c099ae More CI optimization: run tests only once, run pylint/mypy/tests in parallel with make 2026-07-22 12:14:24 -07:00
Ian McEwen a509bca86d Increase pyarrow version for a pkg_resources dependency issue 2026-07-22 11:55:04 -07:00
Ian McEwen dca0b1fced Improve CI job to fail faster/use up fewer runners when things fail 2026-07-22 11:40:34 -07:00
Ian McEwen 11b71e9587 Convey bitfield forward in firmware harness so prehop drop doesn't get mad at us 2026-07-22 11:20:19 -07:00
Anton VinogradovandClaude Opus 4.8 ef9cdc34a4 Skip node DB fetch during --ota-update / --reboot-ota
OTA only needs the local node to send the startOTA admin request and then
streams firmware directly to the loader over WiFi; it never reads the node DB.
On a device with a large node DB the initial dump can stall and the firmware
closes the API connection before it processes the OTA request, so OTA silently
fails. Imply --no-nodes for --ota-update / --reboot-ota.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 02:02:46 +03:00
github-actions 19d066970e bump version to 2.7.11 2026-07-17 17:37:04 +00:00
Ian McEwen 485d860911 use right repository name for daily simradio testing 2026-07-17 10:32:07 -07:00
Ian McEwen e6f07123f6 Merge pull request #958 from meshtastic/tcp-graceful-close
Half-close TCP connections so the last write is not lost
2026-07-17 10:29:15 -07:00
Thomas Göttgens f623732694 Half-close TCP connections so the last write is not lost
close() went straight to shutdown(SHUT_RDWR) + close(). Doing that while either
side still has unread data makes the stack send RST rather than FIN, and the
device's FromRadio stream means there is essentially always unread data.

Winsock discards data that was received but not yet read when an RST arrives, so
an admin message written moments earlier is thrown away before the device reads
it. Linux delivers the buffered bytes before reporting ECONNRESET, which is why
this only surfaced on Windows: every one-shot TCP write there (--set, --seturl,
--sendtext) reported success and did nothing.

writeConfig() does not wait for an ack, so close() runs microseconds after
sendall(). The device cannot compensate; it polls every 5ms and the RST beats its
next read.

Send FIN first with shutdown(SHUT_WR), which lets the device consume what we
wrote, wait briefly for the reader thread to drain and exit, then tear down
exactly as before. The full shutdown is kept so a reader still blocked in recv()
is unblocked for the join in StreamInterface.close(). The wait is bounded: the
device is not obliged to close just because we half-closed.

Fixes #957
2026-07-17 14:34:18 +02:00
Ian McEwen 82220ba49b Merge pull request #946 from ianmcorvidae/sim-based-testing
Initial implementation of simradio-based testing with a multi-node simulated mesh
2026-07-14 09:16:09 -07:00
Ian McEwen 4339cda6bd Run smoke tests daily to find regressions 2026-07-07 14:45:09 -07:00
Ian McEwen 5760fb3ad9 Add reconnect for firmware_node 2026-07-07 12:40:25 -07:00
Ian McEwen 2281ea5b64 Set the region on the simradio nodes so we can test setting presets other than LONG_FAST properly 2026-07-07 12:17:34 -07:00
Ian McEwen 7950798378 quick-win review fixes 2026-07-06 14:36:25 -07:00
Ian McEwen bffaff1c71 Update ch-preset shorthand arguments 2026-07-06 14:16:12 -07:00
Ian McEwen ceca7b3b10 Add a few extra tests of the revamped channel deletion behavior 2026-07-06 11:26:19 -07:00
Ian McEwen 15438c8a6e matrix up daily/alpha/beta 2026-07-04 02:41:21 -07:00
Ian McEwen 2f9b285dd5 Give the simradio testing a better name in the action 2026-07-04 02:38:04 -07:00
Ian McEwen 9b892711df Restructure tests, fix up a few other things, rework smokevirt thoroughly 2026-07-04 02:37:22 -07:00
Ian McEwen 1cd1aed33c Fix pylint/mypy stuff 2026-07-02 23:31:38 -07:00
Ian McEwen ab14fb2a86 Initial implementation of simradio-based testing with a multi-node simulated mesh 2026-07-02 22:48:01 -07:00
Ian McEwen 7a7353ca15 fix auto review skip-certain-authors key in coderabbit config 2026-07-02 09:17:31 -07:00
Ian McEwen e2844cb8d6 Merge pull request #944 from tylerpieper/master
add support for .cfg export/import
2026-07-01 21:17:09 -07:00
Ian McEwen 441c6e80a1 coderabbit yaml, primarily to disable the stupid poem 2026-07-01 16:34:12 -07:00
Ian McEwenandCopilot Autofix powered by AI b05e545396 guard against None
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-01 15:21:25 -07:00
Ian McEwen b6a13d58fe Refactor DeviceProfile import/export and harden handling
- Unify parsing for yaml/DeviceProfile paths
- Improve autodetection
- Ensure partial-config behavior with yaml
- gate configuration to local node (avoiding some pre-existing bugs)
- gate fixed position updates behind explicit opt-in via position settings field
- use setOwner properly
- tests, tests, and more tests
2026-07-01 13:57:42 -07:00
tylerpieper 8c5efb926c add support for .cfg export/import
adds binary import and export to mirror functionality of android (and soon ios) app
2026-06-29 14:41:16 -07:00
github-actions 6d76edf8a7 bump version to 2.7.10 2026-06-29 20:45:10 +00:00
Ian McEwen 74cce6bab9 protobufs: v2.7.26 2026-06-26 12:51:57 -07:00
Ian McEwen 1309f4f344 Merge pull request #943 from ianmcorvidae/flag-int-improvements
feat: add named-flag/power-of-2 bitfield support to --set
2026-06-26 11:13:28 -07:00
Ian McEwen fa01cb7a5d Fix mypy complaint 2026-06-26 11:09:15 -07:00
Ian McEwen dcdbda0524 feat: add named-flag/power-of-2 bitfield support to --set
This enables e.g. `--set position.position_flags ALTITUDE,SPEED`
rather than the dedicated `--pos-flags`, and similarly for
`network.enabled_protocols`.

Displaying these named values when printing out configurations
is not yet implemented.
2026-06-26 11:01:19 -07:00
Ian McEwen 9479fb2a77 Merge pull request #942 from ianmcorvidae/document-port
Better document how to specify a port when using TCP connection
2026-06-25 14:47:45 -07:00
Ian McEwen 990ecc2350 Better document how to specify a port when using TCP connection
Fixes #868

Also, fix a pylint complaint.
2026-06-25 14:46:45 -07:00
Ian McEwen 7fc69e957c Fail earlier if the ota update file isn't found 2026-06-25 14:28:32 -07:00
Ian McEwen f0de977be5 Some more test improvements 2026-06-25 14:07:36 -07:00
Ian McEwen c118334933 Add tests for the flags_to_list util function 2026-06-18 20:55:13 -07:00
Ian McEwen 307d8d81e7 Merge pull request #933 from ianmcorvidae/shared-contact
Add support for adding contacts using the CLI, including remotely
2026-06-17 07:42:14 -07:00
Ian McEwen 405a6bbc5d Fix an issue in to_node_num and add a bunch of tests to it as well 2026-06-16 20:36:06 -07:00
Ian McEwen db746a0981 pylint appeasement 2026-06-16 20:32:38 -07:00
Ian McEwen 13b8cdcb04 Improve test coverage, use property-based tests 2026-06-16 20:27:14 -07:00
Ian McEwen 9d445098f4 small fixes 2026-06-14 20:39:26 -07:00
Ian McEwen 8c84074c1d pylint appeasement 2026-06-14 19:06:45 -07:00
Ian McEwen 1cffae6add Add support for adding contacts using the CLI, including remotely 2026-06-14 15:28:07 -07:00
Ian McEwen e4f0fb222b Merge pull request #930 from pieceofr/fix/traceroute-routing-error-response
Handle ROUTING_APP error response in onResponseTraceRoute
2026-06-12 13:17:58 -07:00
github-actions 6eeb90066c bump version to 2.7.9 2026-06-09 02:01:44 +00:00
Ian McEwen e216848c91 Merge pull request #932 from ianmcorvidae/fixup-container
Fix up the container build
2026-06-08 13:33:09 -07:00
Ian McEwen dd1df473c7 Fix up the container build to use a multi-stage build, cache better, and most importantly actually keep the library in the final image (fixes #931) 2026-06-08 13:28:09 -07:00
NoxandClaude Sonnet 4.6 953d01206b Handle NONE errorReason in onResponseTraceRoute and add regression test
ROUTING_APP with errorReason NONE is an ACK (success), not an error.
Only print failure message for non-NONE error reasons.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 21:03:49 +08:00
NoxandClaude Sonnet 4.6 c71d3df332 Add unit test for onResponseTraceRoute ROUTING_APP error handling
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 12:45:59 +08:00
NoxandClaude Sonnet 4.6 0413d00825 Handle ROUTING_APP error response in onResponseTraceRoute
Previously, receiving a ROUTING_APP packet in response to a traceroute
would cause the function to attempt parsing it as a RouteDiscovery
payload, resulting in a crash or silent failure followed by a timeout.

This mirrors the error handling already present in onResponseTelemetry
and onResponseWaypoint, but displays the specific errorReason instead
of a hardcoded firmware version message, since traceroute is a
diagnostic tool where the exact failure reason is more valuable.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 22:50:56 +08:00
Ian McEwen 8f0faf5c3f One more dependency update 2026-05-31 20:22:00 -07:00
Ian McEwen ab656d54a8 More lockfile updates now that dependabot woke up 2026-05-31 20:17:59 -07:00
Ian McEwen c575547fae Merge branch 'review/pr-894'
Closes #894
2026-05-31 18:10:59 -07:00
Ian McEwen 0bef1370d7 tests of new BLEError functionality 2026-05-31 18:10:30 -07:00
Ian McEwen f9a1357816 Add a BLEError 'kind' field and branch on it instead of string matching 2026-05-31 18:03:26 -07:00
Ian McEwen d1f3552f64 Merge pull request #928 from ianmcorvidae/examples
Make examples more regularized and focused, and add contribution guidelines for the examples folder
2026-05-31 17:59:53 -07:00
Ian McEwen 81ae8b6c87 Make examples more regularized and focused, and add contribution guidelines for the examples folder 2026-05-31 17:50:14 -07:00
Ian McEwen 9c1573f1b3 Merge branch 'review/pr-739' into tmp/examples-consolidation 2026-05-31 17:19:11 -07:00
Ian McEwen ff26f9769d Remove arm/v7 from the container build right now, can't be bothered 2026-05-31 17:02:39 -07:00
Ian McEwen e5449be38e tryfix container build issue 2026-05-31 16:54:34 -07:00
Ian McEwen e350c238c5 A bunch of dependency updates in poetry.lock (to shut up Dependabot) 2026-05-31 16:43:18 -07:00
Ian McEwen 2fca36b3fe Merge branch 'review/pr-847' 2026-05-31 15:45:14 -07:00
Ian McEwen b16441378e Harden a bit, update some sections, add a README section 2026-05-31 15:45:02 -07:00
Ian McEwen 6037944215 Merge branch 'review/pr-901'
Closes #901
2026-05-31 15:13:40 -07:00
Ian McEwen 6909d3d21f make test more deterministic for reconnect count testing 2026-05-31 15:10:37 -07:00
Ian McEwen dbf4cabee1 Avoid deadlocking on potentially re-entrant _startConfig call, and don't reconnect when _wantExit 2026-05-31 15:03:58 -07:00
Ian McEwen 435e53eae2 Re-establish the OSError being raised to match former behavior, but still reconnect. TBD if this is quite the right approach. 2026-05-31 14:53:46 -07:00
Ian McEwen 12509bef30 Merge branch 'master' into review/pr-901 2026-05-31 14:30:04 -07:00
Ian McEwen 02485a88fb some pre-merge cleanup 2026-05-31 14:23:34 -07:00
Ian McEwen 4d3553b955 pylint/test fixes 2026-05-31 14:06:08 -07:00
Ian McEwen 95bd3ea7af pylint strikes again 2026-05-31 13:53:47 -07:00
Ian McEwen 2c443c082d Merge pull request #923 from ianmcorvidae/close-fixes
Fix some leaks/hangs on close: unstarted StreamInterface streams & TCP reader unblock
2026-05-31 13:48:52 -07:00
Ian McEwen ce5bbcda68 Fix some leaks/hangs on close: unstarted StreamInterface streams & TCP reader unblock 2026-05-31 13:48:06 -07:00
Ian McEwen c2a01f8294 fix a missing paren in a comment 2026-05-31 13:35:24 -07:00
Ian McEwen 14f53398d7 Merge commit 'refs/pull/857/head' of github.com:meshtastic/python into tmp/merge-918-857 2026-05-31 12:52:02 -07:00
Ian McEwen 4d8091a7e3 Merge commit 'refs/pull/918/head' of github.com:meshtastic/python into tmp/merge-918-857 2026-05-31 12:52:01 -07:00
Ian McEwen 15ce4968db pylint fix 2026-05-31 12:48:18 -07:00
Ian McEwen 9c0b253103 Merge branch 'review/pr-883'
Closes #883
2026-05-31 12:43:49 -07:00
Ian McEwen c7f43b3100 Document the use of --ch-index along with --reply a little 2026-05-31 12:43:38 -07:00
Ian McEwen 8c7eb118fc Merge branch 'review/pr-917'
Closes #917
2026-05-31 12:30:49 -07:00
Ian McEwen badf7279af Update the other factory reset to use integer too 2026-05-31 12:30:34 -07:00
Ian McEwen 13c9f34589 Merge pull request #911 from syn-ack-ai/fix/position-precision-overwrite
Fix position overwrite by lower-precision data
2026-05-31 12:21:14 -07:00
Ian McEwen 781764553e fix import order 2026-05-31 12:01:48 -07:00
Ian McEwen f5e7faea3b Merge pull request #913 from SilverBull239/examples/textchat-replymessage-demo
Add textchat.py and replymessage.py examples
2026-05-31 11:47:45 -07:00
Ian McEwen e86a495b50 Merge pull request #899 from dim5x/master
Refactor the Meshtastic TCP pub/sub example to ensure proper resource cleanup and clearer exception handling.
2026-05-31 11:18:14 -07:00
Ian McEwen 5dde67ef82 Merge pull request #922 from ianmcorvidae/nanopb-options-inject
Inject options in nanopb .options files into the protobuf files used for code generation
2026-05-31 10:27:27 -07:00
Ian McEwen a7d13eb2c1 the fuzz 2026-05-31 10:17:27 -07:00
Ian McEwen 3916009dbd pylint cleanups 2026-05-31 10:01:39 -07:00
Ian McEwen 280323da8b Add tests of nanopb options injection 2026-05-31 09:52:20 -07:00
Ian McEwen 89d81c9c7a Inject options in nanopb .options files into the protobuf files used for code generation 2026-05-31 09:42:55 -07:00
Ian McEwen fddaad316a protobufs: v2.7.24 2026-05-31 09:00:56 -07:00
nightjoker7 bfe38ac0c7 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.
2026-04-25 15:02:38 -05:00
Ben Meadors c5fc51ea37 Update factory reset to use integer for config reset 2026-04-17 05:03:56 -05:00
Ian McEwen cec79a7c1f poetry lock 2026-04-10 11:42:58 -07:00
Ian McEwen 6805fee667 loosen packaging version requirement 2026-04-10 11:41:29 -07:00
Ian McEwen 3cb9ce2a56 protobufs: v2.7.21 2026-04-10 11:40:56 -07:00
Aron Tkachuk b05a6a6017 Added textchat.py and replymessage.py example scripts from TODO 2026-04-05 18:23:02 -05:00
Benjamin BabeshkinandClaude Opus 4.6 8a842ed82b 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 #910

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 11:18:20 -07:00
github-actions eb964d78bb bump version to 2.7.8 2026-03-02 17:30:29 +00:00
Ian McEwen 80c40ef893 yolo away some pylint complaints 2026-03-02 10:22:21 -07:00
Ian McEwen 2494bb4c63 Merge pull request #828 from NekoCWD/nekocwd/hop-limits
FR: Add Hop Limits to send functions
2026-03-02 10:20:34 -07:00
Ian McEwen 1e4822fe87 Merge pull request #898 from skgsergio/feat/esp32-unified-ota
feat: Add ESP32 WiFi Unified OTA update support
2026-03-02 10:19:20 -07:00
Ian McEwen c7ee644ad2 update hopLimit docs to be in correct sections/include types 2026-03-02 10:15:14 -07:00
Ian McEwenandCopilot 9af5f22837 Apply trailing comma suggestions from code review
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-02 10:10:52 -07:00
Ian McEwen 1d3bdf143a Merge pull request #871 from viric/traceroute-0hop
Fix traceroute timeout for case of 0-hops
2026-03-02 10:05:33 -07:00
Ian McEwenandCopilot 7129a9f963 Update meshtastic/mesh_interface.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-02 10:01:27 -07:00
Ian McEwen d8e438b225 Merge pull request #890 from h3lix1/traffic_module
Add traffic management module to the python libraries
2026-03-02 10:00:12 -07:00
Ian McEwen 6511d06e2f Apply suggestion from @ianmcorvidae 2026-03-02 09:56:44 -07:00
Ian McEwen 94c531e7ee protobufs: v2.7.19 2026-03-02 09:51:43 -07:00
Ian McEwen ad4f3f557e Merge pull request #908 from pdxlocations/fix-label-error
Fix: Update repeated field checks to use is_repeated property
2026-03-02 09:36:44 -07:00
Ian McEwen 1c9cf37974 Merge pull request #907 from cpatulea/get-security
Fix '--get security' (incorrect AdminMessage.ConfigType value).
2026-03-02 09:34:55 -07:00
Ian McEwen cd9199bc00 Apply suggestion from @ianmcorvidae 2026-03-02 09:34:36 -07:00
pdxlocationsandIan McEwen 04a23ae882 Update meshtastic/__main__.py
Co-authored-by: Ian McEwen <ianmcorvidae@ianmcorvidae.net>
2026-03-02 08:26:18 -08:00
pdxlocations b003214d1b Fix property fallback 2026-03-01 08:58:42 -08:00
pdxlocations 414a621091 add fallback for older protobuf dependency 2026-03-01 08:44:37 -08:00
pdxlocations a87065a081 fix: update repeated field checks to use is_repeated property 2026-02-28 22:11:11 -08:00
Catalin Patulea 942ce115f3 Fix '--get security' (incorrect AdminMessage.ConfigType value).
requestConfig was assuming that the order of fields in meshtastic.LocalConfig
matches the order of enum values in AdminMessage.ConfigType. This is true for
'device', 'position', etc. but is NOT true for 'security' due to the intervening
'version' field.

Look up LocalConfig fields by name, not index, to prevent this error in the future.

LocalConfig.security was introduced in https://github.com/meshtastic/protobufs/pull/553
2026-02-27 23:27:52 -05:00
Clive Blackledge d5eaecea07 Add traffic management unit tests 2026-02-25 01:47:53 -08:00
Clive Blackledge 545c3ab192 Add traffic management module to the config. 2026-02-25 01:47:53 -08:00
github-actions fee2b6ccbd Update protobufs 2026-02-14 16:48:14 +00:00
Ben Meadors d0ccb1a597 Update protobufs GH action 2026-02-14 10:47:14 -06:00
Sergio Conde 5721859314 fix: cleanup imports in tests 2026-02-14 15:05:11 +01:00
Stephen Thorne 07172f88f3 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.
2026-02-05 22:46:40 +01:00
dim5x 3ea1994cf5 Refactor the Meshtastic TCP pub/sub example to ensure proper resource cleanup and clearer exception handling. 2026-02-05 13:42:37 +03:00
Sergio Conde 4de19f50d9 fix: add tests 2026-02-01 21:25:49 +01:00
Sergio Conde 4d8430d360 fix: throw propper exceptions and cleanup code 2026-02-01 11:39:09 +01:00
Ben MeadorsandCopilot bf580c36ae Update meshtastic/__main__.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-31 18:15:25 -06:00
Sergio Conde 1eb13c9953 feat: Add ESP32 WiFi Unified OTA update support 2026-01-31 16:44:03 +01:00
Aleksei SviridkinandClaude 1fc2407c5e 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>
2026-01-26 11:07:16 +03:00
Aleksei SviridkinandClaude 25ffd25fda 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>
2026-01-26 11:07:13 +03:00
Ian McEwen cdf893e618 Merge pull request #893 from lexfrei/fix/ble-race-condition
fix(ble): resolve BLE connection hangs on macOS without --debug flag
2026-01-25 15:24:06 -07:00
Aleksei SviridkinandClaude 9b9df9e585 fix(ble): resolve BLE hangs on macOS without --debug flag
This fixes two issues that caused BLE connections to hang on macOS
when not using the --debug flag:

1. Race condition in BLEClient event loop initialization
   - The event loop thread was started but asyncio operations were
     submitted before the loop was actually running
   - Added threading.Event synchronization to ensure the event loop
     is running before any operations are submitted
   - The ready signal is sent from within the loop via call_soon()
     to guarantee the loop is truly active

2. CoreBluetooth callback delivery on macOS
   - On macOS, CoreBluetooth requires occasional I/O operations for
     callbacks to be properly delivered to the main thread
   - Without --debug, no I/O was happening, causing callbacks to
     never be processed and operations to hang indefinitely
   - Added sys.stdout.flush() call before waiting for async results
     to trigger the necessary I/O

The --debug flag masked these issues because:
- Debug logging introduces timing delays that let the event loop start
- Logger I/O triggers the necessary callback delivery mechanism

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-01-25 03:06:58 +03:00
github-actions b26d80f186 bump version to 2.7.7 2026-01-24 16:34:15 +00:00
Ian McEwen da559f0e37 protobufs: v2.7.18 2026-01-24 09:31:04 -07:00
Ian McEwen a73f432f42 Merge pull request #888 from dandrzejewski/node-favorites
Show favorite nodes in --nodes
2026-01-09 13:09:31 -07:00
David Andrzejewski c3c5ce64dd Copilot had a few suggestions on code review, implemented them. 2026-01-08 19:08:18 -05:00
David Andrzejewski 683dd23d63 Fix a few pylint things. 2026-01-08 18:53:55 -05:00
David Andrzejewski 57d43b84e4 Merge branch 'master' into node-favorites 2026-01-08 18:40:43 -05:00
David Andrzejewski 4f6d183ed1 Show favorite nodes in --nodes 2026-01-08 18:13:03 -05:00
github-actions 9a72e36ca6 bump version to 2.7.6 2026-01-05 02:25:14 +00:00
Ian McEwen 4d54ee7431 protobufs: v2.7.17 2026-01-04 19:21:54 -07:00
Rob c8b1b8ea6f 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.
2025-12-26 15:17:46 -05:00
Lluís Batlle i Rossell d9057c0aaf Fix traceroute timeout for case of 0-hops
It was not waiting any time.
2025-11-29 15:40:33 +01:00
Olliver Schinagl 77db30125a 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>
2025-11-21 16:16:04 +01:00
Ian McEwen 5cc0dae394 protobufs: v2.7.15 2025-11-20 14:30:39 -07:00
Ian McEwen 1a50377d34 Merge pull request #865 from ianmcorvidae/device-metadata-details
Add a bunch more detail to --device-metadata output, from fields we weren't formerly using in the output.
2025-11-20 14:29:20 -07:00
Ian McEwen 2a44be9269 Add a bunch more detail to --device-metadata output, from fields we weren't formerly using in the output. 2025-11-20 11:42:57 -07:00
github-actions 776debcc86 bump version to 2.7.5 2025-11-18 19:48:40 +00:00
Ian McEwen 1088cc607e Merge branch 'revert-close-refactor' 2025-11-13 12:10:46 -07:00
Ian McEwen aeec5447ed Revert "Merge pull request #841 from SpudGunMan/master"
This reverts commit b4662251ed, reversing
changes made to 2065598754.
2025-11-13 12:08:42 -07:00
Ian McEwen 096fec95c8 Revert "Wrap double-close in a try-catch. Slightly ugly but oh well."
This reverts commit dbc0101a7a.
2025-11-13 12:07:48 -07:00
Ian McEwen dea5f788a2 Revert "Add more exception logging, fix some additional stream read/write issues"
This reverts commit f15a0bdc0b.
2025-11-13 12:07:32 -07:00
Ian McEwen f15a0bdc0b Add more exception logging, fix some additional stream read/write issues 2025-11-13 11:38:00 -07:00
Travis-L-R 1214d5010b Linting adjustment for change to StreamInterface non-abstract use check 2025-11-13 07:38:25 +10:30
Ian McEwen 0906fc6bc0 Merge pull request #858 from Travis-L-R/conf_work
Splitting out true_defaults for moduleConfig in export_config()
2025-11-12 10:16:19 -07:00
Travis-L-R ccb530574b Splitting true_defaults for moduleConfig out in export_config() 2025-11-11 21:03:01 +10:30
Travis-L-R 76418b8e57 Reorganising connect method calls for when connectNow is false 2025-11-11 19:25:51 +10:30
Travis-L-R 3be73b42e2 Removing unnecessary initialization of self.stream in TCPInterface 2025-11-11 19:19:44 +10:30
Travis-L-R 2245ac8d97 Shifting serial interface connection parts from __init__() into connect() method 2025-11-11 19:18:00 +10:30
Travis-L-R 040f332078 Updating test for change of deprecation exception 2025-11-11 19:14:42 +10:30
Travis-L-R f3f17a7d50 Removing superfluous setting of noProto in init() 2025-11-11 19:02:54 +10:30
Travis-L-R 79334e83e6 Adjusting old deprecation test and exception for non-abstract use of StreamInterface 2025-11-11 18:57:52 +10:30
Ian McEwen dbc0101a7a Wrap double-close in a try-catch. Slightly ugly but oh well. 2025-11-10 13:58:12 -07:00
Ian McEwen debbb8caeb Update some dependencies 2025-11-10 13:09:44 -07:00
github-actions 3c6dba78a3 bump version to 2.7.4 2025-11-06 22:50:41 +00:00
Ian McEwen 8a3b114153 protobufs: v2.7.13 2025-11-06 15:37:32 -07:00
Ian McEwen 0bb04100e1 Merge pull request #843 from SpudGunMan/channel-hash-info
Add Channel Hash Utility to Node class
2025-11-06 15:35:36 -07:00
Ian McEwen ad04c26d13 split out constant, improve logic some, add tests for channel_hash and generate_channel_hash 2025-11-06 15:19:12 -07:00
Ian McEwen 0e67ef37aa Return hash from channel_hash function 2025-11-06 14:55:41 -07:00
Ian McEwen e924afd140 Merge branch 'master' into channel-hash-info 2025-11-06 14:51:32 -07:00
Ian McEwen 87682c153b Merge pull request #834 from NillRudd/feat/cli-ignore-formats
CLI: normalize lora.ignore_incoming IDs (dec/!hex/0x), dedupe, YAML [] clear, fix bytes→int crash
2025-11-06 14:19:57 -07:00
Ian McEwen e6c276fe96 Fix trailing whitespace 2025-11-06 14:17:07 -07:00
Ian McEwen b4662251ed Merge pull request #841 from SpudGunMan/master
refactor close()
2025-11-06 13:24:59 -07:00
SpudGunMan a17cfe9d2b rename to generate_channel_hash 2025-11-06 12:01:53 -08:00
SpudGunMan 471e3ce145 generate_hash to util 2025-11-06 12:00:16 -08:00
SpudGunMan 39c9864682 move and rename hash function
this function, xor_hash, and a variable for the default key (as bytes, I think, rather than the base64 version) really all belong in meshtastic.util rather than here.
There's multiple forms of hashing in firmware so this should be named to denote that, perhaps channel_hash. If we later want to add the frequency-slot-style hash, better if it's distinguished better from the start.
2025-11-06 11:49:57 -08:00
Niklas Roslund 1d3a7d39f7 Move to_node_num to util and updated function calls 2025-11-06 19:05:34 +01:00
Ian McEwen 2065598754 Merge pull request #825 from trose/feature/tdeck-support
Add T-Deck device support
2025-11-06 11:00:56 -07:00
Ian McEwen 4cac15686b Merge pull request #851 from pdxlocations/waypoint-icon
add icon parameter to sendWaypoint()
2025-11-06 10:28:00 -07:00
pdxlocations 9285f7d13f add waypoint parameter 2025-10-29 08:58:25 -07:00
SpudGunMan f6f1b748dc hashGen fix
using from https://raw.githubusercontent.com/pdxlocations/mudp/refs/heads/main/mudp/encryption.py

@pdxlocations
2025-10-22 14:21:46 -07:00
SpudGunMan d2d9c03bc8 tidy up the autobot 2025-10-22 14:19:52 -07:00
SpudGunMan c44f9b1bb4 Add Channel Hash Utility to Node class
### Summary

- Added a new method `get_channels_with_hash()` to the `Node` class.
- This method returns a list of dictionaries, each containing the channel index, role, name, and a hash value derived from the channel name and PSK.
- The hash is calculated using the existing `generate_hash` utility, ensuring consistency with other parts of the codebase.
- This utility makes it easier to programmatically access channel metadata, including a unique hash, for scripting, debugging, or integration purposes.

### Motivation

- The protobuf `Channel` objects do not include a hash value by default.
- This addition provides a Pythonic, easy-to-use way to access channel info and a unique hash for each channel, which can be useful for diagnostics, scripting, or UI display.

### Example Usage

```python
channels = node.get_channels_with_hash()
for ch in channels:
    print(f"Index {ch['index']}: {ch['role']} name='{ch['name']}' hash={ch['hash']}")
```

### Impact

- No breaking changes; existing APIs and CLI output remain unchanged.
- The new method is additive and can be used where needed for enhanced channel introspection.
2025-10-22 10:42:54 -07:00
Kelly 6f35eb3923 Merge branch 'meshtastic:master' into master 2025-10-22 09:40:37 -07:00
Ian McEwen dcd077d85e Merge pull request #839 from horrible-knots/master
Plumb timeout from --timeout through MeshInterface
2025-10-17 08:47:42 -07:00
SpudGunMan da416fcd20 refactor flush
The double flush() is not the root cause; the real issue is that code is trying to use the serial port after it has been closed.
The error occurs both in close() (during flush()) and later in _writeBytes() (during write()), indicating the port is closed or invalid at those times.
2025-10-14 07:54:37 -07:00
SpudGunMan 93da1da386 flush() is only called if the stream is open
This ensures flush() is only called if the stream is open, and logs (but ignores) any exceptions during flush. This should prevent the "Bad file descriptor" error.

I see this error a lot on a rak unit, I dont know this is the way but .. you be the judge.
2025-10-14 07:42:27 -07:00
horrible-knots 2de7c30a27 Plumb timeout from --timeout through MeshInterface
Fix C0301: Line too long

Ignore the pylint for 6 positional arguments
2025-10-13 17:35:30 -04:00
Niklas Roslund 43a685f012 cli: notmalize ignore IDs for dec, hex and 0x. support YAML [] clear
Signed-off-by: Niklas Roslund <nroslund@kth.se>
2025-10-05 12:55:59 +02:00
Ian McEwen 7554c03a26 protobufs: v2.7.10 2025-09-24 21:20:49 -07:00
Vasiliy Doylov 38a13f300f mesh_interface: deleteWaypoint: add hopLimit 2025-09-23 16:32:29 +03:00
Vasiliy Doylov eef8a37703 mesh_interface: sendWaypoint: add hopLimit 2025-09-23 16:31:52 +03:00
Vasiliy Doylov 63b940defb mesh_interface: sendTelemetry: add hopLimit 2025-09-23 16:30:18 +03:00
Vasiliy Doylov e5efa94264 mesh_interface: sendPosition: add hopLimit 2025-09-23 16:28:56 +03:00
Vasiliy Doylov efa841b08c mesh_interface: sendAlert: add hopLimit 2025-09-23 16:26:02 +03:00
Vasiliy Doylov 6ff13eb95c mesh_interface: sendText: add hopLimit 2025-09-23 16:23:24 +03:00
Taylor Rose 49783d9108 Add T-Deck device support to Meshtastic CLI
- Add T-Deck device definition with verified USB IDs (303a:1001)
- Add T-Deck to supported_devices list
- Uses t-deck firmware identifier and ESP32 device class
- Supports Linux (ttyACM), macOS (cu.usbmodem), and Windows (COM) ports
- Update .gitignore to exclude virtual environment files

T-Deck is now fully supported and automatically detected by the CLI.
2025-09-18 14:39:46 -06:00
github-actions 3615135e97 bump version to 2.7.3 2025-09-18 16:42:32 +00:00
Ian McEwen cfb23788e1 Merge pull request #812 from shukari/loggingHandler2
Added Logging Handler Names & windows pytest support
2025-09-18 09:26:55 -07:00
Ian McEwen 14ff3eb9c4 Merge branch 'master' into loggingHandler2 2025-09-18 09:26:32 -07:00
Ian McEwen 47e5b04d3b protobufs: v2.7.9 2025-09-18 09:24:33 -07:00
github-actions e5159f1156 bump version to 2.7.2 2025-09-12 16:49:34 +00:00
Ian McEwen 593b05dbcd protobufs: v2.7.8 2025-09-12 09:46:52 -07:00
Ian McEwen f519d1f2d2 Merge pull request #818 from pdxlocations/check-module-bitmask
Fix Getting and Setting Excluded Modules: Ringtone and Canned Messages
2025-09-12 09:44:29 -07:00
Ian McEwen 8b36561406 Merge pull request #821 from shukari/contributionHelp
Added Github CONTRIBUTING.md file
2025-09-12 09:43:30 -07:00
shukari e2b4948d45 Github CONTRIBUTING.md file 2025-09-09 18:17:29 +02:00
pdxlocations 7e3d347b63 remove trailing whitespaces 2025-08-29 23:19:22 -07:00
pdxlocations c6efccdbd2 init 2025-08-29 23:11:26 -07:00
shukari fe093ac34b fix errors and better linux/windows handling in tests 2025-08-21 15:56:10 +02:00
Ian McEwen 2b10459db0 remove dangling submodule 2025-08-20 10:13:27 -07:00
shukari dd238dcbe3 ignore pathlib import 2025-08-19 22:03:37 +02:00
shukari e330afc899 more paths to os.path.join 2025-08-19 13:38:59 +02:00
shukari a63f3f6e94 use os.path.join for path 2025-08-19 11:23:22 +02:00
shukari 52eb112b95 new Parameter --debuglib for only meshtastic debug, more termios fixes for windows tests
(cherry picked from commit 4fc4d41d3d29998bb7b697bf412be5c1449ea950)
2025-08-19 11:14:38 +02:00
github-actions d53ced216c bump version to 2.7.1 2025-08-18 19:40:36 +00:00
Ian McEwen f5ecd28705 protobufs: v2.7.5 2025-08-18 09:52:48 -07:00
Ian McEwen 82ad9b2f51 Merge pull request #813 from pdxlocations/unmess-unmessageable
Fix --set-owner by calling setOwner!
2025-08-18 09:50:28 -07:00
shukari cbf7b9befe pylint: test_mesh_interface.py line too long 2025-08-09 16:58:48 +02:00
shukari f3ba660cf4 fix: lint error 2025-08-07 13:30:47 +02:00
shukari 4b143030d3 system independent path separators; pytest for windows fixes 2025-08-07 12:05:43 +02:00
pdxlocations 03aaa4c98e always call setOwner 2025-08-06 21:58:42 -07:00
shukari a79e17a575 fix test_exit_with_exception test, pytest for windows
- test are now runable on windows, some are ignored and a fake termios for the decorators
- test_exit_with_exception with a true exception
2025-08-06 23:01:40 +02:00
shukari 38b163fa89 move logger init to the end of the import block for mesh_interface 2025-08-06 20:00:42 +02:00
shukari af4947d020 move logger init to the end of the import block 2025-08-06 19:54:08 +02:00
shukari db1891b651 Added Logging Handler Names 2025-08-06 18:21:32 +02:00
github-actions d7d9c7219a bump version to 2.7.0 2025-08-01 22:56:21 +00:00
Ian McEwen c60b5d4b05 Add some extra fields that now appear in MyNodeInfo to tests 2025-08-01 15:53:13 -07:00
Ian McEwen 83d82c518a Merge pull request #807 from pdxlocations/export-true-defaults
Export missing defaults when set False
2025-08-01 15:51:05 -07:00
Ian McEwen 8a95ce4636 protobufs: v2.7.4 2025-08-01 15:48:32 -07:00
pdxlocations 0261313fc5 add test 2025-07-27 12:10:05 -07:00
pdxlocations c1a6234694 make tuple type hint explicit 2025-07-26 23:16:50 -07:00
pdxlocations 66e32f812a fix launch.json punctuation 2025-07-26 22:35:18 -07:00
pdxlocations eb85439000 rename function 2025-07-26 22:27:10 -07:00
pdxlocations 885eb4898d init 2025-07-26 22:20:19 -07:00
Ian McEwen 172c123990 protobufs: v2.7.3 2025-07-22 17:26:15 -07:00
Ian McEwen fcdd83838b Merge pull request #795 from pdxlocations/canned-config
Add Ringtone and Canned Messages to --export-config and --configure
2025-07-22 17:22:11 -07:00
Ian McEwen 58967e1d91 Merge pull request #794 from pdxlocations/is-unmessageable
add --set-is-unmessageable to CLI commands
2025-07-22 17:07:50 -07:00
pdxlocations 17f7e8e20e don't check for typoed keyword 2025-07-22 11:23:45 -07:00
pdxlocations 9b5a889676 combine arguments 2025-07-22 10:17:40 -07:00
pdxlocations ce7c61861f Merge branch 'master' into is-unmessageable 2025-07-22 10:15:19 -07:00
Ian McEwen 4adcbb6787 Merge pull request #743 from ianmcorvidae/client-notifications
Add handling of clientNotification on FromRadio
2025-07-22 09:55:19 -07:00
Ian McEwen 125f63419e Merge branch 'master' into client-notifications 2025-07-22 09:54:17 -07:00
Ian McEwen cad5d18aff Merge pull request #798 from pdxlocations/export-utf8
Allow forced UTF-8 encoding for --export-config
2025-07-22 09:28:14 -07:00
Ian McEwen 706d0649c1 Merge pull request #801 from Cyclic3/master
Plumb reply_id into sendText and sendData
2025-07-22 09:23:02 -07:00
Ian McEwen 167044907d for some reason pylint likes this better 2025-07-16 19:16:16 -07:00
Ian McEwen ab1669994f Merge pull request #804 from dangerdyke/fix-vids
Add seeed xiao esp32s3 support
2025-07-16 18:44:16 -07:00
Tom Fifield 6f67f33378 Merge pull request #806 from meshtastic/fifieldt-patch-1
Add check for poetry install to regen-protobufs.sh
2025-07-17 09:05:17 +08:00
Tom Fifield e60c8ea105 Add check for poetry install to regen-protobufs.sh
This was failing in the CI because the poetry was not initialized.
2025-07-17 10:49:23 +10:00
flockofsparrows d633f8c895 Add seeed xiao esp32s3 to supported devices 2025-07-09 16:23:21 -04:00
Cyclic3 ca82e1ce2b plumb reply_id into sendText and sendData 2025-07-08 23:38:02 +01:00
pdxlocations 0ae23eec7e don't think i needed that 2025-06-30 00:34:44 -07:00
pdxlocations 2fa85bac1f typo 2025-06-30 00:21:15 -07:00
pdxlocations 58fc614fb7 fix test 2025-06-30 00:12:42 -07:00
pdxlocations 795b652069 export utf-8 2025-06-30 00:03:11 -07:00
rcarteraz 213faa0cae rename and move license file 2025-06-25 12:23:00 -07:00
pdxlocations 68a2009e0e add more tests 2025-06-24 11:47:33 -07:00
pdxlocations c76e4dac87 undo comment 2025-06-24 11:17:01 -07:00
pdxlocations 428be9fbce add mesh_interface tests 2025-06-24 11:16:23 -07:00
pdxlocations d83f7b2307 add to example config 2025-06-24 10:12:12 -07:00
pdxlocations eb453a2e8a add to tests 2025-06-24 08:16:45 -07:00
pdxlocations 308ac93399 add to configure 2025-06-24 07:40:11 -07:00
pdxlocations 84417f0bb1 working export-config 2025-06-23 22:39:24 -07:00
pdxlocations 0bb3389b3b init 2025-06-23 22:11:50 -07:00
pdxlocations 22b3062151 remove whitespace 2025-06-23 16:37:49 -07:00
pdxlocations 373b8a3139 combine with set_owner handling 2025-06-23 16:14:59 -07:00
github-actions db21942244 bump version to 2.6.4 2025-06-23 22:15:45 +00:00
Ian McEwen c55f1ef610 Merge pull request #782 from Crank-Git/validate-short-and-long-names
Add validation/prevent --set-owner, --set-owner-short, and --set-ham from accepting empty or whitespace-only names
2025-06-23 09:15:47 -07:00
pdxlocations 51b543ff40 add tests 2025-06-21 18:16:24 -07:00
pdxlocations 8752a0de6e remove whitespace 2025-06-21 16:25:48 -07:00
pdxlocations 7160e79fbf more not incorrect spelling 2025-06-21 13:51:58 -07:00
pdxlocations b73fcbff88 didn't spell it wrong 2025-06-21 13:50:30 -07:00
pdxlocations 1b5b07e752 add --set-is-unmessageable 2025-06-21 13:44:25 -07:00
Ian McEwen ab997aac84 Merge pull request #790 from ianmcorvidae/sendMqttClientProxyMessage
Add a sendMqttClientProxyMessage helper for client proxy implementation
2025-06-16 12:48:47 -07:00
Ian McEwen a097161dbc Add a sendMqttClientProxyMessage helper for client proxy implementation 2025-06-16 12:45:25 -07:00
Ian McEwen e6750507c8 protobufs: v2.6.12 2025-06-16 12:22:31 -07:00
Ian McEwen 0deb98b4c6 Merge pull request #785 from grleblanc/master
fix(util): update waitForTraceRoute reset logic
2025-06-09 16:22:45 -07:00
grleblanc 04a0ff6322 fix(util): fix typo 2025-06-09 15:50:21 -04:00
grleblanc b4764d3bc3 fix(util): update waitForTraceRoute reset logic 2025-06-09 15:05:53 -04:00
Ian McEwen 9281c4a335 Correctly call self.close() in disconnected_callback for bleak (fixes #770) 2025-06-09 11:52:23 -07:00
Ben Meadors 3c2dd6f4ff Merge pull request #783 from pdxlocations/config-work
Fix --configure by adding delays after sections
2025-06-09 05:37:25 -05:00
pdxlocations 8e48d141c8 add sleeps 2025-06-08 23:32:32 -07:00
Crank-Git 8a6ee5fb35 fixed assertion errors by replace ham id with ham radio callsign, fixed _sendAdmin assertion 2025-06-08 20:39:26 -04:00
Crank-Git aa786c7ebd Fix linting errors and duplicate function 2025-06-08 19:51:21 -04:00
Crank-Git 23be2d2189 Add validation/prevent --set-owner, --set-owner-short, and --set-ham from accepting empty or whitespace-only names. This is in relation to meshtastic#6867 firmware feature request https://github.com/meshtastic/firmware/issues/6867 2025-06-08 18:16:49 -04:00
Ian McEwen 622a435465 Add __repr__ methods to interface types and Node, for nicer printing/logging 2025-05-08 21:23:00 -07:00
github-actions 56680f8da6 bump version to 2.6.3 2025-05-08 22:27:56 +00:00
Ian McEwen 321a960c13 protobufs: v2.6.7 2025-05-08 15:26:47 -07:00
Ian McEwen 4668852b0b Admin messages should be requesting acknowledgements 2025-05-08 15:22:27 -07:00
github-actions c3973117c8 bump version to 2.6.2 2025-04-23 16:49:59 +00:00
Ian McEwen d456e4ce30 protobufs: v2.6.6 2025-04-23 09:47:13 -07:00
Ben Meadors ec78f62992 Merge pull request #763 from meshtastic/revert-753-fix-rtsdtr-windows
Revert "Work around RTS/DTR serial reset issues on Windows"
2025-04-15 16:37:23 -05:00
Ben Meadors dfc9547ffc Revert "Work around RTS/DTR serial reset issues on Windows" 2025-04-15 16:03:33 -05:00
Ian McEwen ee0f73a20e Merge pull request #761 from derekn/master
fixing tcp_interface 100% cpu usage bug (#709)
2025-04-15 13:56:28 -07:00
Derek Nicol 2e73fe310c fixing tcp_interface 100% cpu usage bug (#709) 2025-04-15 15:23:09 -05:00
Ian McEwen d4bc39153a bump to 2.6.2a0 2025-04-05 23:01:43 -07:00
github-actions 2e8f823431 bump version to 2.6.1 2025-04-06 05:56:17 +00:00
Ian McEwen 675169167c protobufs: v2.6.4 2025-04-05 22:53:02 -07:00
Ian McEwen 75f8e27799 Merge pull request #749 from cdanis/fix-default-pytest
Fix pytest test collection on default install
2025-03-30 22:28:28 -07:00
Ian McEwen f426699d2b Merge pull request #753 from dwhinham/fix-rtsdtr-windows
Work around RTS/DTR serial reset issues on Windows
2025-03-30 22:27:34 -07:00
Dale Whinham bc1664dade Work around RTS/DTR serial reset issues on Windows
Some devices (e.g. Heltec V3.1) are reset when the RTS or DTR pins are
messed with, which means that without workarounds, Meshtastic CLI will
cause a reset with any operation that opens the serial port.

This behavior is documented in this PySerial issue:
https://github.com/pyserial/pyserial/issues/124

On Linux, we already handle this by disabling the HUPCL termios flag,
which has the effect of preventing the offending lines being toggled. On
Windows, setting the initial state of RTS and DTR before opening the
port has a similar effect.

Implement this workaround so that Meshtastic CLI can be used on Windows
with these devices.
2025-03-20 13:47:25 +00:00
Ian McEwen 32a61b0209 Make nanopb.proto stuff work right again, hopefully 2025-03-12 14:13:43 -07:00
github-actions 0c38a9eb0b bump version to 2.6.0 2025-03-10 23:18:47 +00:00
Ian McEwen e591cc184f set 2.6 version num 2025-03-10 16:17:40 -07:00
Chris Danis b42d33824a Fix pytest test collection on default install
As is, with a default `poetry install`, a simple `poetry pytest run`
will encouter import errors while reading the test files themselves --
which causes pytest to fatally error during test collection, meaning no
tests are run.
2025-03-07 13:16:20 -05:00
rcarteraz ed908fc4b6 Merge pull request #748 from rcarteraz/master
update README
2025-03-03 11:44:08 -07:00
rcarteraz 3710e6e909 update README 2025-03-03 11:42:15 -07:00
Ian McEwen faa8064ccc protobufs: v2.6.0 2025-03-02 12:14:20 -07:00
Ian McEwen 2f44351945 Merge pull request #742 from ianmcorvidae/multiset
Enable setting things from more than one configuration section with `--set`, using a configuration transaction.
2025-02-21 10:08:15 -07:00
Ian McEwen f5fa30cb22 try to get pylint happier 2025-02-20 12:40:39 -07:00
Ian McEwen 46a8db286c Add handling of clientNotification on FromRadio, sending a pubsub message 2025-02-20 12:36:21 -07:00
Ian McEwen 852949575b Enable setting things from more than one configuration section with --set, using a configuration transaction. 2025-02-20 11:39:16 -07:00
henri acff793f28 Updates and bug fixes meshtastic_serial_message_reader.py 2025-02-20 08:44:32 +13:00
github-actions e2c9c1315e bump version to 2.5.12 2025-02-19 17:21:25 +00:00
Ian McEwen f41ef042a9 Merge pull request #708 from mikeymakesit/add-channel
Add new channels from an add URL with the new --ch-add-url option
2025-02-19 10:18:09 -07:00
Ian McEwen 84bec5a7c4 fix up misc. lint/type/test issues, streamline, align argument names and groupings 2025-02-19 10:15:27 -07:00
Ian McEwen 985366c812 Merge pull request #710 from digitaldisarray/master
Add text message port cli option
2025-02-19 09:55:01 -07:00
Ian McEwen 3954fbd404 fix misc lint, test, type complaints from CI 2025-02-19 09:52:16 -07:00
Ian McEwen 5f174b2850 Merge pull request #731 from migillett/464/0x-prefix
464: allow for 0x node prefix values
2025-02-19 09:30:15 -07:00
Michael Gillett 23ea19c00b linter fixes following failed build 2025-02-19 11:26:49 -05:00
Ian McEwen acc47146c9 Merge pull request #736 from dandrzejewski/master
Add optional CLI parameter to specify node info fields to show with --nodes parameter.
2025-02-19 09:26:17 -07:00
Ian McEwen dd8803793d fix test mock 2025-02-19 09:21:51 -07:00
Ian McEwen 68ec588804 remove trailing whitespace 2025-02-19 09:15:45 -07:00
henri 53b58500f7 Added example script : meshtastic_serial_message_reader.py 2025-02-19 17:48:48 +13:00
David Andrzejewski 2f48594dc3 Fix a pylint error regarding typing. 2025-02-18 22:09:37 -05:00
David Andrzejewski c7c3c69fc3 Fix a pylint error regarding typing. 2025-02-18 22:08:26 -05:00
David Andrzejewski 53e40d5aab Fix a pylint error 2025-02-18 22:05:46 -05:00
David Andrzejewski dd3da6a670 Merge remote-tracking branch 'origin/master' 2025-02-18 21:18:59 -05:00
David Andrzejewski e500b399f4 Merge branch 'meshtastic:master' into master 2025-02-18 21:18:48 -05:00
David Andrzejewski 27ac28e300 Fixing indentation per project coding standards. 2025-02-18 21:18:22 -05:00
David Andrzejewski 6bc5f5e305 Fix count vs len on a list (https://github.com/meshtastic/python/pull/736#discussion_r1960099918) 2025-02-18 21:15:31 -05:00
Michael Gillett c844e4e0fe help details for new prefix values 2025-02-18 15:25:24 -05:00
Michael Gillett 060df86bb6 Update __main__.py 2025-02-18 15:10:55 -05:00
Michael Gillett 7d87d5037e Update __main__.py 2025-02-18 15:10:26 -05:00
Michael Gillett 4ec7698d94 metavars being a PAIN 2025-02-18 15:09:59 -05:00
Michael Gillett 7cc65aa08a missed one 2025-02-18 15:06:20 -05:00
Michael Gillett cc411ce0bb remove nargs and unneeded fstrings 2025-02-18 15:05:31 -05:00
Michael Gillett edff956f9d specify nargs 2025-02-18 14:53:01 -05:00
Michael Gillett bd68739158 >= 8 instead of > 8 2025-02-18 14:30:28 -05:00
Michael Gillett 530d92ead2 remove redundant f string 2025-02-18 14:24:04 -05:00
Michael Gillett 60f9dc6266 new implementation 2025-02-18 14:23:55 -05:00
Michael Gillett f9ae021e43 remove old implementation 2025-02-18 14:23:40 -05:00
Michael Gillett 317d81c983 list -> tuple fix for pytest 2025-02-18 14:02:48 -05:00
Ian McEwen 5837bd0172 Merge pull request #734 from dudash/master
catch unhandled OSError when serial port in use
2025-02-18 09:52:03 -07:00
Ian McEwen 5487f7a791 Merge pull request #735 from tache/master
Added descriptive FileNotFoundError handler for serial device connection
2025-02-18 09:51:17 -07:00
Ian McEwen c6d8a540eb Bump to 2.5.12a0 2025-02-18 09:43:55 -07:00
Ian McEwen 0962c9b058 protobufs: v2.5.22 2025-02-18 09:43:32 -07:00
Ian McEwen 4f98602ac2 ignore argcomplete import in main 2025-02-18 09:41:54 -07:00
David Andrzejewski 6ebddb67c0 Refactored showNodes. It now uses a lookup table to determine the human-readable names of the fields. 2025-02-15 17:18:14 -05:00
David Andrzejewski 82554a1f18 Add optional parameter to specify what fields to show with --nodes 2025-02-15 02:23:13 -05:00
tache 8c115dc636 Added descriptive FileNotFoundError handler for serial device connection
- Added a detailed error message for FileNotFoundError when the serial device is not found.
- Included troubleshooting steps for common issues.
2025-02-09 14:23:31 -05:00
dudash e2fe359527 catch unhandled OSError when serial port in use 2025-02-08 14:51:53 -05:00
Michael Gillett 5600ce92b0 update to comments 2025-01-26 13:10:56 -05:00
Michael Gillett efb848adf9 comments explaining logic 2025-01-26 12:58:18 -05:00
Michael Gillett 0d8646189f remove logging.info used for local debugging 2025-01-26 12:56:17 -05:00
Michael Gillett d0023df8ca slight logic rework 2025-01-26 12:55:03 -05:00
Michael Gillett b522abf33e update docs 2025-01-26 12:06:31 -05:00
Michael Gillett c086b6372e Update mesh_interface.py 2025-01-26 11:58:48 -05:00
github-actions 6ec506fe3b bump version to 2.5.11 2025-01-24 17:22:13 +00:00
Ian McEwen fc3b81dfde protobufs: v2.5.20 2025-01-24 10:19:13 -07:00
Ben Meadors 9c53ea017c Merge pull request #730 from migillett/feature/nodes-add-role
#692 feature/nodes-add-role
2025-01-23 19:07:46 -06:00
Michael Gillett 1e6625d062 feature/nodes-add-role 2025-01-21 22:31:33 -05:00
digitaldisarray 0fb72b8ad1 Only allow PRIVATE_APP custom port 2024-12-31 15:15:18 -08:00
digitaldisarray 74c911cb75 Add text message port cli option 2024-12-06 17:54:08 -08:00
Mike Hornung 579383cd5a Add new channels from an add URL with the new --ch-add-url option 2024-12-01 23:33:47 -08:00
122 changed files with 23190 additions and 3905 deletions

No files matched your search

+16
View File
@@ -0,0 +1,16 @@
language: en-US
reviews:
profile: chill
high_level_summary: true
poem: false
auto_review:
enabled: true
drafts: false
ignore_usernames:
- renovate
- renovate[bot]
abort_on_close: true
path_filters:
- "!/protobufs/**"
- "!/meshtastic/protobuf/**"
+11
View File
@@ -0,0 +1,11 @@
*
!Containerfile*
!Dockerfile
!README.md
!LICENSE.md
!bin/container-entrypoint.sh
!examples/
!extra/
!meshtastic/
!poetry.lock
!pyproject.toml
+22
View File
@@ -0,0 +1,22 @@
# Contributing to Meshtastic Python
## Development resources
- [API Documentation](https://python.meshtastic.org/)
- [Meshtastic Python Development](https://meshtastic.org/docs/development/python/)
- [Building Meshtastic Python](https://meshtastic.org/docs/development/python/building/)
- [Using the Meshtastic Python Library](https://meshtastic.org/docs/development/python/library/)
## How to check your code (pytest/pylint) before a PR
- [Pre-requisites](https://meshtastic.org/docs/development/python/building/#pre-requisites)
- also execute `poetry install --all-extras --with dev,powermon` for all optional dependencies
- check your code with github ci actions locally
- You need to have act installed. You can get it at https://nektosact.com/
- on linux: `act -P ubuntu-latest=-self-hosted --matrix "python-version:3.12"`
- on windows:
- linux checks (linux docker): `act --matrix "python-version:3.12"`
- windows checks (windows host): `act -P ubuntu-latest=-self-hosted --matrix "python-version:3.12"`
- or run all locally:
- run `poetry run pylint meshtastic examples/ --ignore-patterns ".*_pb2.pyi?$"`
- run `poetry run mypy meshtastic/`
- run `poetry run pytest`
- more commands see [CI workflow](https://github.com/meshtastic/python/blob/master/.github/workflows/ci.yml)
+206
View File
@@ -0,0 +1,206 @@
# Copilot Instructions for Meshtastic Python
## Project Overview
This is the Meshtastic Python library and CLI - a Python API for interacting with Meshtastic mesh radio devices. It supports communication via Serial, TCP, and BLE interfaces.
## Technology Stack
- **Language**: Python 3.9 - 3.14
- **Package Manager**: Poetry
- **Testing**: pytest with hypothesis for property-based testing
- **Linting**: pylint
- **Type Checking**: mypy (working toward strict mode)
- **Documentation**: pdoc3
- **License**: GPL-3.0
## Project Structure
```
meshtastic/ # Main library package
├── __init__.py # Core interface classes and pub/sub topics
├── __main__.py # CLI entry point
├── mesh_interface.py # Base interface class for all connection types
├── serial_interface.py
├── tcp_interface.py
├── ble_interface.py
├── node.py # Node representation and configuration
├── protobuf/ # Generated Protocol Buffer files (*_pb2.py, *_pb2.pyi)
├── tests/ # Unit and integration tests
├── powermon/ # Power monitoring tools
└── analysis/ # Data analysis tools
examples/ # Usage examples
protobufs/ # Protocol Buffer source definitions
```
## Coding Standards
### Style Guidelines
- Follow PEP 8 style conventions
- Use type hints for function parameters and return values
- Document public functions and classes with docstrings
- Prefer explicit imports over wildcard imports
- Use `logging` module instead of print statements for debug output
### Type Annotations
- Add type hints to all new code
- Use `Optional[T]` for nullable types
- Use `Dict`, `List`, `Tuple` from `typing` module for Python 3.9 compatibility
- Protobuf types are in `meshtastic.protobuf.*_pb2` modules
### Naming Conventions
- Classes: `PascalCase` (e.g., `MeshInterface`, `SerialInterface`)
- Functions/methods: `camelCase` for public API (e.g., `sendText`, `sendData`)
- Internal functions: `snake_case` with leading underscore (e.g., `_send_packet`)
- Constants: `UPPER_SNAKE_CASE` (e.g., `BROADCAST_ADDR`, `LOCAL_ADDR`)
### Error Handling
- Use custom exception classes when appropriate (e.g., `MeshInterface.MeshInterfaceError`)
- Provide meaningful error messages
- Use `our_exit()` from `meshtastic.util` for CLI exits with error codes
## Testing
### Test Organization
Tests are in `meshtastic/tests/` and use pytest markers:
- `@pytest.mark.unit` - Fast unit tests (default)
- `@pytest.mark.unitslow` - Slower unit tests
- `@pytest.mark.int` - Integration tests
- `@pytest.mark.smoke1` - Single device smoke tests
- `@pytest.mark.smoke2` - Two device smoke tests
- `@pytest.mark.smokevirt` - Virtual device smoke tests
- `@pytest.mark.examples` - Example validation tests
### Running Tests
```bash
# Run unit tests only (default)
make test
# or
pytest -m unit
# Run all tests
pytest
# Run with coverage
make cov
```
### Writing Tests
- Use `pytest` fixtures from `conftest.py`
- Use `hypothesis` for property-based testing where appropriate
- Mock external dependencies (serial ports, network connections)
- Test file naming: `test_<module_name>.py`
## Pub/Sub Events
The library uses pypubsub for event handling. Key topics:
- `meshtastic.connection.established` - Connection successful
- `meshtastic.connection.lost` - Connection lost
- `meshtastic.receive.text(packet)` - Text message received
- `meshtastic.receive.position(packet)` - Position update received
- `meshtastic.receive.data.portnum(packet)` - Data packet by port number
- `meshtastic.node.updated(node)` - Node database changed
- `meshtastic.log.line(line)` - Raw log line from device
## Protocol Buffers
- Protobuf definitions are in `protobufs/meshtastic/`
- Generated Python files are in `meshtastic/protobuf/`
- Never edit `*_pb2.py` or `*_pb2.pyi` files directly
- Regenerate with: `make protobufs` or `./bin/regen-protobufs.sh`
## Common Patterns
### Creating an Interface
```python
import meshtastic.serial_interface
# Auto-detect device
iface = meshtastic.serial_interface.SerialInterface()
# Specific device
iface = meshtastic.serial_interface.SerialInterface(devPath="/dev/ttyUSB0")
# Always close when done
iface.close()
# Or use context manager
with meshtastic.serial_interface.SerialInterface() as iface:
iface.sendText("Hello mesh")
```
### Sending Messages
```python
# Text message (broadcast)
iface.sendText("Hello")
# Text message to specific node
iface.sendText("Hello", destinationId="!abcd1234")
# Binary data
iface.sendData(data, portNum=portnums_pb2.PRIVATE_APP)
```
### Subscribing to Events
```python
from pubsub import pub
def on_receive(packet, interface):
print(f"Received: {packet}")
pub.subscribe(on_receive, "meshtastic.receive")
```
## Development Workflow
1. Install dependencies: `poetry install --all-extras --with dev`
2. Make changes
3. Run linting: `poetry run pylint meshtastic examples/`
4. Run type checking: `poetry run mypy meshtastic/`
5. Run tests: `poetry run pytest -m unit`
6. Update documentation if needed
## CLI Development
The CLI is in `meshtastic/__main__.py`. When adding new CLI commands:
- Use argparse for argument parsing
- Support `--dest` for specifying target node
- Provide `--help` documentation
- Handle errors gracefully with meaningful messages
## Dependencies
### Required
- `pyserial` - Serial port communication
- `protobuf` - Protocol Buffers
- `pypubsub` - Pub/sub messaging
- `bleak` - BLE communication
- `tabulate` - Table formatting
- `pyyaml` - YAML config support
- `requests` - HTTP requests
### Optional (extras)
- `cli` extra: `pyqrcode`, `print-color`, `dotmap`, `argcomplete`
- `tunnel` extra: `pytap2`
- `analysis` extra: `dash`, `pandas`
## Important Notes
- Always test with actual Meshtastic hardware when possible
- Be mindful of radio regulations in your region
- The nodedb (`interface.nodes`) is read-only
- Packet IDs are random 32-bit integers
- Default timeout is 300 seconds for operations
Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

+54 -17
View File
@@ -1,4 +1,7 @@
name: CI
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
branches:
@@ -8,7 +11,29 @@ on:
- master
jobs:
validate:
runs-on: ubuntu-latest
strategy:
matrix:
python-version:
- "3.12"
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Install Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install meshtastic from local
run: |
python -m pip install --upgrade pip
pip3 install poetry
poetry install --all-extras --with dev,powermon
poetry run meshtastic --version
build:
needs: validate
runs-on: ubuntu-latest
strategy:
matrix:
@@ -17,10 +42,14 @@ jobs:
- "3.10"
- "3.11"
- "3.12"
- "3.13"
- "3.14"
steps:
- uses: actions/checkout@v4
- name: Install Python 3
- name: Install Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Uninstall meshtastic
run: |
pip3 uninstall -y meshtastic
@@ -32,15 +61,8 @@ jobs:
run: |
poetry install --all-extras --with dev,powermon
poetry run meshtastic --version
- name: Run pylint
run: poetry run pylint meshtastic examples/ --ignore-patterns ".*_pb2.pyi?$"
- name: Check types with mypy
run: poetry run mypy meshtastic/
- name: Run tests with pytest
run: poetry run pytest --cov=meshtastic
- name: Generate coverage report
run: |
poetry run pytest --cov=meshtastic --cov-report=xml
- name: Run lint, type check, and tests in parallel
run: make -j3 --output-sync=target ci
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
@@ -51,22 +73,37 @@ jobs:
name: codecov-umbrella
fail_ci_if_error: true
verbose: true
validate:
simradio_testing:
needs: [validate, build]
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version:
- "3.9"
- "3.10"
- "3.11"
- "3.12"
channel:
- beta
- alpha
- daily
continue-on-error: ${{ matrix.channel == 'daily' }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Install Python 3
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install meshtastic from local
run: |
python -m pip install --upgrade pip
pip3 install poetry
poetry install
poetry install --all-extras --with dev
poetry run meshtastic --version
- name: Install meshtasticd (${{ matrix.channel }}) from PPA
run: |
sudo add-apt-repository -y ppa:meshtastic/${{ matrix.channel }}
sudo apt-get update
sudo apt-get install -y meshtasticd
- name: Run firmware smoke tests
run: poetry run pytest -m "smokevirt or smokemesh" -v
timeout-minutes: 15
+79
View File
@@ -0,0 +1,79 @@
name: Create and publish Container image
on:
push:
branches:
- master
tags:
- '*.*.*'
pull_request:
branches:
- master
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-push-image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
strategy:
matrix:
include:
- container: Containerfile.debian
autotag: false
suffix: -debian
- container: Containerfile.alpine
autotag: ${{ github.ref == 'refs/heads/master' && 'true' || 'auto' }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Container registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=edge
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
flavor: |
latest=${{ matrix.autotag }}
suffix=${{ matrix.suffix }}
- name: Build and push
uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64
context: .
file: ${{ matrix.container }}
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
+36
View File
@@ -0,0 +1,36 @@
name: Daily SimRadio Tests
on:
schedule:
- cron: 0 6 * * *
workflow_dispatch:
permissions:
contents: read
jobs:
simradio_testing:
runs-on: ubuntu-latest
if: github.repository == 'meshtastic/python'
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Install Python 3
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install meshtastic from local
run: |
python -m pip install --upgrade pip
pip3 install poetry
poetry install --all-extras --with dev
poetry run meshtastic --version
- name: Install meshtasticd (daily) from PPA
run: |
sudo add-apt-repository -y ppa:meshtastic/daily
sudo apt-get update
sudo apt-get install -y meshtasticd
- name: Run firmware smoke tests
run: poetry run pytest -m "smokevirt or smokemesh" -v
timeout-minutes: 15
+74 -9
View File
@@ -1,5 +1,18 @@
name: "Update protobufs"
on: workflow_dispatch
on:
workflow_dispatch:
inputs:
version:
description: "Protobufs version tag to sync to (e.g., v2.7.26). Defaults to the latest protobufs release."
required: false
default: ""
type: string
repository_dispatch:
types: [protobufs-release]
permissions:
contents: write
jobs:
update-protobufs:
@@ -9,33 +22,85 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: true
persist-credentials: false
- name: Update Submodule
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install Poetry
run: |
git pull --recurse-submodules
git submodule update --remote --recursive
python -m pip install --upgrade pip
python -m pip install poetry
- name: Determine target protobufs version
id: version
env:
INPUT_VERSION: ${{ github.event.inputs.version }}
PAYLOAD_VERSION: ${{ github.event.client_payload.version }}
run: |
cd protobufs
git fetch --tags
if [ -n "${INPUT_VERSION}" ]; then
target="${INPUT_VERSION}"
elif [ -n "${PAYLOAD_VERSION}" ]; then
target="${PAYLOAD_VERSION}"
else
target=$(git tag --list 'v*' --sort=-version:refname | head -n1)
if [ -z "${target}" ]; then
echo "Error: no release tags found in meshtastic/protobufs"
exit 1
fi
fi
if ! git show-ref --verify --quiet "refs/tags/${target}"; then
echo "Error: '${target}' is not a valid tag in meshtastic/protobufs"
exit 1
fi
printf 'version=%s\n' "${target}" >> "$GITHUB_OUTPUT"
echo "Target protobufs version: ${target}"
- name: Update protobuf submodule to release
env:
TARGET_VERSION: ${{ steps.version.outputs.version }}
run: |
git submodule sync --recursive
git submodule update --init --recursive
cd protobufs
git checkout "${TARGET_VERSION}"
cd ..
- name: Download nanopb
run: |
wget https://jpa.kapsi.fi/nanopb/download/nanopb-0.4.8-linux-x86.tar.gz
curl -L -o nanopb-0.4.8-linux-x86.tar.gz https://jpa.kapsi.fi/nanopb/download/nanopb-0.4.8-linux-x86.tar.gz
tar xvzf nanopb-0.4.8-linux-x86.tar.gz
mv nanopb-0.4.8-linux-x86 nanopb-0.4.8
- name: Install poetry (needed by regen-protobufs.sh)
- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
pip3 install poetry
poetry install --with dev
- name: Re-generate protocol buffers
run: |
./bin/regen-protobufs.sh
- name: Commit update
env:
TARGET_VERSION: ${{ steps.version.outputs.version }}
run: |
git config --global user.name 'github-actions'
git config --global user.email 'bot@noreply.github.com'
git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}
git add protobufs
git add meshtastic/protobuf
git commit -m "Update protobuf submodule" && git push || echo "No changes to commit"
if [[ -n "$(git status --porcelain)" ]]; then
git commit -m "protobufs: ${TARGET_VERSION}"
git push
else
echo "No changes to commit"
fi
+2 -1
View File
@@ -17,4 +17,5 @@ examples/__pycache__
meshtastic.spec
.hypothesis/
coverage.xml
.ipynb_checkpoints
.ipynb_checkpoints
.cursor/
+26 -1
View File
@@ -4,6 +4,7 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "meshtastic BLE",
"type": "debugpy",
@@ -245,6 +246,30 @@
"module": "meshtastic",
"justMyCode": true,
"args": ["--debug", "--nodes"]
}
},
{
"name": "meshtastic nodes table",
"type": "debugpy",
"request": "launch",
"module": "meshtastic",
"justMyCode": true,
"args": ["--nodes"]
},
{
"name": "meshtastic nodes table with show-fields",
"type": "debugpy",
"request": "launch",
"module": "meshtastic",
"justMyCode": true,
"args": ["--nodes", "--show-fields", "AKA,Pubkey,Role,Role,Role,Latitude,Latitude,deviceMetrics.voltage"]
},
{
"name": "meshtastic --export-config",
"type": "debugpy",
"request": "launch",
"module": "meshtastic",
"justMyCode": true,
"args": ["--export-config", "config.json"]
},
]
}
+1
View File
@@ -0,0 +1 @@
Containerfile.alpine
+40
View File
@@ -0,0 +1,40 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# Copyright (C) 2025 Olliver Schinagl <oliver@schinagl.nl>
ARG TARGET_VERSION="3.11-alpine"
ARG TARGET_ARCH="library"
FROM docker.io/${TARGET_ARCH}/python:${TARGET_VERSION} AS builder
WORKDIR /tmp/build
COPY pyproject.toml poetry.lock /tmp/build/
RUN apk add --no-cache --virtual .build-deps build-base libffi-dev && \
pip install --no-cache-dir 'poetry==2.4.1' && \
poetry config virtualenvs.create false && \
poetry install --without dev --extras cli --extras tunnel --no-interaction --no-ansi --no-root
COPY . /tmp/build/
RUN poetry build --format wheel --no-interaction
FROM docker.io/${TARGET_ARCH}/python:${TARGET_VERSION}
RUN apk add --no-cache libffi && \
addgroup -S meshtastic && \
adduser -S -G meshtastic -h /home/meshtastic meshtastic
COPY --from=builder /tmp/build/dist/*.whl /tmp/
RUN wheel=$(echo /tmp/meshtastic-*.whl) && pip install --no-cache-dir "${wheel}[cli,tunnel]" && \
rm -f /tmp/meshtastic-*.whl
COPY "./bin/container-entrypoint.sh" "/init"
RUN chmod 0755 /init
WORKDIR /home/meshtastic
USER meshtastic
ENTRYPOINT [ "/init" ]
+37
View File
@@ -0,0 +1,37 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# Copyright (C) 2025 Olliver Schinagl <oliver@schinagl.nl>
ARG TARGET_VERSION="3.11"
ARG TARGET_ARCH="library"
FROM docker.io/${TARGET_ARCH}/python:${TARGET_VERSION} AS builder
WORKDIR /tmp/build
COPY pyproject.toml poetry.lock /tmp/build/
RUN pip install --no-cache-dir 'poetry==2.4.1' && \
poetry config virtualenvs.create false && \
poetry install --without dev --extras cli --extras tunnel --no-interaction --no-ansi --no-root
COPY . /tmp/build/
RUN poetry build --format wheel --no-interaction
FROM docker.io/${TARGET_ARCH}/python:${TARGET_VERSION}
RUN useradd --system --create-home --home-dir /home/meshtastic meshtastic
COPY --from=builder /tmp/build/dist/*.whl /tmp/
RUN wheel=$(echo /tmp/meshtastic-*.whl) && pip install --no-cache-dir "${wheel}[cli,tunnel]" && \
rm -f /tmp/meshtastic-*.whl
COPY "./bin/container-entrypoint.sh" "/init"
RUN chmod 0755 /init
WORKDIR /home/meshtastic
USER meshtastic
ENTRYPOINT [ "/init" ]
Symlink
+1
View File
@@ -0,0 +1 @@
Containerfile
File renamed without changes.
+14
View File
@@ -42,5 +42,19 @@ cov:
examples: FORCE
pytest -mexamples
# CI targets (run via poetry, executed in parallel by the CI workflow)
.PHONY: ci-pylint ci-mypy ci-test ci
ci-pylint:
poetry run pylint meshtastic examples/ --ignore-patterns ".*_pb2.pyi?$$"
ci-mypy:
poetry run mypy meshtastic/
ci-test:
poetry run pytest --cov=meshtastic --cov-report=xml
ci: ci-pylint ci-mypy ci-test
# Makefile hack to get the examples to always run
FORCE: ;
+31 -4
View File
@@ -1,4 +1,10 @@
# Meshtastic Python
<div align="center" markdown="1">
<img src=".github/meshtastic_logo.png" alt="Meshtastic Logo" width="80"/>
<h1 align="center"> Meshtastic Python
</h1>
<p style="font-size:15px;" align="center">A Python library and client for use with Meshtastic devices. </p>
[![codecov](https://codecov.io/gh/meshtastic/python/branch/master/graph/badge.svg?token=TIWPJL73KV)](https://codecov.io/gh/meshtastic/python)
![PyPI - Downloads](https://img.shields.io/pypi/dm/meshtastic)
@@ -7,16 +13,37 @@
[![Fiscal Contributors](https://opencollective.com/meshtastic/tiers/badge.svg?label=Fiscal%20Contributors&color=deeppink)](https://opencollective.com/meshtastic/)
![GPL-3.0](https://img.shields.io/badge/License-GPL%20v3-blue.svg)
</div>
<div align="center">
<a href="https://meshtastic.org/docs/software/python/cli/installation">Getting Started Guide</a>
-
<a href="https://python.meshtastic.org">API Documentation</a>
</div>
## Overview
A Python client for use with Meshtastic devices.
This small library (and example application) provides an easy API for sending and receiving messages over mesh radios.
It also provides access to any of the operations/data available in the device user interface or the Android application.
Events are delivered using a publish-subscribe model, and you can subscribe to only the message types you are interested in.
**[Getting Started Guide](https://meshtastic.org/docs/software/python/cli/installation)**
## Container usage
**[API Documentation](https://python.meshtastic.org)**
Container images are published to GHCR for this repository. The container entrypoint defaults to running `meshtastic`,
so CLI flags can be passed directly:
```bash
docker run --rm ghcr.io/meshtastic/python --help
```
To run another command, pass it explicitly (for example, a shell):
```bash
docker run --rm -it --entrypoint /bin/sh ghcr.io/meshtastic/python
```
The container runs as a non-root user by default. When talking to local hardware, pass the serial device through
explicitly (for example `--device /dev/ttyUSB0:/dev/ttyUSB0`) and ensure host device permissions allow access.
## Call for Contributors
+24
View File
@@ -0,0 +1,24 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-3.0-or-later
#
# Copyright (C) 2025 Olliver Schinagl <oliver@schinagl.nl>
#
# A beginning user should be able to docker run image bash (or sh) without
# needing to learn about --entrypoint
# https://github.com/docker-library/official-images#consistency
set -eu
bin='meshtastic'
# run command if it is not starting with a "-" and is an executable in PATH
if [ "${#}" -le 0 ] || \
[ "${1#-}" != "${1}" ] || \
[ -d "${1}" ] || \
! command -v "${1}" > '/dev/null' 2>&1; then
entrypoint='true'
fi
exec ${entrypoint:+${bin:?}} "${@}"
exit 0
+344
View File
@@ -0,0 +1,344 @@
#!/usr/bin/env python3
"""Inject nanopb .options constraints as inline field options into a .proto file.
The nanopb .options file format is specific to the nanopb C generator and is
ignored by standard protoc (including --python_out). By injecting the options
directly into the proto file's field declarations, protoc will embed them in
the serialized descriptor, making them accessible in Python via:
from meshtastic.protobuf import mesh_pb2, nanopb_pb2
field = mesh_pb2.DESCRIPTOR.message_types_by_name['User'].fields_by_name['long_name']
opts = field.GetOptions().Extensions[nanopb_pb2.nanopb]
print(opts.max_size) # 40
Usage:
inject_nanopb_options.py <options_file> <proto_file>
The proto_file is modified in-place. Intended to operate on temporary copies
generated by regen-protobufs.sh, not the source .proto files.
"""
import re
import sys
from pathlib import Path
from typing import Any, Dict, List, Tuple
# IntSize enum values from nanopb.proto
INT_SIZE_ENUM = {8: "IS_8", 16: "IS_16", 32: "IS_32", 64: "IS_64"}
# FieldType enum names from nanopb.proto. Only FT_IGNORE carries meaning for a
# Python client (the firmware omits the field entirely); the rest describe C
# storage class, but are passed through so the descriptor mirrors the .options.
FIELD_TYPE_ENUM = frozenset(
{"FT_DEFAULT", "FT_CALLBACK", "FT_POINTER", "FT_STATIC", "FT_IGNORE", "FT_INLINE"}
)
# Options that are valid proto FieldOptions and useful outside of C code generation.
# We skip C-only options (anonymous_oneof, no_unions, skip_message, packed_struct,
# packed_enum, mangle_names, callback_datatype, callback_function, descriptorsize,
# type_override) since they either don't apply to proto fields or are C-specific.
FIELD_OPTIONS = frozenset(
{
"max_size",
"max_length",
"max_count",
"int_size",
"type",
"fixed_length",
"fixed_count",
"long_names",
"proto3",
"default_has",
"sort_by_tag",
"msgid",
}
)
def parse_value(s: str) -> Any:
"""Convert an option value string to an appropriate Python type."""
if re.fullmatch(r"-?[0-9]+", s):
return int(s)
if s.lower() == "true":
return True
if s.lower() == "false":
return False
return s
def parse_options_file(
path: Path,
) -> Tuple[Dict[Tuple[str, ...], Dict[str, Any]], Dict[str, Dict[str, Any]]]:
"""Parse a nanopb .options file.
Returns:
specific: maps (message_path..., field_name) -> {option: value}
e.g. ('User', 'long_name') or ('Route', 'Link', 'uid')
wildcard: maps field_name -> {option: value}
applies to any field with this name in the same proto file
"""
specific: Dict[Tuple[str, ...], Dict[str, Any]] = {}
wildcard: Dict[str, Dict[str, Any]] = {}
with open(path, encoding="utf-8") as f:
for line in f:
# Strip inline comments
comment_pos = line.find("#")
if comment_pos >= 0:
line = line[:comment_pos]
line = line.strip().lstrip("*").strip()
if not line:
continue
tokens = line.split()
if len(tokens) < 2:
continue
pattern = tokens[0]
opts: Dict[str, Any] = {}
for tok in tokens[1:]:
if ":" in tok:
k, v = tok.split(":", 1)
if k in FIELD_OPTIONS:
opts[k] = parse_value(v)
if not opts:
continue
if "." in pattern:
# e.g. "User.long_name" -> key=('User', 'long_name')
# or "Route.Link.uid" -> key=('Route', 'Link', 'uid')
parts = tuple(pattern.split("."))
if parts in specific:
specific[parts].update(opts)
else:
specific[parts] = opts
else:
# wildcard: applies to any field with this name
if pattern in wildcard:
wildcard[pattern].update(opts)
else:
wildcard[pattern] = opts
return specific, wildcard
def format_nanopb_opts(opts: Dict[str, Any]) -> str:
"""Format a dict of nanopb options as a proto field options annotation string."""
parts = []
for k, v in opts.items():
if k == "int_size":
enum_val = INT_SIZE_ENUM.get(v, f"IS_{v}")
parts.append(f"(nanopb).int_size = {enum_val}")
elif k == "type":
if v not in FIELD_TYPE_ENUM:
raise ValueError(f"unknown nanopb field type {v!r}")
parts.append(f"(nanopb).type = {v}")
elif isinstance(v, bool):
parts.append(f"(nanopb).{k} = {'true' if v else 'false'}")
else:
parts.append(f"(nanopb).{k} = {v}")
return ", ".join(parts)
def message_path_matches(
context_stack: List[Tuple[str, str]], msg_path: Tuple[str, ...]
) -> bool:
"""Check if the current message context ends with msg_path.
context_stack entries are ('message'|'oneof'|'enum', name).
msg_path is the tuple of message names from the options pattern,
e.g. ('User',) or ('Route', 'Link').
"""
msg_names = [name for kind, name in context_stack if kind == "message"]
n = len(msg_path)
return len(msg_names) >= n and tuple(msg_names[-n:]) == msg_path
def inject_into_proto(
content: str,
specific: Dict[Tuple[str, ...], Dict[str, Any]],
wildcard: Dict[str, Dict[str, Any]],
nanopb_import_path: str,
) -> str:
"""Inject nanopb field options into a proto file's text content.
Adds an import for nanopb.proto if not already present.
Returns the modified content.
"""
if not specific and not wildcard:
return content
lines = content.split("\n")
# Check if nanopb is already imported (after sed fixup, it will be
# 'meshtastic/protobuf/nanopb.proto')
nanopb_already_imported = any(
"nanopb.proto" in line
for line in lines
if line.strip().startswith("import")
)
# Track the index of the last import line so we can insert after it
last_import_idx = max(
(
i
for i, line in enumerate(lines)
if line.strip().startswith("import ") and line.strip().endswith(";")
),
default=-1,
)
# State
context_stack: List[Tuple[str, str]] = [] # ('message'|'oneof'|'enum', name)
result: List[str] = []
import_added = nanopb_already_imported
# Patterns for proto structural elements
message_re = re.compile(r"^(\s*)message\s+(\w+)\s*\{")
oneof_re = re.compile(r"^(\s*)oneof\s+(\w+)\s*\{")
enum_re = re.compile(r"^(\s*)enum\s+(\w+)\s*\{")
close_re = re.compile(r"^\s*\}")
# Pattern for field declarations:
# indent [optional|repeated] type name = number [options] ;
# We exclude map<> fields (different syntax, nanopb handles them differently)
# and enum value lines (no type keyword before the name).
field_re = re.compile(
r"^(\s*)" # (1) indent
r"(optional\s+|repeated\s+)?" # (2) optional qualifier
r"([\w.]+)\s+" # (3) field type (possibly qualified like google.protobuf.Any)
r"(\w+)\s*" # (4) field name
r"=\s*(\d+)" # (5) field number
r"(?:\s*\[([^\]]*)\])?" # (6) existing options, without brackets
r"\s*;" # trailing semicolon
)
for i, line in enumerate(lines):
# Insert nanopb import right after the last existing import line.
# Only do this when there IS an existing import (last_import_idx >= 0);
# if there are no imports we fall through to the syntax-line fallback below.
if not import_added and last_import_idx >= 0 and i == last_import_idx + 1:
result.append(f'import "{nanopb_import_path}";')
import_added = True
# --- Track message/oneof/enum nesting ---
m = message_re.match(line)
if m:
context_stack.append(("message", m.group(2)))
result.append(line)
continue
m = oneof_re.match(line)
if m:
context_stack.append(("oneof", m.group(2)))
result.append(line)
continue
m = enum_re.match(line)
if m:
context_stack.append(("enum", m.group(2)))
result.append(line)
continue
if close_re.match(line) and context_stack:
context_stack.pop()
result.append(line)
continue
# Skip field injection inside enum bodies (enum values look like fields
# but should not have nanopb options added)
in_enum = bool(context_stack) and context_stack[-1][0] == "enum"
# --- Try to match and modify a field declaration ---
m = field_re.match(line)
if m and not in_enum:
indent = m.group(1)
qualifier = m.group(2) or ""
ftype = m.group(3)
fname = m.group(4)
fnum = m.group(5)
existing_opts = m.group(6) or ""
# Collect applicable nanopb options (wildcard < specific)
extra: Dict[str, Any] = {}
# 1. Wildcard: any field with this name in this proto file
if fname in wildcard:
extra.update(wildcard[fname])
# 2. Specific: check all keys whose last element is fname and whose
# preceding path matches the current message context
for key, opts in specific.items():
if key[-1] == fname:
msg_path = key[:-1]
if message_path_matches(context_stack, msg_path):
extra.update(opts)
break
if extra:
nanopb_str = format_nanopb_opts(extra)
if existing_opts.strip():
opts_block = f"[{existing_opts}, {nanopb_str}]"
else:
opts_block = f"[{nanopb_str}]"
qual = qualifier.rstrip()
sep = " " if qual else ""
line = f"{indent}{qual}{sep}{ftype} {fname} = {fnum} {opts_block};"
result.append(line)
# Edge case: if there were no import lines, add nanopb import after syntax line
if not import_added:
for i, line in enumerate(result):
if line.strip().startswith("syntax") and line.strip().endswith(";"):
result.insert(i + 1, f'import "{nanopb_import_path}";')
break
return "\n".join(result)
def main() -> int:
"""Parse an .options file and inject its constraints into a .proto file in-place."""
if len(sys.argv) != 3:
print(
f"Usage: {sys.argv[0]} <options_file> <proto_file>",
file=sys.stderr,
)
return 1
opts_path = Path(sys.argv[1])
proto_path = Path(sys.argv[2])
if not opts_path.exists():
print(f"Options file not found: {opts_path}", file=sys.stderr)
return 1
if not proto_path.exists():
print(f"Proto file not found: {proto_path}", file=sys.stderr)
return 1
specific, wildcard = parse_options_file(opts_path)
total = len(specific) + len(wildcard)
if total == 0:
print(f" [{opts_path.name}] No injectable options found, skipping.")
return 0
content = proto_path.read_text(encoding="utf-8")
# After regen-protobufs.sh's sed fixup, the nanopb import path is:
nanopb_import_path = "meshtastic/protobuf/nanopb.proto"
modified = inject_into_proto(content, specific, wildcard, nanopb_import_path)
proto_path.write_text(modified, encoding="utf-8")
print(
f" [{opts_path.name}] Injected {len(specific)} specific + "
f"{len(wildcard)} wildcard option(s) into {proto_path.name}"
)
return 0
if __name__ == "__main__":
sys.exit(main())
+23
View File
@@ -6,6 +6,12 @@ set -e
#gsed -i 's/import "\//import ".\//g' ./protobufs/meshtastic/*
#gsed -i 's/package meshtastic;//g' ./protobufs/meshtastic/*
POETRYDIR=$(poetry env info --path)
if [[ -z "${POETRYDIR}" ]]; then
poetry install
fi
# protoc looks for mypy plugin in the python path
source $(poetry env info --path)/bin/activate
@@ -22,6 +28,8 @@ OUTDIR=${TMPDIR}/out
PYIDIR=${TMPDIR}/out
mkdir -p "${OUTDIR}" "${INDIR}" "${PYIDIR}"
cp ./protobufs/meshtastic/*.proto "${INDIR}"
cp ./protobufs/meshtastic/*.options "${INDIR}"
cp ./protobufs/nanopb.proto "${INDIR}"
# OS-X sed is apparently a little different and expects an arg for -i
if [[ $OSTYPE == 'darwin'* ]]; then
@@ -36,6 +44,21 @@ $SEDCMD 's/^package meshtastic;/package meshtastic.protobuf;/' "${INDIR}/"*.prot
# fix the imports to match
$SEDCMD 's/^import "meshtastic\//import "meshtastic\/protobuf\//' "${INDIR}/"*.proto
$SEDCMD 's/^import "nanopb.proto"/import "meshtastic\/protobuf\/nanopb.proto"/' "${INDIR}/"*.proto
# Inject nanopb .options constraints as inline proto field options so that
# protoc --python_out embeds them in the generated descriptors. Python code
# can then read them via:
# field.GetOptions().Extensions[nanopb_pb2.nanopb].max_size
echo "Injecting nanopb options into proto files..."
for OPTS_FILE in "${INDIR}"/*.options; do
BASENAME=$(basename "${OPTS_FILE}" .options)
PROTO_FILE="${INDIR}/${BASENAME}.proto"
if [ -f "${PROTO_FILE}" ]; then
python3 ./bin/inject_nanopb_options.py "${OPTS_FILE}" "${PROTO_FILE}"
fi
done
# Generate the python files
./nanopb-0.4.8/generator-bin/protoc -I=$TMPDIR/in --python_out "${OUTDIR}" "--mypy_out=${PYIDIR}" $INDIR/*.proto
+3
View File
@@ -4,6 +4,9 @@ owner_short: BOB
channel_url: https://www.meshtastic.org/e/#CgMSAQESCDgBQANIAVAe
canned_messages: Hi|Bye|Yes|No|Ok
ringtone: 24:d=32,o=5,b=565:f6,p,f6,4p,p,f6,p,f6,2p,p,b6,p,b6,p,b6,p,b6,p,b,p,b,p,b,p,b,p,b,p,b,p,b,p,b,1p.,2p.,p
location:
lat: 35.88888
lon: -93.88888
+70
View File
@@ -0,0 +1,70 @@
# Contributing Example Scripts
Use this guide when adding or updating scripts in `examples/`.
## Must-have checklist before opening a PR
1. Script teaches one clear thing (its primary learning goal).
2. File name matches that goal.
3. Top docstring states purpose, transport scope, behavior, and expected output.
4. Script has safe shutdown (`with ...` or `finally`) and graceful `KeyboardInterrupt` handling.
5. Argument handling is clear (`argparse` preferred).
6. Errors are explicit (no bare `except:`).
7. The script is not a near-duplicate of an existing example.
## Choose the right teaching goal
Each example should have one primary lesson. Keep it focused.
- Good: "Send one text message over serial."
- Good: "Print inbound text messages."
- Avoid: discovery + chat + config mutation all in one script unless that combined flow is the lesson.
## Transport scope must be explicit
State exactly what transports are supported and why.
- Serial-only when that keeps the example simplest.
- Multi-transport (Serial/TCP/BLE) only when transport selection is part of the lesson.
- If TCP/BLE are supported, expose explicit flags (`--host`, `--ble`) and document defaults.
## Behavior and output should be predictable
Readers should know if the script sends, receives, mutates config, or combines those.
- Receive examples: subscribe to the narrowest pubsub topic that matches the lesson.
- Send examples: clarify destination behavior (broadcast default vs explicit destination).
- Mutation examples: clearly document side effects.
Output should make success easy to confirm:
- Print concise, stable status/event lines.
- Avoid noisy debug output unless the script is specifically diagnostic-focused.
## Cleanup and error handling
- Use context managers where practical; otherwise close interfaces in `finally`.
- Handle `KeyboardInterrupt` cleanly.
- Exit non-zero for invalid args, connection/setup failures, or command failures.
## Naming guidance
Use descriptive names tied to the teaching goal.
- Prefer names like `tcp_connection_info_once.py` over `pub_sub_example.py`.
- Prefer names like `tcp_pubsub_send_and_receive.py` over `pub_sub_example2.py`.
- Avoid generic names such as `example2.py`.
Keep existing filenames only when compatibility or discoverability outweighs clarity.
## New script vs extending an existing one
Create a new script when:
- The learning goal is genuinely distinct.
- Combining behaviors would make either example harder to understand.
Extend an existing script when:
- The change deepens the same lesson.
- The resulting script remains focused and readable.
+35 -14
View File
@@ -1,21 +1,42 @@
"""Simple program to demo how to use meshtastic library.
To run: python examples/get_hw.py
"""Print the local node hardware model.
Purpose: show the narrowest read-only local hardware lookup.
Transport scope: Serial only.
Behavior: reads local node metadata and prints hwModel.
Expected output: one hardware model line, if available.
Cleanup/error handling: exits with code 3 for bad args and closes interface on exit.
"""
import argparse
import sys
import meshtastic
import meshtastic.serial_interface
# simple arg check
if len(sys.argv) != 1:
print(f"usage: {sys.argv[0]}")
print("Print the hardware model for the local node.")
sys.exit(3)
iface = meshtastic.serial_interface.SerialInterface()
if iface.nodes:
for n in iface.nodes.values():
if n["num"] == iface.myInfo.my_node_num:
print(n["user"]["hwModel"])
iface.close()
def main() -> int:
"""Print the hardware model for the local node."""
if len(sys.argv) != 1:
print(f"usage: {sys.argv[0]}")
print("Print the hardware model for the local node.")
return 3
parser = argparse.ArgumentParser(description="Print local Meshtastic hardware model")
parser.parse_args()
try:
with meshtastic.serial_interface.SerialInterface() as iface:
if iface.nodes:
for node in iface.nodes.values():
if node["num"] == iface.myInfo.my_node_num:
print(node["user"]["hwModel"])
break
except KeyboardInterrupt:
return 0
except Exception as exc:
print(f"Error: Could not read hardware model: {exc}")
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
+31 -12
View File
@@ -1,19 +1,38 @@
"""Simple program to demo how to use meshtastic library.
To run: python examples/hello_world_serial.py
"""Send one text message over serial.
Purpose: minimal send-only example.
Transport scope: Serial only.
Behavior: sends one message and exits.
Expected output: no output on success.
Cleanup/error handling: exits with code 3 for bad args, closes interface on exit.
"""
import argparse
import sys
import meshtastic
import meshtastic.serial_interface
# simple arg check
if len(sys.argv) < 2:
print(f"usage: {sys.argv[0]} message")
sys.exit(3)
# By default will try to find a meshtastic device,
# otherwise provide a device path like /dev/ttyUSB0
iface = meshtastic.serial_interface.SerialInterface()
iface.sendText(sys.argv[1])
iface.close()
def main() -> int:
"""Parse arguments and send one text message."""
if len(sys.argv) < 2:
print(f"usage: {sys.argv[0]} message")
return 3
parser = argparse.ArgumentParser(description="Send one Meshtastic text message over serial")
parser.add_argument("message", help="Message text to broadcast")
args = parser.parse_args()
try:
with meshtastic.serial_interface.SerialInterface() as iface:
iface.sendText(args.message)
except KeyboardInterrupt:
return 0
except Exception as exc:
print(f"Error: Could not send message: {exc}")
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
+39 -13
View File
@@ -1,20 +1,46 @@
"""Simple program to demo how to use meshtastic library.
To run: python examples/info.py
"""Show a concise local node summary over serial.
Purpose: read local node identity and metadata in one place.
Transport scope: Serial only.
Behavior: reads node database, prints local node ID/name/hardware model.
Expected output: 1-3 summary lines describing the local node.
Cleanup/error handling: closes interface on exit and prints clear errors on failure.
"""
import meshtastic
import meshtastic.serial_interface
iface = meshtastic.serial_interface.SerialInterface()
# call showInfo() just to ensure values are populated
# info = iface.showInfo()
def main() -> int:
"""Print local node summary fields."""
try:
with meshtastic.serial_interface.SerialInterface() as iface:
local_num = iface.myInfo.my_node_num
local_node = None
if iface.nodes:
for node in iface.nodes.values():
if node["num"] == local_num:
local_node = node
break
if not local_node:
print(f"Local node not found in node database (node num: {local_num}).")
return 1
user = local_node.get("user", {})
print(f"Node number: {local_num}")
print(f"Node ID: {local_node.get('id', 'unknown')}")
print(
"Name: "
f"{user.get('longName', 'unknown')} ({user.get('shortName', 'unknown')})"
)
print(f"Hardware model: {user.get('hwModel', 'unknown')}")
except KeyboardInterrupt:
return 0
except Exception as exc:
print(f"Error: Could not read local node summary: {exc}")
return 1
return 0
if iface.nodes:
for n in iface.nodes.values():
if n["num"] == iface.myInfo.my_node_num:
print(n["user"]["hwModel"])
break
iface.close()
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,64 @@
#!/usr/bin/env python3
"""Passively monitor incoming text messages over serial.
Purpose: receive-only monitor for text messages.
Transport scope: Serial only.
Behavior: subscribes to text receive events and prints timestamp/channel/sender/text.
Expected output: one line per received text message.
Cleanup/error handling: graceful Ctrl+C exit and explicit connection errors.
"""
import argparse
import time
from datetime import datetime
from typing import Any, Optional
from pubsub import pub
import meshtastic.serial_interface
_TZ_NAME = time.tzname[time.localtime().tm_isdst > 0]
def on_receive(packet: dict[str, Any], interface: Any) -> None: # pylint: disable=unused-argument
"""Print a compact line for each received text packet."""
decoded = packet.get("decoded", {})
if decoded.get("portnum") != "TEXT_MESSAGE_APP":
return
message = decoded.get("text")
if not message:
return
channel_num = packet.get("channel", 0)
sender_id = packet.get("fromId", "unknown")
message_time = datetime.now().strftime(f"%a %b %d %Y %H:%M:%S {_TZ_NAME}")
print(f"{message_time} : {channel_num} : {sender_id} : {message}")
def main() -> int:
"""Connect over serial and print inbound text messages."""
parser = argparse.ArgumentParser(description="Read incoming Meshtastic text over serial")
parser.add_argument("--port", default=None, help="Serial port path (default: auto-detect)")
args = parser.parse_args()
pub.subscribe(on_receive, "meshtastic.receive")
iface: Optional[meshtastic.serial_interface.SerialInterface] = None
try:
iface = meshtastic.serial_interface.SerialInterface(devPath=args.port)
print("Connected. Listening for text messages. Press Ctrl+C to exit.")
while True:
time.sleep(1)
except KeyboardInterrupt:
return 0
except Exception as exc:
print(f"Error: Could not monitor serial messages: {exc}")
return 1
finally:
if iface:
iface.close()
return 0
if __name__ == "__main__":
raise SystemExit(main())
-30
View File
@@ -1,30 +0,0 @@
"""Simple program to demo how to use meshtastic library.
To run: python examples/pub_sub_example.py
"""
import sys
from pubsub import pub
import meshtastic
import meshtastic.tcp_interface
# simple arg check
if len(sys.argv) < 2:
print(f"usage: {sys.argv[0]} host")
sys.exit(1)
def onConnection(interface, topic=pub.AUTO_TOPIC): # pylint: disable=unused-argument
"""This is called when we (re)connect to the radio."""
print(interface.myInfo)
interface.close()
pub.subscribe(onConnection, "meshtastic.connection.established")
try:
iface = meshtastic.tcp_interface.TCPInterface(sys.argv[1])
except:
print(f"Error: Could not connect to {sys.argv[1]}")
sys.exit(1)
-39
View File
@@ -1,39 +0,0 @@
"""Simple program to demo how to use meshtastic library.
To run: python examples/pub_sub_example2.py
"""
import sys
import time
from pubsub import pub
import meshtastic
import meshtastic.tcp_interface
# simple arg check
if len(sys.argv) < 2:
print(f"usage: {sys.argv[0]} host")
sys.exit(1)
def onReceive(packet, interface): # pylint: disable=unused-argument
"""called when a packet arrives"""
print(f"Received: {packet}")
def onConnection(interface, topic=pub.AUTO_TOPIC): # pylint: disable=unused-argument
"""called when we (re)connect to the radio"""
# defaults to broadcast, specify a destination ID if you wish
interface.sendText("hello mesh")
pub.subscribe(onReceive, "meshtastic.receive")
pub.subscribe(onConnection, "meshtastic.connection.established")
try:
iface = meshtastic.tcp_interface.TCPInterface(hostname=sys.argv[1])
while True:
time.sleep(1000)
iface.close()
except Exception as ex:
print(f"Error: Could not connect to {sys.argv[1]} {ex}")
sys.exit(1)
+73
View File
@@ -0,0 +1,73 @@
"""Auto-reply to received text messages.
Purpose: demonstrate receive callback + generated reply flow.
Transport scope: Serial default, optional TCP/BLE.
Behavior: listens for text, prints message metadata, sends one reply per text message.
Expected output: "Connected..." plus message/reply lines while running.
Cleanup/error handling: clear connect failures and graceful Ctrl+C close.
"""
import argparse
import time
from typing import Any, Optional, Union
from pubsub import pub
import meshtastic.serial_interface
import meshtastic.tcp_interface
import meshtastic.ble_interface
from meshtastic.mesh_interface import MeshInterface
def onReceive(packet: dict, interface: MeshInterface) -> None:
"""Reply to every received packet with some info"""
text: Optional[str] = packet.get("decoded", {}).get("text")
if text:
rx_snr: Any = packet.get("rxSnr", "unknown")
hop_limit: Any = packet.get("hopLimit", "unknown")
print(f"message: {text}")
reply: str = f"got msg '{text}' with rxSnr: {rx_snr} and hopLimit: {hop_limit}"
print("Sending reply: ", reply)
interface.sendText(reply)
def onConnection(interface: MeshInterface, topic: Any = pub.AUTO_TOPIC) -> None: # pylint: disable=unused-argument
"""called when we (re)connect to the radio"""
print("Connected. Will auto-reply to all messages while running.")
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:
# note: timeout only applies after connection, not during the initial connect attempt
# TCPInterface.myConnect() calls socket.create_connection() without a timeout
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()
+40 -16
View File
@@ -1,7 +1,13 @@
"""Program to scan for hardware
To run: python examples/scan_for_devices.py
"""Scan host serial hardware for supported Meshtastic devices.
Purpose: host-side discovery without opening a radio session.
Transport scope: none (OS/device scanning only).
Behavior: scans vendor IDs, lists matched devices, and candidate active ports.
Expected output: vendor ID list, zero-or-more detected devices, and port list.
Cleanup/error handling: exits with code 3 for bad args and code 1 on scan errors.
"""
import argparse
import sys
from meshtastic.util import (
@@ -10,20 +16,38 @@ from meshtastic.util import (
get_unique_vendor_ids,
)
# simple arg check
if len(sys.argv) != 1:
print(f"usage: {sys.argv[0]}")
print("Detect which device we might have.")
sys.exit(3)
vids = get_unique_vendor_ids()
print(f"Searching for all devices with these vendor ids {vids}")
def main() -> int:
"""Run device detection and print candidate ports."""
if len(sys.argv) != 1:
print(f"usage: {sys.argv[0]}")
print("Detect which device we might have.")
return 3
sds = detect_supported_devices()
if len(sds) > 0:
print("Detected possible devices:")
for d in sds:
print(f" name:{d.name}{d.version} firmware:{d.for_firmware}")
parser = argparse.ArgumentParser(description="Scan host for supported Meshtastic devices")
parser.parse_args()
ports = active_ports_on_supported_devices(sds)
print(f"ports:{ports}")
try:
vids = get_unique_vendor_ids()
print(f"Searching for all devices with these vendor ids {vids}")
supported_devices = detect_supported_devices()
if supported_devices:
print("Detected possible devices:")
for device in supported_devices:
print(
f" name:{device.name}{device.version} firmware:{device.for_firmware}"
)
else:
print("Detected possible devices: none")
ports = active_ports_on_supported_devices(supported_devices)
print(f"ports:{ports}")
except Exception as exc:
print(f"Error: device scan failed: {exc}")
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
+33 -14
View File
@@ -1,21 +1,40 @@
"""Simple program to demo how to use meshtastic library.
To run: python examples/set_owner.py Bobby 333
"""Set local owner long/short name over serial.
Purpose: demonstrate a local config mutation workflow.
Transport scope: Serial only.
Behavior: updates owner long name and optional short name.
Expected output: prints the owner values being applied.
Cleanup/error handling: exits with code 3 for bad args and closes interface on exit.
"""
import argparse
import sys
import meshtastic
import meshtastic.serial_interface
# simple arg check
if len(sys.argv) < 2:
print(f"usage: {sys.argv[0]} long_name [short_name]")
sys.exit(3)
iface = meshtastic.serial_interface.SerialInterface()
long_name = sys.argv[1]
short_name = None
if len(sys.argv) > 2:
short_name = sys.argv[2]
iface.localNode.setOwner(long_name, short_name)
iface.close()
def main() -> int:
"""Parse args and set owner fields."""
if len(sys.argv) < 2:
print(f"usage: {sys.argv[0]} long_name [short_name]")
return 3
parser = argparse.ArgumentParser(description="Set Meshtastic local owner information")
parser.add_argument("long_name", help="Owner long name")
parser.add_argument("short_name", nargs="?", default=None, help="Owner short name")
args = parser.parse_args()
print(f"Setting owner long_name={args.long_name}, short_name={args.short_name}")
try:
with meshtastic.serial_interface.SerialInterface() as iface:
iface.localNode.setOwner(args.long_name, args.short_name)
except KeyboardInterrupt:
return 0
except Exception as exc:
print(f"Error: Could not set owner: {exc}")
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
+20 -2
View File
@@ -1,6 +1,24 @@
"""Simple program to show serial ports.
"""List serial ports currently visible to Meshtastic helpers.
Purpose: fastest host-side serial port enumeration.
Transport scope: none (host serial listing only).
Behavior: prints result of `findPorts()`.
Expected output: list-like representation of available candidate ports.
Cleanup/error handling: exits with code 1 on unexpected scan error.
"""
from meshtastic.util import findPorts
print(findPorts())
def main() -> int:
"""Print discovered serial ports."""
try:
print(findPorts())
except Exception as exc:
print(f"Error: Could not list ports: {exc}")
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
+41
View File
@@ -0,0 +1,41 @@
"""Connect over TCP, print connection info once, then exit.
Purpose: demonstrate pubsub connection lifecycle callback.
Transport scope: TCP only.
Behavior: subscribe to `meshtastic.connection.established`, print `myInfo`, then close.
Expected output: one object/line showing local radio info after connect.
Cleanup/error handling: explicit connect failure message and clean close on callback.
"""
import argparse
from pubsub import pub
import meshtastic.tcp_interface
def on_connection(interface, topic=pub.AUTO_TOPIC): # pylint: disable=unused-argument
"""Print local radio info when connected, then close."""
print(interface.myInfo)
interface.close()
def main() -> int:
"""Parse args, connect, and wait for established callback."""
parser = argparse.ArgumentParser(description="Print radio info on TCP connect and exit")
parser.add_argument("host", help="TCP hostname or IP of the Meshtastic node")
args = parser.parse_args()
pub.subscribe(on_connection, "meshtastic.connection.established")
try:
meshtastic.tcp_interface.TCPInterface(args.host)
except KeyboardInterrupt:
return 0
except Exception as exc:
print(f"Error: Could not connect to {args.host}: {exc}")
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
+43 -10
View File
@@ -1,14 +1,47 @@
"""Demonstration of how to look up a radio's location via its LAN connection.
Before running, connect your machine to the same WiFi network as the radio.
"""
"""Look up local node position over TCP.
Purpose: demonstrate read-only position lookup via LAN/TCP.
Transport scope: TCP only.
Behavior: connects, reads local node position, prints it, then exits.
Expected output: position dict for local node.
Cleanup/error handling: explicit connect/read failures and clean close.
"""
# pylint: disable=duplicate-code
import argparse
import meshtastic
import meshtastic.tcp_interface
radio_hostname = "meshtastic.local" # Can also be an IP
iface = meshtastic.tcp_interface.TCPInterface(radio_hostname)
my_node_num = iface.myInfo.my_node_num
pos = iface.nodesByNum[my_node_num]["position"]
print(pos)
iface.close()
def main() -> int:
"""Connect over TCP and print local node position."""
parser = argparse.ArgumentParser(description="Print local node position over TCP")
parser.add_argument(
"--host",
default="meshtastic.local",
help="TCP hostname or IP (default: meshtastic.local)",
)
args = parser.parse_args()
iface = None
try:
iface = meshtastic.tcp_interface.TCPInterface(args.host)
my_node_num = iface.myInfo.my_node_num
pos = iface.nodesByNum[my_node_num].get("position")
if pos is None:
print(f"No position available for local node {my_node_num}")
return 1
print(pos)
except KeyboardInterrupt:
return 0
except Exception as exc:
print(f"Error: Could not read position from {args.host}: {exc}")
return 1
finally:
if iface:
iface.close()
return 0
if __name__ == "__main__":
raise SystemExit(main())
+54
View File
@@ -0,0 +1,54 @@
"""Send once on connect and print received packets over TCP.
Purpose: demonstrate pubsub send-on-connect plus receive callback flow.
Transport scope: TCP only.
Behavior: sends "hello mesh" at connect, prints packets while running.
Expected output: "Connected..." plus "Received: ..." lines for inbound packets.
Cleanup/error handling: graceful Ctrl+C exit and clean interface close.
"""
import argparse
import time
from pubsub import pub
from meshtastic.tcp_interface import TCPInterface
def on_receive(packet, interface): # pylint: disable=unused-argument
"""Print each inbound packet."""
print(f"Received: {packet}")
def on_connection(interface, topic=pub.AUTO_TOPIC): # pylint: disable=unused-argument
"""Send a broadcast text when connected."""
print("Connected. Sending one broadcast message.")
interface.sendText("hello mesh")
def main() -> int:
"""Parse args, connect via TCP, and run callbacks."""
parser = argparse.ArgumentParser(description="TCP pubsub send-and-receive example")
parser.add_argument("host", help="TCP hostname or IP of the Meshtastic node")
args = parser.parse_args()
pub.subscribe(on_receive, "meshtastic.receive")
pub.subscribe(on_connection, "meshtastic.connection.established")
iface = None
try:
iface = TCPInterface(hostname=args.host)
while True:
time.sleep(1)
except KeyboardInterrupt:
return 0
except Exception as exc:
print(f"Error: Could not connect to {args.host}: {exc}")
return 1
finally:
if iface:
iface.close()
return 0
if __name__ == "__main__":
raise SystemExit(main())
+73
View File
@@ -0,0 +1,73 @@
"""Interactive text chat demo.
Purpose: demonstrate bidirectional text chat loop.
Transport scope: Serial default, optional TCP/BLE.
Behavior: prints incoming messages and sends each typed line as text.
Expected output: incoming sender/text lines and sent messages reaching peers.
Cleanup/error handling: explicit connect errors and graceful Ctrl+C / EOF close.
"""
import argparse
from typing import Any, Optional, Union
from pubsub import pub
import meshtastic.serial_interface
import meshtastic.tcp_interface
import meshtastic.ble_interface
from meshtastic.mesh_interface import MeshInterface
def onReceive(packet: dict, interface: MeshInterface) -> None: # pylint: disable=unused-argument
"""called when a packet arrives"""
text: Optional[str] = packet.get("decoded", {}).get("text")
if text:
sender: str = packet.get("fromId", "unknown")
print(f"{sender}: {text}")
def onConnection(interface: MeshInterface, topic: Any = pub.AUTO_TOPIC) -> None: # pylint: disable=unused-argument
"""called when we (re)connect to the radio"""
print("Connected. Type a message and press Enter to send. Ctrl+C to exit.")
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: 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:
# note: timeout only applies after connection, not during the initial connect attempt
# TCPInterface.myConnect() calls socket.create_connection() without a timeout
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
assert iface is not None
try:
while True:
line = input()
if line:
iface.sendText(line)
except KeyboardInterrupt:
pass
except EOFError:
pass
finally:
if iface:
iface.close()
+31 -26
View File
@@ -1,7 +1,10 @@
"""Program to create and delete waypoint
To run:
python3 examples/waypoint.py --port /dev/ttyUSB0 create 45 test the_desc_2 '2024-12-18T23:05:23' 48.74 7.35
python3 examples/waypoint.py delete 45
"""Create or delete a waypoint.
Purpose: demonstrate waypoint mutation API (create/delete).
Transport scope: Serial only.
Behavior: sends waypoint create/delete request and prints API response.
Expected output: request response object printed to stdout.
Cleanup/error handling: explicit argument parsing and clean interface close.
"""
import argparse
@@ -12,25 +15,28 @@ import meshtastic
import meshtastic.serial_interface
parser = argparse.ArgumentParser(
prog='waypoint',
description='Create and delete Meshtastic waypoint')
parser.add_argument('--port', default=None)
parser.add_argument('--debug', default=False, action='store_true')
prog="waypoint", description="Create and delete Meshtastic waypoint"
)
parser.add_argument("--port", default=None)
parser.add_argument("--debug", default=False, action="store_true")
subparsers = parser.add_subparsers(dest='cmd')
parser_delete = subparsers.add_parser('delete', help='Delete a waypoint')
parser_delete.add_argument('id', help="id of the waypoint")
subparsers = parser.add_subparsers(dest="cmd", required=True)
parser_delete = subparsers.add_parser("delete", help="Delete a waypoint")
parser_delete.add_argument("id", type=int, help="ID of the waypoint")
parser_create = subparsers.add_parser('create', help='Create a new waypoint')
parser_create.add_argument('id', help="id of the waypoint")
parser_create.add_argument('name', help="name of the waypoint")
parser_create.add_argument('description', help="description of the waypoint")
parser_create.add_argument('expire', help="expiration date of the waypoint as interpreted by datetime.fromisoformat")
parser_create.add_argument('latitude', help="latitude of the waypoint")
parser_create.add_argument('longitude', help="longitude of the waypoint")
parser_create = subparsers.add_parser("create", help="Create a new waypoint")
parser_create.add_argument("id", type=int, help="ID of the waypoint")
parser_create.add_argument("name", help="Name of the waypoint")
parser_create.add_argument("description", help="Description of the waypoint")
parser_create.add_argument("icon", help="Icon of the waypoint")
parser_create.add_argument(
"expire",
help="Expiration time as ISO timestamp accepted by datetime.fromisoformat",
)
parser_create.add_argument("latitude", type=float, help="Latitude of the waypoint")
parser_create.add_argument("longitude", type=float, help="Longitude of the waypoint")
args = parser.parse_args()
print(args)
# By default will try to find a meshtastic device,
# otherwise provide a device path like /dev/ttyUSB0
@@ -39,17 +45,16 @@ if args.debug:
else:
d = None
with meshtastic.serial_interface.SerialInterface(args.port, debugOut=d) as iface:
if args.cmd == 'create':
if args.cmd == "create":
p = iface.sendWaypoint(
waypoint_id=int(args.id),
waypoint_id=args.id,
name=args.name,
description=args.description,
icon=args.icon,
expire=int(datetime.datetime.fromisoformat(args.expire).timestamp()),
latitude=float(args.latitude),
longitude=float(args.longitude),
latitude=args.latitude,
longitude=args.longitude,
)
else:
p = iface.deleteWaypoint(int(args.id))
p = iface.deleteWaypoint(args.id)
print(p)
# iface.close()
+35 -11
View File
@@ -35,6 +35,7 @@ type of packet, you should subscribe to the full topic name. If you want to see
- `meshtastic.receive.data.portnum(packet)` (where portnum is an integer or well known PortNum enum)
- `meshtastic.node.updated(node = NodeInfo)` - published when a node in the DB changes (appears, location changed, username changed, etc...)
- `meshtastic.log.line(line)` - a raw unparsed log line from the radio
- `meshtastic.clientNotification(notification, interface) - a ClientNotification sent from the radio
We receive position, user, or data packets from the mesh. You probably only care about `meshtastic.receive.data`. The first argument for
that publish will be the packet. Text or binary data packets (from `sendData` or `sendText`) will both arrive this way. If you print packet
@@ -128,6 +129,7 @@ NODELESS_WANT_CONFIG_ID = 69420
publishingThread = DeferredExecution("publishing")
logger = logging.getLogger(__name__)
class ResponseHandler(NamedTuple):
"""A pending response callback, waiting for a response to one of our messages"""
@@ -159,31 +161,53 @@ def _onTextReceive(iface, asDict):
#
# Usually btw this problem is caused by apps sending binary data but setting the payload type to
# text.
logging.debug(f"in _onTextReceive() asDict:{asDict}")
logger.debug(f"in _onTextReceive() asDict:{asDict}")
try:
asBytes = asDict["decoded"]["payload"]
asDict["decoded"]["text"] = asBytes.decode("utf-8")
except Exception as ex:
logging.error(f"Malformatted utf8 in text message: {ex}")
logger.error(f"Malformatted utf8 in text message: {ex}")
_receiveInfoUpdate(iface, asDict)
def _onPositionReceive(iface, asDict):
"""Special auto parsing for received messages"""
logging.debug(f"in _onPositionReceive() asDict:{asDict}")
logger.debug(f"in _onPositionReceive() asDict:{asDict}")
if "decoded" in asDict:
if "position" in asDict["decoded"] and "from" in asDict:
p = asDict["decoded"]["position"]
logging.debug(f"p:{p}")
logger.debug(f"p:{p}")
p = iface._fixupPosition(p)
logging.debug(f"after fixup p:{p}")
# update node DB as needed
iface._getOrCreateByNum(asDict["from"])["position"] = p
logger.debug(f"after fixup p:{p}")
# For the local node, only accept position updates with equal
# or better precision. The local GPS is authoritative, and
# low-precision echoes from the mesh (e.g., map reports relayed
# by other nodes) should not overwrite it.
# For remote nodes, always accept the latest position since
# any update from them reflects their current state.
node = iface._getOrCreateByNum(asDict["from"])
is_local_node = (
iface.myInfo is not None
and asDict["from"] == iface.myInfo.my_node_num
)
if is_local_node:
existing = node.get("position", {})
existing_precision = existing.get("precisionBits", 0) or 0
new_precision = p.get("precisionBits", 0) or 0
if existing_precision == 0 or new_precision >= existing_precision:
node["position"] = p
else:
logger.debug(
f"Ignoring low-precision position echo for local node "
f"({new_precision} < {existing_precision})"
)
else:
node["position"] = p
def _onNodeInfoReceive(iface, asDict):
"""Special auto parsing for received messages"""
logging.debug(f"in _onNodeInfoReceive() asDict:{asDict}")
logger.debug(f"in _onNodeInfoReceive() asDict:{asDict}")
if "decoded" in asDict:
if "user" in asDict["decoded"] and "from" in asDict:
p = asDict["decoded"]["user"]
@@ -197,7 +221,7 @@ def _onNodeInfoReceive(iface, asDict):
def _onTelemetryReceive(iface, asDict):
"""Automatically update device metrics on received packets"""
logging.debug(f"in _onTelemetryReceive() asDict:{asDict}")
logger.debug(f"in _onTelemetryReceive() asDict:{asDict}")
if "from" not in asDict:
return
@@ -221,7 +245,7 @@ def _onTelemetryReceive(iface, asDict):
updateObj = telemetry.get(toUpdate)
newMetrics = node.get(toUpdate, {})
newMetrics.update(updateObj)
logging.debug(f"updating {toUpdate} metrics for {asDict['from']} to {newMetrics}")
logger.debug(f"updating {toUpdate} metrics for {asDict['from']} to {newMetrics}")
node[toUpdate] = newMetrics
def _receiveInfoUpdate(iface, asDict):
@@ -233,7 +257,7 @@ def _receiveInfoUpdate(iface, asDict):
def _onAdminReceive(iface, asDict):
"""Special auto parsing for received messages"""
logging.debug(f"in _onAdminReceive() asDict:{asDict}")
logger.debug(f"in _onAdminReceive() asDict:{asDict}")
if "decoded" in asDict and "from" in asDict and "admin" in asDict["decoded"]:
adminMessage = asDict["decoded"]["admin"]["raw"]
iface._getOrCreateByNum(asDict["from"])["adminSessionPassKey"] = adminMessage.session_passkey
+792 -161
View File
File diff suppressed because it is too large. Load diff
+4 -3
View File
@@ -2,6 +2,7 @@
import argparse
import logging
import os
from typing import cast, List
import dash_bootstrap_components as dbc # type: ignore[import-untyped]
@@ -143,8 +144,8 @@ def create_dash(slog_path: str) -> Dash:
"""
app = Dash(external_stylesheets=[dbc.themes.BOOTSTRAP])
dpwr = read_pandas(f"{slog_path}/power.feather")
dslog = read_pandas(f"{slog_path}/slog.feather")
dpwr = read_pandas(os.path.join(slog_path, "power.feather"))
dslog = read_pandas(os.path.join(slog_path, "slog.feather"))
pmon_raises = get_pmon_raises(dslog)
@@ -190,7 +191,7 @@ def main():
parser = create_argparser()
args = parser.parse_args()
if not args.slog:
args.slog = f"{root_dir()}/latest"
args.slog = os.path.join(root_dir(), "latest")
app = create_dash(slog_path=args.slog)
port = 8051
+72 -25
View File
@@ -4,9 +4,10 @@ import asyncio
import atexit
import logging
import struct
import sys
import time
import io
from threading import Thread
from threading import Thread, Event
from typing import List, Optional
import google.protobuf
@@ -23,6 +24,7 @@ FROMRADIO_UUID = "2c55e69e-4993-11ed-b878-0242ac120002"
FROMNUM_UUID = "ed9da18c-a800-4f66-a670-aa7547e34453"
LEGACY_LOGRADIO_UUID = "6c6fd238-78fa-436b-aacf-15c5be1ef2e2"
LOGRADIO_UUID = "5a3d6e49-06e6-4423-9944-e9de8cdf9547"
logger = logging.getLogger(__name__)
class BLEInterface(MeshInterface):
@@ -31,32 +33,43 @@ class BLEInterface(MeshInterface):
class BLEError(Exception):
"""An exception class for BLE errors."""
def __init__(
DEVICE_NOT_FOUND = "device_not_found"
MULTIPLE_DEVICES = "multiple_devices"
READ_ERROR = "read_error"
WRITE_ERROR = "write_error"
UNKNOWN = "unknown"
def __init__(self, message: str, kind: str = UNKNOWN):
super().__init__(message)
self.kind = kind
def __init__( # pylint: disable=R0917
self,
address: Optional[str],
noProto: bool = False,
debugOut: Optional[io.TextIOWrapper]=None,
noNodes: bool = False,
timeout: int = 300,
) -> None:
MeshInterface.__init__(
self, debugOut=debugOut, noProto=noProto, noNodes=noNodes
self, debugOut=debugOut, noProto=noProto, noNodes=noNodes, timeout=timeout
)
self.should_read = False
logging.debug("Threads starting")
logger.debug("Threads starting")
self._want_receive = True
self._receiveThread: Optional[Thread] = Thread(
target=self._receiveFromRadioImpl, name="BLEReceive", daemon=True
)
self._receiveThread.start()
logging.debug("Threads running")
logger.debug("Threads running")
self.client: Optional[BLEClient] = None
try:
logging.debug(f"BLE connecting to: {address if address else 'any'}")
logger.debug(f"BLE connecting to: {address if address else 'any'}")
self.client = self.connect(address)
logging.debug("BLE connected")
logger.debug("BLE connected")
except BLEInterface.BLEError as e:
self.close()
raise e
@@ -69,13 +82,13 @@ class BLEInterface(MeshInterface):
if self.client.has_characteristic(LOGRADIO_UUID):
self.client.start_notify(LOGRADIO_UUID, self.log_radio_handler)
logging.debug("Mesh configure starting")
logger.debug("Mesh configure starting")
self._startConfig()
if not self.noProto:
self._waitConnected(timeout=60.0)
self.waitForConfig()
logging.debug("Register FROMNUM notify callback")
logger.debug("Register FROMNUM notify callback")
self.client.start_notify(FROMNUM_UUID, self.from_num_handler)
# We MUST run atexit (if we can) because otherwise (at least on linux) the BLE device is not disconnected
@@ -83,12 +96,23 @@ class BLEInterface(MeshInterface):
# Note: the on disconnected callback will call our self.close which will make us nicely wait for threads to exit
self._exit_handler = atexit.register(self.client.disconnect)
def __repr__(self):
rep = f"BLEInterface(address={self.client.address if self.client else None!r}"
if self.debugOut is not None:
rep += f", debugOut={self.debugOut!r}"
if self.noProto:
rep += ", noProto=True"
if self.noNodes:
rep += ", noNodes=True"
rep += ")"
return rep
def from_num_handler(self, _, b: bytes) -> None: # pylint: disable=C0116
"""Handle callbacks for fromnum notify.
Note: this method does not need to be async because it is just setting a bool.
"""
from_num = struct.unpack("<I", bytes(b))[0]
logging.debug(f"FROMNUM notify: {from_num}")
logger.debug(f"FROMNUM notify: {from_num}")
self.should_read = True
async def log_radio_handler(self, _, b): # pylint: disable=C0116
@@ -103,7 +127,7 @@ class BLEInterface(MeshInterface):
)
self._handleLogLine(message)
except google.protobuf.message.DecodeError:
logging.warning("Malformed LogRecord received. Skipping.")
logger.warning("Malformed LogRecord received. Skipping.")
async def legacy_log_radio_handler(self, _, b): # pylint: disable=C0116
log_radio = b.decode("utf-8").replace("\n", "")
@@ -113,7 +137,7 @@ class BLEInterface(MeshInterface):
def scan() -> List[BLEDevice]:
"""Scan for available BLE devices."""
with BLEClient() as client:
logging.info("Scanning for BLE devices (takes 10 seconds)...")
logger.info("Scanning for BLE devices (takes 10 seconds)...")
response = client.discover(
timeout=10, return_adv=True, service_uuids=[SERVICE_UUID]
)
@@ -143,11 +167,13 @@ class BLEInterface(MeshInterface):
if len(addressed_devices) == 0:
raise BLEInterface.BLEError(
f"No Meshtastic BLE peripheral with identifier or address '{address}' found. Try --ble-scan to find it."
f"No Meshtastic BLE peripheral with identifier or address '{address}' found. Try --ble-scan to find it.",
BLEInterface.BLEError.DEVICE_NOT_FOUND,
)
if len(addressed_devices) > 1:
raise BLEInterface.BLEError(
f"More than one Meshtastic BLE peripheral with identifier or address '{address}' found."
f"More than one Meshtastic BLE peripheral with identifier or address '{address}' found.",
BLEInterface.BLEError.MULTIPLE_DEVICES,
)
return addressed_devices[0]
@@ -163,7 +189,7 @@ class BLEInterface(MeshInterface):
# Bleak docs recommend always doing a scan before connecting (even if we know addr)
device = self.find_device(address)
client = BLEClient(device.address, disconnected_callback=lambda _: self.close)
client = BLEClient(device.address, disconnected_callback=lambda _: self.close())
client.connect()
client.discover()
return client
@@ -175,29 +201,32 @@ class BLEInterface(MeshInterface):
retries: int = 0
while self._want_receive:
if self.client is None:
logging.debug(f"BLE client is None, shutting down")
logger.debug(f"BLE client is None, shutting down")
self._want_receive = False
continue
try:
b = bytes(self.client.read_gatt_char(FROMRADIO_UUID))
except BleakDBusError as e:
# Device disconnected probably, so end our read loop immediately
logging.debug(f"Device disconnected, shutting down {e}")
logger.debug(f"Device disconnected, shutting down {e}")
self._want_receive = False
except BleakError as e:
# We were definitely disconnected
if "Not connected" in str(e):
logging.debug(f"Device disconnected, shutting down {e}")
logger.debug(f"Device disconnected, shutting down {e}")
self._want_receive = False
else:
raise BLEInterface.BLEError("Error reading BLE") from e
raise BLEInterface.BLEError(
"Error reading BLE",
BLEInterface.BLEError.READ_ERROR,
) from e
if not b:
if retries < 5:
time.sleep(0.1)
retries += 1
continue
break
logging.debug(f"FROMRADIO read: {b.hex()}")
logger.debug(f"FROMRADIO read: {b.hex()}")
self._handleFromRadio(b)
else:
time.sleep(0.01)
@@ -205,7 +234,7 @@ class BLEInterface(MeshInterface):
def _sendToRadioImpl(self, toRadio) -> None:
b: bytes = toRadio.SerializeToString()
if b and self.client: # we silently ignore writes while we are shutting down
logging.debug(f"TORADIO write: {b.hex()}")
logger.debug(f"TORADIO write: {b.hex()}")
try:
self.client.write_gatt_char(
TORADIO_UUID, b, response=True
@@ -213,7 +242,8 @@ class BLEInterface(MeshInterface):
# search Bleak src for org.bluez.Error.InProgress
except Exception as e:
raise BLEInterface.BLEError(
"Error writing BLE (are you in the 'bluetooth' user group? did you enter the pairing PIN on your computer?)"
"Error writing BLE (are you in the 'bluetooth' user group? did you enter the pairing PIN on your computer?)",
BLEInterface.BLEError.WRITE_ERROR,
) from e
# Allow to propagate and then make sure we read
time.sleep(0.01)
@@ -223,7 +253,7 @@ class BLEInterface(MeshInterface):
try:
MeshInterface.close(self)
except Exception as e:
logging.error(f"Error closing mesh interface: {e}")
logger.error(f"Error closing mesh interface: {e}")
if self._want_receive:
self._want_receive = False # Tell the thread we want it to stop
@@ -245,14 +275,16 @@ class BLEClient:
"""Client for managing connection to a BLE device"""
def __init__(self, address=None, **kwargs) -> None:
self._loop_ready = Event()
self._eventLoop = asyncio.new_event_loop()
self._eventThread = Thread(
target=self._run_event_loop, name="BLEClient", daemon=True
)
self._eventThread.start()
self._loop_ready.wait() # Wait for event loop to be running
if not address:
logging.debug("No address provided - only discover method will work.")
logger.debug("No address provided - only discover method will work.")
return
self.bleak_client = BleakClient(address, **kwargs)
@@ -293,13 +325,28 @@ class BLEClient:
self.close()
def async_await(self, coro, timeout=None): # pylint: disable=C0116
return self.async_run(coro).result(timeout)
"""Wait for async operation to complete.
On macOS, CoreBluetooth requires occasional I/O operations for
callbacks to be properly delivered. The debug logging provides this
I/O when enabled, allowing the system to process pending callbacks.
"""
logger.debug(f"async_await: waiting for {coro}")
future = self.async_run(coro)
# On macOS without debug logging, callbacks may not be delivered
# unless we trigger some I/O. This is a known quirk of CoreBluetooth.
sys.stdout.flush()
result = future.result(timeout)
logger.debug("async_await: complete")
return result
def async_run(self, coro): # pylint: disable=C0116
return asyncio.run_coroutine_threadsafe(coro, self._eventLoop)
def _run_event_loop(self):
try:
# Signal ready from WITHIN the loop to guarantee it's actually running
self._eventLoop.call_soon(self._loop_ready.set)
self._eventLoop.run_forever()
finally:
self._eventLoop.close()
+253 -124
View File
@@ -1,6 +1,6 @@
"""Mesh Interface class
"""
# pylint: disable=R0917
# pylint: disable=R0917,C0302
import collections
import json
@@ -17,6 +17,7 @@ from decimal import Decimal
from typing import Any, Callable, Dict, List, Optional, Union
import google.protobuf.json_format
try:
import print_color # type: ignore[import-untyped]
except ImportError as e:
@@ -46,6 +47,7 @@ from meshtastic.util import (
stripnl,
)
logger = logging.getLogger(__name__)
def _timeago(delta_secs: int) -> str:
"""Convert a number of seconds in the past into a short, friendly string
@@ -88,7 +90,7 @@ class MeshInterface: # pylint: disable=R0902
super().__init__(self.message)
def __init__(
self, debugOut=None, noProto: bool = False, noNodes: bool = False
self, debugOut=None, noProto: bool = False, noNodes: bool = False, timeout: int = 300
) -> None:
"""Constructor
@@ -97,13 +99,14 @@ class MeshInterface: # pylint: disable=R0902
link - just be a dumb serial client.
noNodes -- If True, instruct the node to not send its nodedb
on startup, just other configuration information.
timeout -- How long to wait for replies (default: 300 seconds)
"""
self.debugOut = debugOut
self.nodes: Optional[Dict[str, Dict]] = None # FIXME
self.isConnected: threading.Event = threading.Event()
self.noProto: bool = noProto
self.localNode: meshtastic.node.Node = meshtastic.node.Node(
self, -1
self, -1, timeout=timeout
) # We fixup nodenum later
self.myInfo: Optional[
mesh_pb2.MyNodeInfo
@@ -117,7 +120,7 @@ class MeshInterface: # pylint: disable=R0902
self.failure = (
None # If we've encountered a fatal exception it will be kept here
)
self._timeout: Timeout = Timeout()
self._timeout: Timeout = Timeout(maxSecs=timeout)
self._acknowledgment: Acknowledgment = Acknowledgment()
self.heartbeatTimer: Optional[threading.Timer] = None
random.seed() # FIXME, we should not clobber the random seedval here, instead tell user they must call it
@@ -149,11 +152,11 @@ class MeshInterface: # pylint: disable=R0902
def __exit__(self, exc_type, exc_value, trace):
if exc_type is not None and exc_value is not None:
logging.error(
logger.error(
f"An exception of type {exc_type} with value {exc_value} has occurred"
)
if trace is not None:
logging.error(f"Traceback: {trace}")
logger.error(f"Traceback:\n{''.join(traceback.format_tb(trace))}")
self.close()
@staticmethod
@@ -222,9 +225,43 @@ class MeshInterface: # pylint: disable=R0902
return infos
def showNodes(
self, includeSelf: bool = True
self, includeSelf: bool = True, showFields: Optional[List[str]] = None
) -> str: # pylint: disable=W0613
"""Show table summary of nodes in mesh"""
"""Show table summary of nodes in mesh
Args:
includeSelf (bool): Include ourself in the output?
showFields (List[str]): List of fields to show in output
"""
def get_human_readable(name):
name_map = {
"user.longName": "User",
"user.id": "ID",
"user.shortName": "AKA",
"user.hwModel": "Hardware",
"user.publicKey": "Pubkey",
"user.role": "Role",
"position.latitude": "Latitude",
"position.longitude": "Longitude",
"position.altitude": "Altitude",
"deviceMetrics.batteryLevel": "Battery",
"deviceMetrics.channelUtilization": "Channel util.",
"deviceMetrics.airUtilTx": "Tx air util.",
"snr": "SNR",
"hopsAway": "Hops",
"channel": "Channel",
"lastHeard": "LastHeard",
"since": "Since",
"isFavorite": "Fav",
}
if name in name_map:
return name_map.get(name) # Default to a formatted guess
else:
return name
def formatFloat(value, precision=2, unit="") -> Optional[str]:
"""Format a float value with precision."""
@@ -246,73 +283,93 @@ class MeshInterface: # pylint: disable=R0902
return None # not handling a timestamp from the future
return _timeago(delta_secs)
def getNestedValue(node_dict: Dict[str, Any], key_path: str) -> Any:
if key_path.index(".") < 0:
logger.debug("getNestedValue was called without a nested path.")
return None
keys = key_path.split(".")
value: Optional[Union[str, dict]] = node_dict
for key in keys:
if isinstance(value, dict):
value = value.get(key)
else:
return None
return value
if showFields is None or len(showFields) == 0:
# The default set of fields to show (e.g., the status quo)
showFields = ["N", "user.longName", "user.id", "user.shortName", "user.hwModel", "user.publicKey",
"user.role", "position.latitude", "position.longitude", "position.altitude",
"deviceMetrics.batteryLevel", "deviceMetrics.channelUtilization",
"deviceMetrics.airUtilTx", "snr", "hopsAway", "channel", "isFavorite", "lastHeard", "since"]
else:
# Always at least include the row number.
showFields.insert(0, "N")
rows: List[Dict[str, Any]] = []
if self.nodesByNum:
logging.debug(f"self.nodes:{self.nodes}")
logger.debug(f"self.nodes:{self.nodes}")
for node in self.nodesByNum.values():
if not includeSelf and node["num"] == self.localNode.nodeNum:
continue
presumptive_id = f"!{node['num']:08x}"
row = {
"N": 0,
"User": f"Meshtastic {presumptive_id[-4:]}",
"ID": presumptive_id,
}
user = node.get("user")
if user:
row.update(
{
"User": user.get("longName", "N/A"),
"AKA": user.get("shortName", "N/A"),
"ID": user["id"],
"Hardware": user.get("hwModel", "UNSET"),
"Pubkey": user.get("publicKey", "UNSET"),
}
)
pos = node.get("position")
if pos:
row.update(
{
"Latitude": formatFloat(pos.get("latitude"), 4, "°"),
"Longitude": formatFloat(pos.get("longitude"), 4, "°"),
"Altitude": formatFloat(pos.get("altitude"), 0, " m"),
}
)
metrics = node.get("deviceMetrics")
if metrics:
batteryLevel = metrics.get("batteryLevel")
if batteryLevel is not None:
if batteryLevel == 0:
batteryString = "Powered"
# This allows the user to specify fields that wouldn't otherwise be included.
fields = {}
for field in showFields:
if "." in field:
raw_value = getNestedValue(node, field)
else:
# The "since" column is synthesized, it's not retrieved from the device. Get the
# lastHeard value here, and then we'll format it properly below.
if field == "since":
raw_value = node.get("lastHeard")
else:
batteryString = str(batteryLevel) + "%"
row.update({"Battery": batteryString})
row.update(
{
"Channel util.": formatFloat(
metrics.get("channelUtilization"), 2, "%"
),
"Tx air util.": formatFloat(
metrics.get("airUtilTx"), 2, "%"
),
}
)
raw_value = node.get(field)
row.update(
{
"SNR": formatFloat(node.get("snr"), 2, " dB"),
"Hops": node.get("hopsAway", "?"),
"Channel": node.get("channel", 0),
"LastHeard": getLH(node.get("lastHeard")),
"Since": getTimeAgo(node.get("lastHeard")),
}
)
formatted_value: Optional[str] = ""
rows.append(row)
# Some of these need special formatting or processing.
if field == "channel":
if raw_value is None:
formatted_value = "0"
elif field == "deviceMetrics.channelUtilization":
formatted_value = formatFloat(raw_value, 2, "%")
elif field == "deviceMetrics.airUtilTx":
formatted_value = formatFloat(raw_value, 2, "%")
elif field == "deviceMetrics.batteryLevel":
if raw_value in (0, 101):
formatted_value = "Powered"
else:
formatted_value = formatFloat(raw_value, 0, "%")
elif field == "isFavorite":
formatted_value = "*" if raw_value else ""
elif field == "lastHeard":
formatted_value = getLH(raw_value)
elif field == "position.latitude":
formatted_value = formatFloat(raw_value, 4, "°")
elif field == "position.longitude":
formatted_value = formatFloat(raw_value, 4, "°")
elif field == "position.altitude":
formatted_value = formatFloat(raw_value, 0, "m")
elif field == "since":
formatted_value = getTimeAgo(raw_value) or "N/A"
elif field == "snr":
formatted_value = formatFloat(raw_value, 0, " dB")
elif field == "user.shortName":
formatted_value = raw_value if raw_value is not None else f'Meshtastic {presumptive_id[-4:]}'
elif field == "user.id":
formatted_value = raw_value if raw_value is not None else presumptive_id
else:
formatted_value = raw_value # No special formatting
fields[field] = formatted_value
# Filter out any field in the data set that was not specified.
filteredData = {get_human_readable(k): v for k, v in fields.items() if k in showFields}
filteredData.update({get_human_readable(k): v for k, v in fields.items()})
rows.append(filteredData)
rows.sort(key=lambda r: r.get("LastHeard") or "0000", reverse=True)
for i, row in enumerate(rows):
@@ -332,7 +389,7 @@ class MeshInterface: # pylint: disable=R0902
n = meshtastic.node.Node(self, nodeId, timeout=timeout)
# Only request device settings and channel info when necessary
if requestChannels:
logging.debug("About to requestChannels")
logger.debug("About to requestChannels")
n.requestChannels()
retries_left = requestChannelAttempts
last_index: int = 0
@@ -344,7 +401,7 @@ class MeshInterface: # pylint: disable=R0902
if new_index != last_index:
retries_left = requestChannelAttempts - 1
if retries_left <= 0:
our_exit(f"Error: Timed out waiting for channels, giving up")
our_exit("Error: Timed out waiting for channels, giving up")
print("Timed out trying to retrieve channel info, retrying")
n.requestChannels(startingIndex=new_index)
last_index = new_index
@@ -360,6 +417,9 @@ class MeshInterface: # pylint: disable=R0902
wantResponse: bool = False,
onResponse: Optional[Callable[[dict], Any]] = None,
channelIndex: int = 0,
portNum: portnums_pb2.PortNum.ValueType = portnums_pb2.PortNum.TEXT_MESSAGE_APP,
replyId: Optional[int]=None,
hopLimit: Optional[int]=None,
):
"""Send a utf8 string to some other node, if the node has a display it
will also be shown on the device.
@@ -370,12 +430,14 @@ class MeshInterface: # pylint: disable=R0902
Keyword Arguments:
destinationId {nodeId or nodeNum} -- where to send this
message (default: {BROADCAST_ADDR})
portNum -- the application portnum (similar to IP port numbers)
of the destination, see portnums.proto for a list
wantAck -- True if you want the message sent in a reliable manner
(with retries and ack/nak provided for delivery)
wantResponse -- True if you want the service on the other side to
send an application layer response
portNum -- the application portnum (similar to IP port numbers)
of the destination, see portnums.proto for a list
replyId -- the ID of the message that this packet is a response to
hopLimit {int} -- hop limit to use
Returns the sent packet. The id field will be populated in this packet
and can be used to track future message acks/naks.
@@ -384,11 +446,13 @@ class MeshInterface: # pylint: disable=R0902
return self.sendData(
text.encode("utf-8"),
destinationId,
portNum=portnums_pb2.PortNum.TEXT_MESSAGE_APP,
portNum=portNum,
wantAck=wantAck,
wantResponse=wantResponse,
onResponse=onResponse,
channelIndex=channelIndex,
replyId=replyId,
hopLimit=hopLimit,
)
@@ -398,8 +462,9 @@ class MeshInterface: # pylint: disable=R0902
destinationId: Union[int, str] = BROADCAST_ADDR,
onResponse: Optional[Callable[[dict], Any]] = None,
channelIndex: int = 0,
hopLimit: Optional[int]=None,
):
"""Send an alert text to some other node. This is similar to a text message,
"""Send an alert text to some other node. This is similar to a text message,
but carries a higher priority and is capable of generating special notifications
on certain clients.
@@ -409,6 +474,7 @@ class MeshInterface: # pylint: disable=R0902
Keyword Arguments:
destinationId {nodeId or nodeNum} -- where to send this
message (default: {BROADCAST_ADDR})
hopLimit {int} -- hop limit to use
Returns the sent packet. The id field will be populated in this packet
and can be used to track future message acks/naks.
@@ -422,9 +488,22 @@ class MeshInterface: # pylint: disable=R0902
wantResponse=False,
onResponse=onResponse,
channelIndex=channelIndex,
priority=mesh_pb2.MeshPacket.Priority.ALERT
priority=mesh_pb2.MeshPacket.Priority.ALERT,
hopLimit=hopLimit,
)
def sendMqttClientProxyMessage(self, topic: str, data: bytes):
"""Send an MQTT Client Proxy message to the radio.
Topic and data should be the MQTT topic and the message
payload from an MQTT broker, respectively."""
prox = mesh_pb2.MqttClientProxyMessage()
prox.topic = topic
prox.data = data
toRadio = mesh_pb2.ToRadio()
toRadio.mqttClientProxyMessage.CopyFrom(prox)
self._sendToRadio(toRadio)
def sendData(
self,
data,
@@ -439,6 +518,7 @@ class MeshInterface: # pylint: disable=R0902
pkiEncrypted: Optional[bool]=False,
publicKey: Optional[bytes]=None,
priority: mesh_pb2.MeshPacket.Priority.ValueType=mesh_pb2.MeshPacket.Priority.RELIABLE,
replyId: Optional[int]=None,
): # pylint: disable=R0913
"""Send a data packet to some other node
@@ -463,17 +543,18 @@ class MeshInterface: # pylint: disable=R0902
will implicitly be true.
channelIndex -- channel number to use
hopLimit -- hop limit to use
replyId -- the ID of the message that this packet is a response to
Returns the sent packet. The id field will be populated in this packet
and can be used to track future message acks/naks.
"""
if getattr(data, "SerializeToString", None):
logging.debug(f"Serializing protobuf as data: {stripnl(data)}")
logger.debug(f"Serializing protobuf as data: {stripnl(data)}")
data = data.SerializeToString()
logging.debug(f"len(data): {len(data)}")
logging.debug(
logger.debug(f"len(data): {len(data)}")
logger.debug(
f"mesh_pb2.Constants.DATA_PAYLOAD_LEN: {mesh_pb2.Constants.DATA_PAYLOAD_LEN}"
)
if len(data) > mesh_pb2.Constants.DATA_PAYLOAD_LEN:
@@ -490,11 +571,13 @@ class MeshInterface: # pylint: disable=R0902
meshPacket.decoded.portnum = portNum
meshPacket.decoded.want_response = wantResponse
meshPacket.id = self._generatePacketId()
if replyId is not None:
meshPacket.decoded.reply_id = replyId
if priority is not None:
meshPacket.priority = priority
if onResponse is not None:
logging.debug(f"Setting a response handler for requestId {meshPacket.id}")
logger.debug(f"Setting a response handler for requestId {meshPacket.id}")
self._addResponseHandler(meshPacket.id, onResponse, ackPermitted=onResponseAckPermitted)
p = self._sendPacket(meshPacket, destinationId, wantAck=wantAck, hopLimit=hopLimit, pkiEncrypted=pkiEncrypted, publicKey=publicKey)
return p
@@ -508,6 +591,7 @@ class MeshInterface: # pylint: disable=R0902
wantAck: bool = False,
wantResponse: bool = False,
channelIndex: int = 0,
hopLimit: Optional[int]=None,
):
"""
Send a position packet to some other node (normally a broadcast)
@@ -521,15 +605,15 @@ class MeshInterface: # pylint: disable=R0902
p = mesh_pb2.Position()
if latitude != 0.0:
p.latitude_i = int(latitude / 1e-7)
logging.debug(f"p.latitude_i:{p.latitude_i}")
logger.debug(f"p.latitude_i:{p.latitude_i}")
if longitude != 0.0:
p.longitude_i = int(longitude / 1e-7)
logging.debug(f"p.longitude_i:{p.longitude_i}")
logger.debug(f"p.longitude_i:{p.longitude_i}")
if altitude != 0:
p.altitude = int(altitude)
logging.debug(f"p.altitude:{p.altitude}")
logger.debug(f"p.altitude:{p.altitude}")
if wantResponse:
onResponse = self.onResponsePosition
@@ -544,6 +628,7 @@ class MeshInterface: # pylint: disable=R0902
wantResponse=wantResponse,
onResponse=onResponse,
channelIndex=channelIndex,
hopLimit=hopLimit,
)
if wantResponse:
self.waitForPosition()
@@ -596,11 +681,19 @@ class MeshInterface: # pylint: disable=R0902
hopLimit=hopLimit,
)
# extend timeout based on number of nodes, limit by configured hopLimit
waitFactor = min(len(self.nodes) - 1 if self.nodes else 0, hopLimit)
nodes_based_factor = (len(self.nodes) - 1) if self.nodes else (hopLimit + 1)
waitFactor = max(1, min(nodes_based_factor, hopLimit + 1))
self.waitForTraceRoute(waitFactor)
def onResponseTraceRoute(self, p: dict):
"""on response for trace route"""
if p["decoded"]["portnum"] == "ROUTING_APP":
error = p["decoded"]["routing"]["errorReason"]
if error != "NONE":
print(f"Traceroute failed: {error}")
self._acknowledgment.receivedTraceRoute = True
return
UNK_SNR = -128 # Value representing unknown SNR
routeDiscovery = mesh_pb2.RouteDiscovery()
@@ -649,7 +742,8 @@ class MeshInterface: # pylint: disable=R0902
destinationId: Union[int, str] = BROADCAST_ADDR,
wantResponse: bool = False,
channelIndex: int = 0,
telemetryType: str = "device_metrics"
telemetryType: str = "device_metrics",
hopLimit: Optional[int]=None,
):
"""Send telemetry and optionally ask for a response"""
r = telemetry_pb2.Telemetry()
@@ -696,6 +790,7 @@ class MeshInterface: # pylint: disable=R0902
wantResponse=wantResponse,
onResponse=onResponse,
channelIndex=channelIndex,
hopLimit=hopLimit,
)
if wantResponse:
self.waitForTelemetry()
@@ -756,6 +851,7 @@ class MeshInterface: # pylint: disable=R0902
self,
name,
description,
icon,
expire: int,
waypoint_id: Optional[int] = None,
latitude: float = 0.0,
@@ -764,6 +860,7 @@ class MeshInterface: # pylint: disable=R0902
wantAck: bool = True,
wantResponse: bool = False,
channelIndex: int = 0,
hopLimit: Optional[int]=None,
): # pylint: disable=R0913
"""
Send a waypoint packet to some other node (normally a broadcast)
@@ -774,21 +871,22 @@ class MeshInterface: # pylint: disable=R0902
w = mesh_pb2.Waypoint()
w.name = name
w.description = description
w.icon = icon
w.expire = expire
if waypoint_id is None:
# Generate a waypoint's id, NOT a packet ID.
# same algorithm as https://github.com/meshtastic/js/blob/715e35d2374276a43ffa93c628e3710875d43907/src/meshDevice.ts#L791
seed = secrets.randbits(32)
w.id = math.floor(seed * math.pow(2, -32) * 1e9)
logging.debug(f"w.id:{w.id}")
logger.debug(f"w.id:{w.id}")
else:
w.id = waypoint_id
if latitude != 0.0:
w.latitude_i = int(latitude * 1e7)
logging.debug(f"w.latitude_i:{w.latitude_i}")
logger.debug(f"w.latitude_i:{w.latitude_i}")
if longitude != 0.0:
w.longitude_i = int(longitude * 1e7)
logging.debug(f"w.longitude_i:{w.longitude_i}")
logger.debug(f"w.longitude_i:{w.longitude_i}")
if wantResponse:
onResponse = self.onResponseWaypoint
@@ -803,6 +901,7 @@ class MeshInterface: # pylint: disable=R0902
wantResponse=wantResponse,
onResponse=onResponse,
channelIndex=channelIndex,
hopLimit=hopLimit,
)
if wantResponse:
self.waitForWaypoint()
@@ -815,12 +914,13 @@ class MeshInterface: # pylint: disable=R0902
wantAck: bool = True,
wantResponse: bool = False,
channelIndex: int = 0,
hopLimit: Optional[int]=None,
):
"""
Send a waypoint deletion packet to some other node (normally a broadcast)
NB: The id must be the waypoint's id and not the id of the packet creation.
Returns the sent packet. The id field will be populated in this packet and
can be used to track future message acks/naks.
"""
@@ -841,6 +941,7 @@ class MeshInterface: # pylint: disable=R0902
wantResponse=wantResponse,
onResponse=onResponse,
channelIndex=channelIndex,
hopLimit=hopLimit,
)
if wantResponse:
self.waitForWaypoint()
@@ -891,8 +992,10 @@ class MeshInterface: # pylint: disable=R0902
else:
our_exit("Warning: No myInfo found.")
# A simple hex style nodeid - we can parse this without needing the DB
elif destinationId.startswith("!"):
nodeNum = int(destinationId[1:], 16)
elif isinstance(destinationId, str) and len(destinationId) >= 8:
# assuming some form of node id string such as !1234578 or 0x12345678
# always grab the last 8 items of the hexadecimal id str and parse to integer
nodeNum = int(destinationId[-8:], 16)
else:
if self.nodes:
node = self.nodes.get(destinationId)
@@ -901,7 +1004,7 @@ class MeshInterface: # pylint: disable=R0902
else:
nodeNum = node["num"]
else:
logging.warning("Warning: There were no self.nodes.")
logger.warning("Warning: There were no self.nodes.")
meshPacket.to = nodeNum
meshPacket.want_ack = wantAck
@@ -925,11 +1028,11 @@ class MeshInterface: # pylint: disable=R0902
toRadio.packet.CopyFrom(meshPacket)
if self.noProto:
logging.warning(
f"Not sending packet because protocol use is disabled by noProto"
logger.warning(
"Not sending packet because protocol use is disabled by noProto"
)
else:
logging.debug(f"Sending packet: {stripnl(meshPacket)}")
logger.debug(f"Sending packet: {stripnl(meshPacket)}")
self._sendToRadio(toRadio)
return meshPacket
@@ -980,7 +1083,7 @@ class MeshInterface: # pylint: disable=R0902
"""Get info about my node."""
if self.myInfo is None or self.nodesByNum is None:
return None
logging.debug(f"self.nodesByNum:{self.nodesByNum}")
logger.debug(f"self.nodesByNum:{self.nodesByNum}")
return self.nodesByNum.get(self.myInfo.my_node_num)
def getMyUser(self):
@@ -1011,6 +1114,20 @@ class MeshInterface: # pylint: disable=R0902
return user.get("publicKey", None)
return None
def getCannedMessage(self):
"""Get canned message"""
node = self.localNode
if node is not None:
return node.get_canned_message()
return None
def getRingtone(self):
"""Get ringtone"""
node = self.localNode
if node is not None:
return node.get_ringtone()
return None
def _waitConnected(self, timeout=30.0):
"""Block until the initial node db download is complete, or timeout
and raise an exception"""
@@ -1056,7 +1173,7 @@ class MeshInterface: # pylint: disable=R0902
def callback():
self.heartbeatTimer = None
interval = 300
logging.debug(f"Sending heartbeat, interval {interval} seconds")
logger.debug(f"Sending heartbeat, interval {interval} seconds")
self.heartbeatTimer = threading.Timer(interval, callback)
self.heartbeatTimer.start()
self.sendHeartbeat()
@@ -1114,11 +1231,11 @@ class MeshInterface: # pylint: disable=R0902
def _sendToRadio(self, toRadio: mesh_pb2.ToRadio) -> None:
"""Send a ToRadio protobuf to the device"""
if self.noProto:
logging.warning(
f"Not sending packet because protocol use is disabled by noProto"
logger.warning(
"Not sending packet because protocol use is disabled by noProto"
)
else:
# logging.debug(f"Sending toRadio: {stripnl(toRadio)}")
# logger.debug(f"Sending toRadio: {stripnl(toRadio)}")
if not toRadio.HasField("packet"):
# not a meshpacket -- send immediately, give queue a chance,
@@ -1131,38 +1248,38 @@ class MeshInterface: # pylint: disable=R0902
resentQueue = collections.OrderedDict()
while self.queue:
# logging.warn("queue: " + " ".join(f'{k:08x}' for k in self.queue))
# logger.warn("queue: " + " ".join(f'{k:08x}' for k in self.queue))
while not self._queueHasFreeSpace():
logging.debug("Waiting for free space in TX Queue")
logger.debug("Waiting for free space in TX Queue")
time.sleep(0.5)
try:
toResend = self.queue.popitem(last=False)
except KeyError:
break
packetId, packet = toResend
# logging.warn(f"packet: {packetId:08x} {packet}")
# logger.warn(f"packet: {packetId:08x} {packet}")
resentQueue[packetId] = packet
if packet is False:
continue
self._queueClaim()
if packet != toRadio:
logging.debug(f"Resending packet ID {packetId:08x} {packet}")
logger.debug(f"Resending packet ID {packetId:08x} {packet}")
self._sendToRadioImpl(packet)
# logging.warn("resentQueue: " + " ".join(f'{k:08x}' for k in resentQueue))
# logger.warn("resentQueue: " + " ".join(f'{k:08x}' for k in resentQueue))
for packetId, packet in resentQueue.items():
if (
self.queue.pop(packetId, False) is False
): # Packet got acked under us
logging.debug(f"packet {packetId:08x} got acked under us")
logger.debug(f"packet {packetId:08x} got acked under us")
continue
if packet:
self.queue[packetId] = packet
# logging.warn("queue + resentQueue: " + " ".join(f'{k:08x}' for k in self.queue))
# logger.warn("queue + resentQueue: " + " ".join(f'{k:08x}' for k in self.queue))
def _sendToRadioImpl(self, toRadio: mesh_pb2.ToRadio) -> None:
"""Send a ToRadio protobuf to the device"""
logging.error(f"Subclass must provide toradio: {toRadio}")
logger.error(f"Subclass must provide toradio: {toRadio}")
def _handleConfigComplete(self) -> None:
"""
@@ -1178,22 +1295,22 @@ class MeshInterface: # pylint: disable=R0902
def _handleQueueStatusFromRadio(self, queueStatus) -> None:
self.queueStatus = queueStatus
logging.debug(
logger.debug(
f"TX QUEUE free {queueStatus.free} of {queueStatus.maxlen}, res = {queueStatus.res}, id = {queueStatus.mesh_packet_id:08x} "
)
if queueStatus.res:
return
# logging.warn("queue: " + " ".join(f'{k:08x}' for k in self.queue))
# logger.warn("queue: " + " ".join(f'{k:08x}' for k in self.queue))
justQueued = self.queue.pop(queueStatus.mesh_packet_id, None)
if justQueued is None and queueStatus.mesh_packet_id != 0:
self.queue[queueStatus.mesh_packet_id] = False
logging.debug(
logger.debug(
f"Reply for unexpected packet ID {queueStatus.mesh_packet_id:08x}"
)
# logging.warn("queue: " + " ".join(f'{k:08x}' for k in self.queue))
# logger.warn("queue: " + " ".join(f'{k:08x}' for k in self.queue))
def _handleFromRadio(self, fromRadioBytes):
"""
@@ -1201,30 +1318,30 @@ class MeshInterface: # pylint: disable=R0902
Called by subclasses."""
fromRadio = mesh_pb2.FromRadio()
logging.debug(
logger.debug(
f"in mesh_interface.py _handleFromRadio() fromRadioBytes: {fromRadioBytes}"
)
try:
fromRadio.ParseFromString(fromRadioBytes)
except Exception as ex:
logging.error(
logger.error(
f"Error while parsing FromRadio bytes:{fromRadioBytes} {ex}"
)
traceback.print_exc()
raise ex
asDict = google.protobuf.json_format.MessageToDict(fromRadio)
logging.debug(f"Received from radio: {fromRadio}")
logger.debug(f"Received from radio: {fromRadio}")
if fromRadio.HasField("my_info"):
self.myInfo = fromRadio.my_info
self.localNode.nodeNum = self.myInfo.my_node_num
logging.debug(f"Received myinfo: {stripnl(fromRadio.my_info)}")
logger.debug(f"Received myinfo: {stripnl(fromRadio.my_info)}")
elif fromRadio.HasField("metadata"):
self.metadata = fromRadio.metadata
logging.debug(f"Received device metadata: {stripnl(fromRadio.metadata)}")
logger.debug(f"Received device metadata: {stripnl(fromRadio.metadata)}")
elif fromRadio.HasField("node_info"):
logging.debug(f"Received nodeinfo: {asDict['nodeInfo']}")
logger.debug(f"Received nodeinfo: {asDict['nodeInfo']}")
node = self._getOrCreateByNum(asDict["nodeInfo"]["num"])
node.update(asDict["nodeInfo"])
@@ -1232,7 +1349,7 @@ class MeshInterface: # pylint: disable=R0902
newpos = self._fixupPosition(node["position"])
node["position"] = newpos
except:
logging.debug("Node without position")
logger.debug("Node without position")
# no longer necessary since we're mutating directly in nodesByNum via _getOrCreateByNum
# self.nodesByNum[node["num"]] = node
@@ -1247,7 +1364,7 @@ class MeshInterface: # pylint: disable=R0902
elif fromRadio.config_complete_id == self.configId:
# we ignore the config_complete_id, it is unneeded for our
# stream API fromRadio.config_complete_id
logging.debug(f"Config complete ID {self.configId}")
logger.debug(f"Config complete ID {self.configId}")
self._handleConfigComplete()
elif fromRadio.HasField("channel"):
self._handleChannel(fromRadio.channel)
@@ -1257,6 +1374,14 @@ class MeshInterface: # pylint: disable=R0902
self._handleLogRecord(fromRadio.log_record)
elif fromRadio.HasField("queueStatus"):
self._handleQueueStatusFromRadio(fromRadio.queueStatus)
elif fromRadio.HasField("clientNotification"):
publishingThread.queueWork(
lambda: pub.sendMessage(
"meshtastic.clientNotification",
notification=fromRadio.clientNotification,
interface=self,
)
)
elif fromRadio.HasField("mqttClientProxyMessage"):
publishingThread.queueWork(
@@ -1352,9 +1477,13 @@ class MeshInterface: # pylint: disable=R0902
self.localNode.moduleConfig.paxcounter.CopyFrom(
fromRadio.moduleConfig.paxcounter
)
elif fromRadio.moduleConfig.HasField("traffic_management"):
self.localNode.moduleConfig.traffic_management.CopyFrom(
fromRadio.moduleConfig.traffic_management
)
else:
logging.debug("Unexpected FromRadio payload")
logger.debug("Unexpected FromRadio payload")
def _fixupPosition(self, position: Dict) -> Dict:
"""Convert integer lat/lon into floats
@@ -1388,7 +1517,7 @@ class MeshInterface: # pylint: disable=R0902
try:
return self.nodesByNum[num]["user"]["id"] # type: ignore[index]
except:
logging.debug(f"Node {num} not found for fromId")
logger.debug(f"Node {num} not found for fromId")
return None
def _getOrCreateByNum(self, nodeNum):
@@ -1444,7 +1573,7 @@ class MeshInterface: # pylint: disable=R0902
# from might be missing if the nodenum was zero.
if not hack and "from" not in asDict:
asDict["from"] = 0
logging.error(
logger.error(
f"Device returned a packet we sent, ignoring: {stripnl(asDict)}"
)
print(
@@ -1458,11 +1587,11 @@ class MeshInterface: # pylint: disable=R0902
try:
asDict["fromId"] = self._nodeNumToId(asDict["from"], False)
except Exception as ex:
logging.warning(f"Not populating fromId {ex}")
logger.warning(f"Not populating fromId {ex}")
try:
asDict["toId"] = self._nodeNumToId(asDict["to"])
except Exception as ex:
logging.warning(f"Not populating toId {ex}")
logger.warning(f"Not populating toId {ex}")
# We could provide our objects as DotMaps - which work with . notation or as dictionaries
# asObj = DotMap(asDict)
@@ -1482,7 +1611,7 @@ class MeshInterface: # pylint: disable=R0902
# it to prevent confusion
if "portnum" not in decoded:
decoded["portnum"] = portnum
logging.warning(f"portnum was not in decoded. Setting to:{portnum}")
logger.warning(f"portnum was not in decoded. Setting to:{portnum}")
else:
portnum = decoded["portnum"]
@@ -1514,7 +1643,7 @@ class MeshInterface: # pylint: disable=R0902
# Is this message in response to a request, if so, look for a handler
requestId = decoded.get("requestId")
if requestId is not None:
logging.debug(f"Got a response for requestId {requestId}")
logger.debug(f"Got a response for requestId {requestId}")
# We ignore ACK packets unless the callback is named `onAckNak`
# or the handler is set as ackPermitted, but send NAKs and
# other, data-containing responses to the handlers
@@ -1531,12 +1660,12 @@ class MeshInterface: # pylint: disable=R0902
or handler.ackPermitted
):
handler = self.responseHandlers.pop(requestId, None)
logging.debug(
logger.debug(
f"Calling response handler for requestId {requestId}"
)
handler.callback(asDict)
logging.debug(f"Publishing {topic}: packet={stripnl(asDict)} ")
logger.debug(f"Publishing {topic}: packet={stripnl(asDict)} ")
publishingThread.queueWork(
lambda: pub.sendMessage(topic, packet=asDict, interface=self)
)
+282 -112
View File
@@ -7,7 +7,7 @@ import time
from typing import Optional, Union, List
from meshtastic.protobuf import admin_pb2, apponly_pb2, channel_pb2, localonly_pb2, mesh_pb2, portnums_pb2
from meshtastic.protobuf import admin_pb2, apponly_pb2, channel_pb2, config_pb2, localonly_pb2, mesh_pb2, portnums_pb2
from meshtastic.util import (
Timeout,
camel_to_snake,
@@ -16,8 +16,12 @@ from meshtastic.util import (
pskToString,
stripnl,
message_to_json,
generate_channel_hash,
to_node_num,
flags_to_list,
)
logger = logging.getLogger(__name__)
class Node:
"""A model of a (local or remote) node in the mesh
@@ -42,11 +46,40 @@ class Node:
self.gotResponse = None
def __repr__(self):
r = f"Node({self.iface!r}, 0x{self.nodeNum:08x}"
if self.noProto:
r += ", noProto=True"
if self._timeout.expireTimeout != 300:
r += ", timeout={self._timeout.expireTimeout!r}"
r += ")"
return r
@staticmethod
def position_flags_list(position_flags: int) -> List[str]:
"Return a list of position flags from the given flags integer"
return flags_to_list(config_pb2.Config.PositionConfig.PositionFlags, position_flags)
@staticmethod
def excluded_modules_list(excluded_modules: int) -> List[str]:
"Return a list of excluded modules from the given flags integer"
return flags_to_list(mesh_pb2.ExcludedModules, excluded_modules)
def module_available(self, excluded_bit: int) -> bool:
"""Check DeviceMetadata.excluded_modules to see if a module is available."""
meta = getattr(self.iface, "metadata", None)
if meta is None:
return True
try:
return (meta.excluded_modules & excluded_bit) == 0
except Exception:
return True
def showChannels(self):
"""Show human readable description of our channels."""
print("Channels:")
if self.channels:
logging.debug(f"self.channels:{self.channels}")
logger.debug(f"self.channels:{self.channels}")
for c in self.channels:
cStr = message_to_json(c.settings)
# don't show disabled channels
@@ -79,7 +112,7 @@ class Node:
def requestChannels(self, startingIndex: int = 0):
"""Send regular MeshPackets to ask channels."""
logging.debug(f"requestChannels for nodeNum:{self.nodeNum}")
logger.debug(f"requestChannels for nodeNum:{self.nodeNum}")
# only initialize if we're starting out fresh
if startingIndex == 0:
self.channels = None
@@ -88,7 +121,7 @@ class Node:
def onResponseRequestSettings(self, p):
"""Handle the response packets for requesting settings _requestSettings()"""
logging.debug(f"onResponseRequestSetting() p:{p}")
logger.debug(f"onResponseRequestSetting() p:{p}")
config_values = None
if "routing" in p["decoded"]:
if p["decoded"]["routing"]["errorReason"] != "NONE":
@@ -137,11 +170,10 @@ class Node:
p.get_config_request = configType
else:
msgIndex = configType.index
if configType.containing_type.name == "LocalConfig":
p.get_config_request = msgIndex
p.get_config_request = admin_pb2.AdminMessage.ConfigType.Value(configType.name.upper() + "_CONFIG")
else:
p.get_module_config_request = msgIndex
p.get_module_config_request = configType.index
self._sendAdmin(p, wantResponse=True, onResponse=onResponse)
if onResponse:
@@ -212,10 +244,12 @@ class Node:
p.set_module_config.ambient_lighting.CopyFrom(self.moduleConfig.ambient_lighting)
elif config_name == "paxcounter":
p.set_module_config.paxcounter.CopyFrom(self.moduleConfig.paxcounter)
elif config_name == "traffic_management":
p.set_module_config.traffic_management.CopyFrom(self.moduleConfig.traffic_management)
else:
our_exit(f"Error: No valid config with name {config_name}")
logging.debug(f"Wrote: {config_name}")
logger.debug(f"Wrote: {config_name}")
if self == self.iface.localNode:
onResponse = None
else:
@@ -228,7 +262,7 @@ class Node:
p = admin_pb2.AdminMessage()
p.set_channel.CopyFrom(self.channels[channelIndex])
self._sendAdmin(p, adminIndex=adminIndex)
logging.debug(f"Wrote channel {channelIndex}")
logger.debug(f"Wrote channel {channelIndex}")
def getChannelByChannelIndex(self, channelIndex):
"""Get channel by channelIndex
@@ -253,12 +287,22 @@ class Node:
# for sending admin channels will also change
adminIndex = self.iface.localNode._getAdminChannelIndex()
# Snapshot serialized channel payloads from channelIndex onward so we
# can avoid writing slots whose protobuf content did not change after
# the shift. Use bytes (not message objects), because _fixupChannels()
# mutates message fields in-place.
old_channels = [
self.channels[i].SerializeToString()
for i in range(channelIndex, len(self.channels))
]
self.channels.pop(channelIndex)
self._fixupChannels() # expand back to 8 channels
index = channelIndex
while index < 8:
self.writeChannel(index, adminIndex=adminIndex)
for old_ch in old_channels:
if self.channels[index].SerializeToString() != old_ch:
self.writeChannel(index, adminIndex=adminIndex)
index += 1
# if we are updating the local node, we might end up
@@ -289,28 +333,37 @@ class Node:
return c.index
return 0
def setOwner(self, long_name: Optional[str]=None, short_name: Optional[str]=None, is_licensed: bool=False):
def setOwner(self, long_name: Optional[str]=None, short_name: Optional[str]=None, is_licensed: bool=False, is_unmessagable: Optional[bool]=None):
"""Set device owner name"""
logging.debug(f"in setOwner nodeNum:{self.nodeNum}")
logger.debug(f"in setOwner nodeNum:{self.nodeNum}")
self.ensureSessionKey()
p = admin_pb2.AdminMessage()
nChars = 4
if long_name is not None:
long_name = long_name.strip()
# Validate that long_name is not empty or whitespace-only
if not long_name:
our_exit("ERROR: Long Name cannot be empty or contain only whitespace characters")
p.set_owner.long_name = long_name
p.set_owner.is_licensed = is_licensed
if short_name is not None:
short_name = short_name.strip()
# Validate that short_name is not empty or whitespace-only
if not short_name:
our_exit("ERROR: Short Name cannot be empty or contain only whitespace characters")
if len(short_name) > nChars:
short_name = short_name[:nChars]
print(f"Maximum is 4 characters, truncated to {short_name}")
p.set_owner.short_name = short_name
if is_unmessagable is not None:
p.set_owner.is_unmessagable = is_unmessagable
# Note: These debug lines are used in unit tests
logging.debug(f"p.set_owner.long_name:{p.set_owner.long_name}:")
logging.debug(f"p.set_owner.short_name:{p.set_owner.short_name}:")
logging.debug(f"p.set_owner.is_licensed:{p.set_owner.is_licensed}")
logger.debug(f"p.set_owner.long_name:{p.set_owner.long_name}:")
logger.debug(f"p.set_owner.short_name:{p.set_owner.short_name}:")
logger.debug(f"p.set_owner.is_licensed:{p.set_owner.is_licensed}")
logger.debug(f"p.set_owner.is_unmessagable:{p.set_owner.is_unmessagable}:")
# If sending to a remote node, wait for ACK/NAK
if self == self.iface.localNode:
onResponse = None
@@ -337,14 +390,59 @@ class Node:
s = s.replace("=", "").replace("+", "-").replace("/", "_")
return f"https://meshtastic.org/e/#{s}"
def setURL(self, url):
def getContactURL(self, node_id: Union[int, str], should_ignore: bool = False, manually_verified: bool = False):
"""Generate a shareable contact URL for the specified node"""
nodeNum = to_node_num(node_id)
node = self.iface.nodesByNum.get(nodeNum)
if not node or not node.get("user"):
our_exit(f"Warning: Node {node_id} not found in NodeDB")
contact = admin_pb2.SharedContact()
contact.node_num = nodeNum
u = node["user"]
if u.get("id"):
contact.user.id = u["id"]
if u.get("macaddr"):
contact.user.macaddr = base64.b64decode(u["macaddr"])
if u.get("longName"):
contact.user.long_name = u["longName"]
if u.get("shortName"):
contact.user.short_name = u["shortName"]
if u.get("hwModel") and u["hwModel"] != "UNSET":
contact.user.hw_model = mesh_pb2.HardwareModel.Value(u["hwModel"])
if u.get("role"):
contact.user.role = config_pb2.Config.DeviceConfig.Role.Value(u["role"])
if u.get("publicKey"):
contact.user.public_key = base64.b64decode(u["publicKey"])
if u.get("isLicensed"):
contact.user.is_licensed = u["isLicensed"]
if u.get("isUnmessagable") is not None:
contact.user.is_unmessagable = u["isUnmessagable"]
if should_ignore:
contact.should_ignore = True
if manually_verified:
contact.manually_verified = True
data = contact.SerializeToString()
s = base64.urlsafe_b64encode(data).decode("ascii")
s = s.replace("=", "").replace("+", "-").replace("/", "_")
return f"https://meshtastic.org/v/#{s}"
def setURL(self, url: str, addOnly: bool = False):
"""Set mesh network URL"""
if self.localConfig is None:
our_exit("Warning: No Config has been read")
if self.localConfig is None or self.channels is None:
our_exit("Warning: config or channels not loaded")
# URLs are of the form https://meshtastic.org/d/#{base64_channel_set}
# Split on '/#' to find the base64 encoded channel settings
splitURL = url.split("/#")
if addOnly:
splitURL = url.split("/?add=true#")
else:
splitURL = url.split("/#")
if len(splitURL) == 1:
our_exit(f"Warning: Invalid URL '{url}'")
b64 = splitURL[-1]
# We normally strip padding to make for a shorter URL, but the python parser doesn't like
@@ -361,29 +459,71 @@ class Node:
if len(channelSet.settings) == 0:
our_exit("Warning: There were no settings.")
i = 0
for chs in channelSet.settings:
ch = channel_pb2.Channel()
ch.role = (
channel_pb2.Channel.Role.PRIMARY
if i == 0
else channel_pb2.Channel.Role.SECONDARY
)
ch.index = i
ch.settings.CopyFrom(chs)
self.channels[ch.index] = ch
logging.debug(f"Channel i:{i} ch:{ch}")
self.writeChannel(ch.index)
i = i + 1
if addOnly:
# Add new channels with names not already present
# Don't change existing channels
for chs in channelSet.settings:
channelExists = self.getChannelByName(chs.name)
if channelExists or chs.name == "":
print(f"Ignoring existing or empty channel \"{chs.name}\" from add URL")
continue
ch = self.getDisabledChannel()
if not ch:
our_exit("Warning: No free channels were found")
ch.settings.CopyFrom(chs)
ch.role = channel_pb2.Channel.Role.SECONDARY
print(f"Adding new channel '{chs.name}' to device")
self.writeChannel(ch.index)
else:
i = 0
for chs in channelSet.settings:
ch = channel_pb2.Channel()
ch.role = (
channel_pb2.Channel.Role.PRIMARY
if i == 0
else channel_pb2.Channel.Role.SECONDARY
)
ch.index = i
ch.settings.CopyFrom(chs)
self.channels[ch.index] = ch
logger.debug(f"Channel i:{i} ch:{ch}")
self.writeChannel(ch.index)
i = i + 1
p = admin_pb2.AdminMessage()
p.set_config.lora.CopyFrom(channelSet.lora_config)
self.ensureSessionKey()
self._sendAdmin(p)
def addContactURL(self, url: str):
"""Add a contact (User) to the NodeDB from a shareable URL"""
self.ensureSessionKey()
splitURL = url.split("/#")
if len(splitURL) == 1:
our_exit(f"Warning: Invalid URL '{url}'")
b64 = splitURL[-1]
missing_padding = len(b64) % 4
if missing_padding:
b64 += "=" * (4 - missing_padding)
decodedURL = base64.urlsafe_b64decode(b64)
contact = admin_pb2.SharedContact()
contact.ParseFromString(decodedURL)
p = admin_pb2.AdminMessage()
p.add_contact.CopyFrom(contact)
if self == self.iface.localNode:
onResponse = None
else:
onResponse = self.onAckNak
return self._sendAdmin(p, onResponse=onResponse)
def onResponseRequestRingtone(self, p):
"""Handle the response packet for requesting ringtone part 1"""
logging.debug(f"onResponseRequestRingtone() p:{p}")
logger.debug(f"onResponseRequestRingtone() p:{p}")
errorFound = False
if "routing" in p["decoded"]:
if p["decoded"]["routing"]["errorReason"] != "NONE":
@@ -396,12 +536,16 @@ class Node:
self.ringtonePart = p["decoded"]["admin"][
"raw"
].get_ringtone_response
logging.debug(f"self.ringtonePart:{self.ringtonePart}")
logger.debug(f"self.ringtonePart:{self.ringtonePart}")
self.gotResponse = True
def get_ringtone(self):
"""Get the ringtone. Concatenate all pieces together and return a single string."""
logging.debug(f"in get_ringtone()")
logger.debug(f"in get_ringtone()")
if not self.module_available(mesh_pb2.EXTNOTIF_CONFIG):
logging.warning("External Notification module not present (excluded by firmware)")
return None
if not self.ringtone:
p1 = admin_pb2.AdminMessage()
p1.get_ringtone_request = True
@@ -412,18 +556,20 @@ class Node:
while self.gotResponse is False:
time.sleep(0.1)
logging.debug(f"self.ringtone:{self.ringtone}")
logger.debug(f"self.ringtone:{self.ringtone}")
self.ringtone = ""
if self.ringtonePart:
self.ringtone += self.ringtonePart
print(f"ringtone:{self.ringtone}")
logging.debug(f"ringtone:{self.ringtone}")
logger.debug(f"ringtone:{self.ringtone}")
return self.ringtone
def set_ringtone(self, ringtone):
"""Set the ringtone. The ringtone length must be less than 230 character."""
if not self.module_available(mesh_pb2.EXTNOTIF_CONFIG):
logging.warning("External Notification module not present (excluded by firmware)")
return None
if len(ringtone) > 230:
our_exit("Warning: The ringtone must be less than 230 characters.")
@@ -443,7 +589,7 @@ class Node:
if i == 0:
p.set_ringtone_message = chunk
logging.debug(f"Setting ringtone '{chunk}' part {i+1}")
logger.debug(f"Setting ringtone '{chunk}' part {i+1}")
# If sending to a remote node, wait for ACK/NAK
if self == self.iface.localNode:
onResponse = None
@@ -453,7 +599,7 @@ class Node:
def onResponseRequestCannedMessagePluginMessageMessages(self, p):
"""Handle the response packet for requesting canned message plugin message part 1"""
logging.debug(f"onResponseRequestCannedMessagePluginMessageMessages() p:{p}")
logger.debug(f"onResponseRequestCannedMessagePluginMessageMessages() p:{p}")
errorFound = False
if "routing" in p["decoded"]:
if p["decoded"]["routing"]["errorReason"] != "NONE":
@@ -466,14 +612,17 @@ class Node:
self.cannedPluginMessageMessages = p["decoded"]["admin"][
"raw"
].get_canned_message_module_messages_response
logging.debug(
logger.debug(
f"self.cannedPluginMessageMessages:{self.cannedPluginMessageMessages}"
)
self.gotResponse = True
def get_canned_message(self):
"""Get the canned message string. Concatenate all pieces together and return a single string."""
logging.debug(f"in get_canned_message()")
logger.debug(f"in get_canned_message()")
if not self.module_available(mesh_pb2.CANNEDMSG_CONFIG):
logging.warning("Canned Message module not present (excluded by firmware)")
return None
if not self.cannedPluginMessage:
p1 = admin_pb2.AdminMessage()
p1.get_canned_message_module_messages_request = True
@@ -486,7 +635,7 @@ class Node:
while self.gotResponse is False:
time.sleep(0.1)
logging.debug(
logger.debug(
f"self.cannedPluginMessageMessages:{self.cannedPluginMessageMessages}"
)
@@ -494,12 +643,14 @@ class Node:
if self.cannedPluginMessageMessages:
self.cannedPluginMessage += self.cannedPluginMessageMessages
print(f"canned_plugin_message:{self.cannedPluginMessage}")
logging.debug(f"canned_plugin_message:{self.cannedPluginMessage}")
logger.debug(f"canned_plugin_message:{self.cannedPluginMessage}")
return self.cannedPluginMessage
def set_canned_message(self, message):
"""Set the canned message. The canned messages length must be less than 200 character."""
if not self.module_available(mesh_pb2.CANNEDMSG_CONFIG):
logging.warning("Canned Message module not present (excluded by firmware)")
return None
if len(message) > 200:
our_exit("Warning: The canned message must be less than 200 characters.")
@@ -519,7 +670,7 @@ class Node:
if i == 0:
p.set_canned_message_module_messages = chunk
logging.debug(f"Setting canned message '{chunk}' part {i+1}")
logger.debug(f"Setting canned message '{chunk}' part {i+1}")
# If sending to a remote node, wait for ACK/NAK
if self == self.iface.localNode:
onResponse = None
@@ -533,7 +684,7 @@ class Node:
self.ensureSessionKey()
p = admin_pb2.AdminMessage()
p.exit_simulator = True
logging.debug("in exitSimulator()")
logger.debug("in exitSimulator()")
return self._sendAdmin(p)
@@ -542,7 +693,7 @@ class Node:
self.ensureSessionKey()
p = admin_pb2.AdminMessage()
p.reboot_seconds = secs
logging.info(f"Telling node to reboot in {secs} seconds")
logger.info(f"Telling node to reboot in {secs} seconds")
# If sending to a remote node, wait for ACK/NAK
if self == self.iface.localNode:
@@ -556,7 +707,7 @@ class Node:
self.ensureSessionKey()
p = admin_pb2.AdminMessage()
p.begin_edit_settings = True
logging.info(f"Telling open a transaction to edit settings")
logger.info(f"Telling open a transaction to edit settings")
# If sending to a remote node, wait for ACK/NAK
if self == self.iface.localNode:
@@ -570,7 +721,7 @@ class Node:
self.ensureSessionKey()
p = admin_pb2.AdminMessage()
p.commit_edit_settings = True
logging.info(f"Telling node to commit open transaction for editing settings")
logger.info(f"Telling node to commit open transaction for editing settings")
# If sending to a remote node, wait for ACK/NAK
if self == self.iface.localNode:
@@ -580,11 +731,11 @@ class Node:
return self._sendAdmin(p, onResponse=onResponse)
def rebootOTA(self, secs: int = 10):
"""Tell the node to reboot into factory firmware."""
"""Tell the node to reboot into factory firmware (firmware < 2.7.18)."""
self.ensureSessionKey()
p = admin_pb2.AdminMessage()
p.reboot_ota_seconds = secs
logging.info(f"Telling node to reboot to OTA in {secs} seconds")
logger.info(f"Telling node to reboot to OTA in {secs} seconds")
# If sending to a remote node, wait for ACK/NAK
if self == self.iface.localNode:
@@ -593,12 +744,28 @@ class Node:
onResponse = self.onAckNak
return self._sendAdmin(p, onResponse=onResponse)
def startOTA(
self,
ota_mode: admin_pb2.OTAMode.ValueType,
ota_file_hash: bytes,
):
"""Tell the node to start OTA mode (firmware >= 2.7.18)."""
if self != self.iface.localNode:
raise ValueError("startOTA only possible in local node")
self.ensureSessionKey()
p = admin_pb2.AdminMessage()
p.ota_request.reboot_ota_mode = ota_mode
p.ota_request.ota_hash = ota_file_hash
return self._sendAdmin(p)
def enterDFUMode(self):
"""Tell the node to enter DFU mode (NRF52)."""
self.ensureSessionKey()
p = admin_pb2.AdminMessage()
p.enter_dfu_mode_request = True
logging.info(f"Telling node to enable DFU mode")
logger.info(f"Telling node to enable DFU mode")
# If sending to a remote node, wait for ACK/NAK
if self == self.iface.localNode:
@@ -612,7 +779,7 @@ class Node:
self.ensureSessionKey()
p = admin_pb2.AdminMessage()
p.shutdown_seconds = secs
logging.info(f"Telling node to shutdown in {secs} seconds")
logger.info(f"Telling node to shutdown in {secs} seconds")
# If sending to a remote node, wait for ACK/NAK
if self == self.iface.localNode:
@@ -625,7 +792,7 @@ class Node:
"""Get the node's metadata."""
p = admin_pb2.AdminMessage()
p.get_device_metadata_request = True
logging.info(f"Requesting device metadata")
logger.info(f"Requesting device metadata")
self._sendAdmin(
p, wantResponse=True, onResponse=self.onRequestGetMetadata
@@ -637,11 +804,11 @@ class Node:
self.ensureSessionKey()
p = admin_pb2.AdminMessage()
if full:
p.factory_reset_device = True
logging.info(f"Telling node to factory reset (full device reset)")
p.factory_reset_device = 1
logger.info(f"Telling node to factory reset (full device reset)")
else:
p.factory_reset_config = True
logging.info(f"Telling node to factory reset (config reset)")
p.factory_reset_config = 1
logger.info(f"Telling node to factory reset (config reset)")
# If sending to a remote node, wait for ACK/NAK
if self == self.iface.localNode:
@@ -653,11 +820,7 @@ class Node:
def removeNode(self, nodeId: Union[int, str]):
"""Tell the node to remove a specific node by ID"""
self.ensureSessionKey()
if isinstance(nodeId, str):
if nodeId.startswith("!"):
nodeId = int(nodeId[1:], 16)
else:
nodeId = int(nodeId)
nodeId = to_node_num(nodeId)
p = admin_pb2.AdminMessage()
p.remove_by_nodenum = nodeId
@@ -671,11 +834,7 @@ class Node:
def setFavorite(self, nodeId: Union[int, str]):
"""Tell the node to set the specified node ID to be favorited on the NodeDB on the device"""
self.ensureSessionKey()
if isinstance(nodeId, str):
if nodeId.startswith("!"):
nodeId = int(nodeId[1:], 16)
else:
nodeId = int(nodeId)
nodeId = to_node_num(nodeId)
p = admin_pb2.AdminMessage()
p.set_favorite_node = nodeId
@@ -689,11 +848,7 @@ class Node:
def removeFavorite(self, nodeId: Union[int, str]):
"""Tell the node to set the specified node ID to be un-favorited on the NodeDB on the device"""
self.ensureSessionKey()
if isinstance(nodeId, str):
if nodeId.startswith("!"):
nodeId = int(nodeId[1:], 16)
else:
nodeId = int(nodeId)
nodeId = to_node_num(nodeId)
p = admin_pb2.AdminMessage()
p.remove_favorite_node = nodeId
@@ -707,11 +862,7 @@ class Node:
def setIgnored(self, nodeId: Union[int, str]):
"""Tell the node to set the specified node ID to be ignored on the NodeDB on the device"""
self.ensureSessionKey()
if isinstance(nodeId, str):
if nodeId.startswith("!"):
nodeId = int(nodeId[1:], 16)
else:
nodeId = int(nodeId)
nodeId = to_node_num(nodeId)
p = admin_pb2.AdminMessage()
p.set_ignored_node = nodeId
@@ -725,11 +876,7 @@ class Node:
def removeIgnored(self, nodeId: Union[int, str]):
"""Tell the node to set the specified node ID to be un-ignored on the NodeDB on the device"""
self.ensureSessionKey()
if isinstance(nodeId, str):
if nodeId.startswith("!"):
nodeId = int(nodeId[1:], 16)
else:
nodeId = int(nodeId)
nodeId = to_node_num(nodeId)
p = admin_pb2.AdminMessage()
p.remove_ignored_node = nodeId
@@ -745,7 +892,7 @@ class Node:
self.ensureSessionKey()
p = admin_pb2.AdminMessage()
p.nodedb_reset = True
logging.info(f"Telling node to reset the NodeDB")
logger.info(f"Telling node to reset the NodeDB")
# If sending to a remote node, wait for ACK/NAK
if self == self.iface.localNode:
@@ -786,7 +933,7 @@ class Node:
self.ensureSessionKey()
p = admin_pb2.AdminMessage()
p.remove_fixed_position = True
logging.info(f"Telling node to remove fixed position")
logger.info(f"Telling node to remove fixed position")
if self == self.iface.localNode:
onResponse = None
@@ -801,7 +948,7 @@ class Node:
timeSec = int(time.time())
p = admin_pb2.AdminMessage()
p.set_time_only = timeSec
logging.info(f"Setting node time to {timeSec}")
logger.info(f"Setting node time to {timeSec}")
if self == self.iface.localNode:
onResponse = None
@@ -833,7 +980,7 @@ class Node:
def onRequestGetMetadata(self, p):
"""Handle the response packet for requesting device metadata getMetadata()"""
logging.debug(f"onRequestGetMetadata() p:{p}")
logger.debug(f"onRequestGetMetadata() p:{p}")
if "routing" in p["decoded"]:
if p["decoded"]["routing"]["errorReason"] != "NONE":
@@ -845,30 +992,42 @@ class Node:
portnums_pb2.PortNum.ROUTING_APP
):
if p["decoded"]["routing"]["errorReason"] != "NONE":
logging.warning(
logger.warning(
f'Metadata request failed, error reason: {p["decoded"]["routing"]["errorReason"]}'
)
self._timeout.expireTime = time.time() # Do not wait any longer
return # Don't try to parse this routing message
logging.debug(f"Retrying metadata request.")
logger.debug(f"Retrying metadata request.")
self.getMetadata()
return
c = p["decoded"]["admin"]["raw"].get_device_metadata_response
self._timeout.reset() # We made forward progress
logging.debug(f"Received metadata {stripnl(c)}")
logger.debug(f"Received metadata {stripnl(c)}")
print(f"\nfirmware_version: {c.firmware_version}")
print(f"device_state_version: {c.device_state_version}")
if c.role in config_pb2.Config.DeviceConfig.Role.values():
print(f"role: {config_pb2.Config.DeviceConfig.Role.Name(c.role)}")
else:
print(f"role: {c.role}")
print(f"position_flags: {self.position_flags_list(c.position_flags)}")
if c.hw_model in mesh_pb2.HardwareModel.values():
print(f"hw_model: {mesh_pb2.HardwareModel.Name(c.hw_model)}")
else:
print(f"hw_model: {c.hw_model}")
print(f"hasPKC: {c.hasPKC}")
if c.excluded_modules > 0:
print(f"excluded_modules: {self.excluded_modules_list(c.excluded_modules)}")
def onResponseRequestChannel(self, p):
"""Handle the response packet for requesting a channel _requestChannel()"""
logging.debug(f"onResponseRequestChannel() p:{p}")
logger.debug(f"onResponseRequestChannel() p:{p}")
if p["decoded"]["portnum"] == portnums_pb2.PortNum.Name(
portnums_pb2.PortNum.ROUTING_APP
):
if p["decoded"]["routing"]["errorReason"] != "NONE":
logging.warning(
logger.warning(
f'Channel request failed, error reason: {p["decoded"]["routing"]["errorReason"]}'
)
self._timeout.expireTime = time.time() # Do not wait any longer
@@ -876,18 +1035,18 @@ class Node:
lastTried = 0
if len(self.partialChannels) > 0:
lastTried = self.partialChannels[-1].index
logging.debug(f"Retrying previous channel request.")
logger.debug(f"Retrying previous channel request.")
self._requestChannel(lastTried)
return
c = p["decoded"]["admin"]["raw"].get_channel_response
self.partialChannels.append(c)
self._timeout.reset() # We made forward progress
logging.debug(f"Received channel {stripnl(c)}")
logger.debug(f"Received channel {stripnl(c)}")
index = c.index
if index >= 8 - 1:
logging.debug("Finished downloading channels")
logger.debug("Finished downloading channels")
self.channels = self.partialChannels
self._fixupChannels()
@@ -922,11 +1081,11 @@ class Node:
print(
f"Requesting channel {channelNum} info from remote node (this could take a while)"
)
logging.debug(
logger.debug(
f"Requesting channel {channelNum} info from remote node (this could take a while)"
)
else:
logging.debug(f"Requesting channel {channelNum}")
logger.debug(f"Requesting channel {channelNum}")
return self._sendAdmin(
p, wantResponse=True, onResponse=self.onResponseRequestChannel
@@ -943,7 +1102,7 @@ class Node:
"""Send an admin message to the specified node (or the local node if destNodeNum is zero)"""
if self.noProto:
logging.warning(
logger.warning(
f"Not sending packet because protocol use is disabled by noProto"
)
else:
@@ -951,18 +1110,15 @@ class Node:
adminIndex == 0
): # unless a special channel index was used, we want to use the admin index
adminIndex = self.iface.localNode._getAdminChannelIndex()
logging.debug(f"adminIndex:{adminIndex}")
if isinstance(self.nodeNum, int):
nodeid = self.nodeNum
else: # assume string starting with !
nodeid = int(self.nodeNum[1:],16)
logger.debug(f"adminIndex:{adminIndex}")
nodeid = to_node_num(self.nodeNum)
if "adminSessionPassKey" in self.iface._getOrCreateByNum(nodeid):
p.session_passkey = self.iface._getOrCreateByNum(nodeid).get("adminSessionPassKey")
return self.iface.sendData(
p,
self.nodeNum,
portNum=portnums_pb2.PortNum.ADMIN_APP,
wantAck=False,
wantAck=True,
wantResponse=wantResponse,
onResponse=onResponse,
channelIndex=adminIndex,
@@ -972,13 +1128,27 @@ class Node:
def ensureSessionKey(self):
"""If our entry in iface.nodesByNum doesn't already have an adminSessionPassKey, make a request to get one"""
if self.noProto:
logging.warning(
logger.warning(
f"Not ensuring session key, because protocol use is disabled by noProto"
)
else:
if isinstance(self.nodeNum, int):
nodeid = self.nodeNum
else: # assume string starting with !
nodeid = int(self.nodeNum[1:],16)
nodeid = to_node_num(self.nodeNum)
if self.iface._getOrCreateByNum(nodeid).get("adminSessionPassKey") is None:
self.requestConfig(admin_pb2.AdminMessage.SESSIONKEY_CONFIG)
def get_channels_with_hash(self):
"""Return a list of dicts with channel info and hash."""
result = []
if self.channels:
for c in self.channels:
if c.settings and hasattr(c.settings, "name") and hasattr(c.settings, "psk"):
hash_val = generate_channel_hash(c.settings.name, c.settings.psk)
else:
hash_val = None
result.append({
"index": c.index,
"role": channel_pb2.Channel.Role.Name(c.role),
"name": c.settings.name if c.settings and hasattr(c.settings, "name") else "",
"hash": hash_val,
})
return result
+128
View File
@@ -0,0 +1,128 @@
"""Meshtastic ESP32 Unified OTA
"""
import os
import hashlib
import socket
import logging
from typing import Optional, Callable
logger = logging.getLogger(__name__)
def _file_sha256(filename: str):
"""Calculate SHA256 hash of a file."""
sha256_hash = hashlib.sha256()
with open(filename, "rb") as f:
for byte_block in iter(lambda: f.read(4096), b""):
sha256_hash.update(byte_block)
return sha256_hash
class OTAError(Exception):
"""Exception for OTA errors."""
class ESP32WiFiOTA:
"""ESP32 WiFi Unified OTA updates."""
def __init__(self, filename: str, hostname: str, port: int = 3232):
self._filename = filename
self._hostname = hostname
self._port = port
self._socket: Optional[socket.socket] = None
if not os.path.exists(self._filename):
raise FileNotFoundError(f"File {self._filename} does not exist")
self._file_hash = _file_sha256(self._filename)
def _read_line(self) -> str:
"""Read a line from the socket."""
if not self._socket:
raise ConnectionError("Socket not connected")
line = b""
while not line.endswith(b"\n"):
char = self._socket.recv(1)
if not char:
raise ConnectionError("Connection closed while waiting for response")
line += char
return line.decode("utf-8").strip()
def hash_bytes(self) -> bytes:
"""Return the hash as bytes."""
return self._file_hash.digest()
def hash_hex(self) -> str:
"""Return the hash as a hex string."""
return self._file_hash.hexdigest()
def update(self, progress_callback: Optional[Callable[[int, int], None]] = None):
"""Perform the OTA update."""
with open(self._filename, "rb") as f:
data = f.read()
size = len(data)
logger.info(f"Starting OTA update with {self._filename} ({size} bytes, hash {self.hash_hex()})")
self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._socket.settimeout(15)
try:
self._socket.connect((self._hostname, self._port))
logger.debug(f"Connected to {self._hostname}:{self._port}")
# Send start command
self._socket.sendall(f"OTA {size} {self.hash_hex()}\n".encode("utf-8"))
# Wait for OK from the device
while True:
response = self._read_line()
if response == "OK":
break
if response == "ERASING":
logger.info("Device is erasing flash...")
elif response.startswith("ERR "):
raise OTAError(f"Device reported error: {response}")
else:
logger.warning(f"Unexpected response: {response}")
# Stream firmware
sent_bytes = 0
chunk_size = 1024
while sent_bytes < size:
chunk = data[sent_bytes : sent_bytes + chunk_size]
self._socket.sendall(chunk)
sent_bytes += len(chunk)
if progress_callback:
progress_callback(sent_bytes, size)
else:
print(f"[{sent_bytes / size * 100:5.1f}%] Sent {sent_bytes} of {size} bytes...", end="\r")
if not progress_callback:
print()
# Wait for OK from device
logger.info("Firmware sent, waiting for verification...")
while True:
response = self._read_line()
if response == "OK":
logger.info("OTA update completed successfully!")
break
if response.startswith("ERR "):
raise OTAError(f"OTA update failed: {response}")
elif response != "ACK":
logger.warning(f"Unexpected final response: {response}")
finally:
if self._socket:
self._socket.close()
self._socket = None
+80 -13
View File
File diff suppressed because one or more lines are too long.
+888 -8
View File
File diff suppressed because it is too large. Load diff
+7 -4
View File
@@ -13,16 +13,19 @@ _sym_db = _symbol_database.Default()
from meshtastic.protobuf import channel_pb2 as meshtastic_dot_protobuf_dot_channel__pb2
from meshtastic.protobuf import config_pb2 as meshtastic_dot_protobuf_dot_config__pb2
from meshtastic.protobuf import nanopb_pb2 as meshtastic_dot_protobuf_dot_nanopb__pb2
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n!meshtastic/protobuf/apponly.proto\x12\x13meshtastic.protobuf\x1a!meshtastic/protobuf/channel.proto\x1a meshtastic/protobuf/config.proto\"\x81\x01\n\nChannelSet\x12\x36\n\x08settings\x18\x01 \x03(\x0b\x32$.meshtastic.protobuf.ChannelSettings\x12;\n\x0blora_config\x18\x02 \x01(\x0b\x32&.meshtastic.protobuf.Config.LoRaConfigBb\n\x13\x63om.geeksville.meshB\rAppOnlyProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n!meshtastic/protobuf/apponly.proto\x12\x13meshtastic.protobuf\x1a!meshtastic/protobuf/channel.proto\x1a meshtastic/protobuf/config.proto\x1a meshtastic/protobuf/nanopb.proto\"\x88\x01\n\nChannelSet\x12=\n\x08settings\x18\x01 \x03(\x0b\x32$.meshtastic.protobuf.ChannelSettingsB\x05\x92?\x02\x10\x08\x12;\n\x0blora_config\x18\x02 \x01(\x0b\x32&.meshtastic.protobuf.Config.LoRaConfigBc\n\x14org.meshtastic.protoB\rAppOnlyProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.apponly_pb2', _globals)
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\rAppOnlyProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
_globals['_CHANNELSET']._serialized_start=128
_globals['_CHANNELSET']._serialized_end=257
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\rAppOnlyProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
_CHANNELSET.fields_by_name['settings']._options = None
_CHANNELSET.fields_by_name['settings']._serialized_options = b'\222?\002\020\010'
_globals['_CHANNELSET']._serialized_start=162
_globals['_CHANNELSET']._serialized_end=298
# @@protoc_insertion_point(module_scope)
+231 -18
View File
File diff suppressed because one or more lines are too long.
+3378 -4
View File
File diff suppressed because it is too large. Load diff
+7 -4
View File
@@ -11,16 +11,19 @@ from google.protobuf.internal import builder as _builder
_sym_db = _symbol_database.Default()
from meshtastic.protobuf import nanopb_pb2 as meshtastic_dot_protobuf_dot_nanopb__pb2
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n(meshtastic/protobuf/cannedmessages.proto\x12\x13meshtastic.protobuf\"-\n\x19\x43\x61nnedMessageModuleConfig\x12\x10\n\x08messages\x18\x01 \x01(\tBn\n\x13\x63om.geeksville.meshB\x19\x43\x61nnedMessageConfigProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n(meshtastic/protobuf/cannedmessages.proto\x12\x13meshtastic.protobuf\x1a meshtastic/protobuf/nanopb.proto\"5\n\x19\x43\x61nnedMessageModuleConfig\x12\x18\n\x08messages\x18\x01 \x01(\tB\x06\x92?\x03\x08\xc9\x01\x42o\n\x14org.meshtastic.protoB\x19\x43\x61nnedMessageConfigProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.cannedmessages_pb2', _globals)
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\031CannedMessageConfigProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
_globals['_CANNEDMESSAGEMODULECONFIG']._serialized_start=65
_globals['_CANNEDMESSAGEMODULECONFIG']._serialized_end=110
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\031CannedMessageConfigProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
_CANNEDMESSAGEMODULECONFIG.fields_by_name['messages']._options = None
_CANNEDMESSAGEMODULECONFIG.fields_by_name['messages']._serialized_options = b'\222?\003\010\311\001'
_globals['_CANNEDMESSAGEMODULECONFIG']._serialized_start=99
_globals['_CANNEDMESSAGEMODULECONFIG']._serialized_end=152
# @@protoc_insertion_point(module_scope)
+17 -10
View File
@@ -11,24 +11,31 @@ from google.protobuf.internal import builder as _builder
_sym_db = _symbol_database.Default()
from meshtastic.protobuf import nanopb_pb2 as meshtastic_dot_protobuf_dot_nanopb__pb2
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n!meshtastic/protobuf/channel.proto\x12\x13meshtastic.protobuf\"\xc1\x01\n\x0f\x43hannelSettings\x12\x17\n\x0b\x63hannel_num\x18\x01 \x01(\rB\x02\x18\x01\x12\x0b\n\x03psk\x18\x02 \x01(\x0c\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\n\n\x02id\x18\x04 \x01(\x07\x12\x16\n\x0euplink_enabled\x18\x05 \x01(\x08\x12\x18\n\x10\x64ownlink_enabled\x18\x06 \x01(\x08\x12<\n\x0fmodule_settings\x18\x07 \x01(\x0b\x32#.meshtastic.protobuf.ModuleSettings\"E\n\x0eModuleSettings\x12\x1a\n\x12position_precision\x18\x01 \x01(\r\x12\x17\n\x0fis_client_muted\x18\x02 \x01(\x08\"\xb3\x01\n\x07\x43hannel\x12\r\n\x05index\x18\x01 \x01(\x05\x12\x36\n\x08settings\x18\x02 \x01(\x0b\x32$.meshtastic.protobuf.ChannelSettings\x12/\n\x04role\x18\x03 \x01(\x0e\x32!.meshtastic.protobuf.Channel.Role\"0\n\x04Role\x12\x0c\n\x08\x44ISABLED\x10\x00\x12\x0b\n\x07PRIMARY\x10\x01\x12\r\n\tSECONDARY\x10\x02\x42\x62\n\x13\x63om.geeksville.meshB\rChannelProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n!meshtastic/protobuf/channel.proto\x12\x13meshtastic.protobuf\x1a meshtastic/protobuf/nanopb.proto\"\xcf\x01\n\x0f\x43hannelSettings\x12\x17\n\x0b\x63hannel_num\x18\x01 \x01(\rB\x02\x18\x01\x12\x12\n\x03psk\x18\x02 \x01(\x0c\x42\x05\x92?\x02\x08 \x12\x13\n\x04name\x18\x03 \x01(\tB\x05\x92?\x02\x08\x0c\x12\n\n\x02id\x18\x04 \x01(\x07\x12\x16\n\x0euplink_enabled\x18\x05 \x01(\x08\x12\x18\n\x10\x64ownlink_enabled\x18\x06 \x01(\x08\x12<\n\x0fmodule_settings\x18\x07 \x01(\x0b\x32#.meshtastic.protobuf.ModuleSettings\">\n\x0eModuleSettings\x12\x1a\n\x12position_precision\x18\x01 \x01(\r\x12\x10\n\x08is_muted\x18\x02 \x01(\x08\"\xba\x01\n\x07\x43hannel\x12\x14\n\x05index\x18\x01 \x01(\x05\x42\x05\x92?\x02\x38\x08\x12\x36\n\x08settings\x18\x02 \x01(\x0b\x32$.meshtastic.protobuf.ChannelSettings\x12/\n\x04role\x18\x03 \x01(\x0e\x32!.meshtastic.protobuf.Channel.Role\"0\n\x04Role\x12\x0c\n\x08\x44ISABLED\x10\x00\x12\x0b\n\x07PRIMARY\x10\x01\x12\r\n\tSECONDARY\x10\x02\x42\x63\n\x14org.meshtastic.protoB\rChannelProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.channel_pb2', _globals)
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\rChannelProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\rChannelProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
_CHANNELSETTINGS.fields_by_name['channel_num']._options = None
_CHANNELSETTINGS.fields_by_name['channel_num']._serialized_options = b'\030\001'
_globals['_CHANNELSETTINGS']._serialized_start=59
_globals['_CHANNELSETTINGS']._serialized_end=252
_globals['_MODULESETTINGS']._serialized_start=254
_globals['_MODULESETTINGS']._serialized_end=323
_globals['_CHANNEL']._serialized_start=326
_globals['_CHANNEL']._serialized_end=505
_globals['_CHANNEL_ROLE']._serialized_start=457
_globals['_CHANNEL_ROLE']._serialized_end=505
_CHANNELSETTINGS.fields_by_name['psk']._options = None
_CHANNELSETTINGS.fields_by_name['psk']._serialized_options = b'\222?\002\010 '
_CHANNELSETTINGS.fields_by_name['name']._options = None
_CHANNELSETTINGS.fields_by_name['name']._serialized_options = b'\222?\002\010\014'
_CHANNEL.fields_by_name['index']._options = None
_CHANNEL.fields_by_name['index']._serialized_options = b'\222?\0028\010'
_globals['_CHANNELSETTINGS']._serialized_start=93
_globals['_CHANNELSETTINGS']._serialized_end=300
_globals['_MODULESETTINGS']._serialized_start=302
_globals['_MODULESETTINGS']._serialized_end=364
_globals['_CHANNEL']._serialized_start=367
_globals['_CHANNEL']._serialized_end=553
_globals['_CHANNEL_ROLE']._serialized_start=505
_globals['_CHANNEL_ROLE']._serialized_end=553
# @@protoc_insertion_point(module_scope)
+6 -6
View File
@@ -1,7 +1,7 @@
"""
@generated by mypy-protobuf. Do not edit manually!
isort:skip_file
"""
trunk-ignore(buf-lint/PACKAGE_DIRECTORY_MATCH)"""
import builtins
import google.protobuf.descriptor
@@ -127,23 +127,23 @@ class ModuleSettings(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
POSITION_PRECISION_FIELD_NUMBER: builtins.int
IS_CLIENT_MUTED_FIELD_NUMBER: builtins.int
IS_MUTED_FIELD_NUMBER: builtins.int
position_precision: builtins.int
"""
Bits of precision for the location sent in position packets.
"""
is_client_muted: builtins.bool
is_muted: builtins.bool
"""
Controls whether or not the phone / clients should mute the current channel
Controls whether or not the client / device should mute the current channel
Useful for noisy public channels you don't necessarily want to disable
"""
def __init__(
self,
*,
position_precision: builtins.int = ...,
is_client_muted: builtins.bool = ...,
is_muted: builtins.bool = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["is_client_muted", b"is_client_muted", "position_precision", b"position_precision"]) -> None: ...
def ClearField(self, field_name: typing.Literal["is_muted", b"is_muted", "position_precision", b"position_precision"]) -> None: ...
global___ModuleSettings = ModuleSettings
+13 -4
View File
@@ -13,16 +13,25 @@ _sym_db = _symbol_database.Default()
from meshtastic.protobuf import localonly_pb2 as meshtastic_dot_protobuf_dot_localonly__pb2
from meshtastic.protobuf import mesh_pb2 as meshtastic_dot_protobuf_dot_mesh__pb2
from meshtastic.protobuf import nanopb_pb2 as meshtastic_dot_protobuf_dot_nanopb__pb2
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n$meshtastic/protobuf/clientonly.proto\x12\x13meshtastic.protobuf\x1a#meshtastic/protobuf/localonly.proto\x1a\x1emeshtastic/protobuf/mesh.proto\"\xc4\x03\n\rDeviceProfile\x12\x16\n\tlong_name\x18\x01 \x01(\tH\x00\x88\x01\x01\x12\x17\n\nshort_name\x18\x02 \x01(\tH\x01\x88\x01\x01\x12\x18\n\x0b\x63hannel_url\x18\x03 \x01(\tH\x02\x88\x01\x01\x12\x35\n\x06\x63onfig\x18\x04 \x01(\x0b\x32 .meshtastic.protobuf.LocalConfigH\x03\x88\x01\x01\x12\x42\n\rmodule_config\x18\x05 \x01(\x0b\x32&.meshtastic.protobuf.LocalModuleConfigH\x04\x88\x01\x01\x12:\n\x0e\x66ixed_position\x18\x06 \x01(\x0b\x32\x1d.meshtastic.protobuf.PositionH\x05\x88\x01\x01\x12\x15\n\x08ringtone\x18\x07 \x01(\tH\x06\x88\x01\x01\x12\x1c\n\x0f\x63\x61nned_messages\x18\x08 \x01(\tH\x07\x88\x01\x01\x42\x0c\n\n_long_nameB\r\n\x0b_short_nameB\x0e\n\x0c_channel_urlB\t\n\x07_configB\x10\n\x0e_module_configB\x11\n\x0f_fixed_positionB\x0b\n\t_ringtoneB\x12\n\x10_canned_messagesBe\n\x13\x63om.geeksville.meshB\x10\x43lientOnlyProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n$meshtastic/protobuf/clientonly.proto\x12\x13meshtastic.protobuf\x1a#meshtastic/protobuf/localonly.proto\x1a\x1emeshtastic/protobuf/mesh.proto\x1a meshtastic/protobuf/nanopb.proto\"\xbe\x04\n\rDeviceProfile\x12\x1d\n\tlong_name\x18\x01 \x01(\tB\x05\x92?\x02\x08\x19H\x00\x88\x01\x01\x12\x1e\n\nshort_name\x18\x02 \x01(\tB\x05\x92?\x02\x08\x05H\x01\x88\x01\x01\x12\x18\n\x0b\x63hannel_url\x18\x03 \x01(\tH\x02\x88\x01\x01\x12\x35\n\x06\x63onfig\x18\x04 \x01(\x0b\x32 .meshtastic.protobuf.LocalConfigH\x03\x88\x01\x01\x12\x42\n\rmodule_config\x18\x05 \x01(\x0b\x32&.meshtastic.protobuf.LocalModuleConfigH\x04\x88\x01\x01\x12:\n\x0e\x66ixed_position\x18\x06 \x01(\x0b\x32\x1d.meshtastic.protobuf.PositionH\x05\x88\x01\x01\x12\x1d\n\x08ringtone\x18\x07 \x01(\tB\x06\x92?\x03\x08\xe7\x01H\x06\x88\x01\x01\x12$\n\x0f\x63\x61nned_messages\x18\x08 \x01(\tB\x06\x92?\x03\x08\xc9\x01H\x07\x88\x01\x01\x12\x1c\n\x0fis_unmessagable\x18\t \x01(\x08H\x08\x88\x01\x01\x12\x18\n\x0bis_licensed\x18\n \x01(\x08H\t\x88\x01\x01\x42\x0c\n\n_long_nameB\r\n\x0b_short_nameB\x0e\n\x0c_channel_urlB\t\n\x07_configB\x10\n\x0e_module_configB\x11\n\x0f_fixed_positionB\x0b\n\t_ringtoneB\x12\n\x10_canned_messagesB\x12\n\x10_is_unmessagableB\x0e\n\x0c_is_licensedBf\n\x14org.meshtastic.protoB\x10\x43lientOnlyProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.clientonly_pb2', _globals)
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\020ClientOnlyProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
_globals['_DEVICEPROFILE']._serialized_start=131
_globals['_DEVICEPROFILE']._serialized_end=583
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\020ClientOnlyProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
_DEVICEPROFILE.fields_by_name['long_name']._options = None
_DEVICEPROFILE.fields_by_name['long_name']._serialized_options = b'\222?\002\010\031'
_DEVICEPROFILE.fields_by_name['short_name']._options = None
_DEVICEPROFILE.fields_by_name['short_name']._serialized_options = b'\222?\002\010\005'
_DEVICEPROFILE.fields_by_name['ringtone']._options = None
_DEVICEPROFILE.fields_by_name['ringtone']._serialized_options = b'\222?\003\010\347\001'
_DEVICEPROFILE.fields_by_name['canned_messages']._options = None
_DEVICEPROFILE.fields_by_name['canned_messages']._serialized_options = b'\222?\003\010\311\001'
_globals['_DEVICEPROFILE']._serialized_start=165
_globals['_DEVICEPROFILE']._serialized_end=739
# @@protoc_insertion_point(module_scope)
+18 -2
View File
@@ -29,6 +29,8 @@ class DeviceProfile(google.protobuf.message.Message):
FIXED_POSITION_FIELD_NUMBER: builtins.int
RINGTONE_FIELD_NUMBER: builtins.int
CANNED_MESSAGES_FIELD_NUMBER: builtins.int
IS_UNMESSAGABLE_FIELD_NUMBER: builtins.int
IS_LICENSED_FIELD_NUMBER: builtins.int
long_name: builtins.str
"""
Long name for the node
@@ -49,6 +51,14 @@ class DeviceProfile(google.protobuf.message.Message):
"""
Predefined messages for CannedMessage
"""
is_unmessagable: builtins.bool
"""
Is the node unmessagable
"""
is_licensed: builtins.bool
"""
Is this node in licensed user mode
"""
@property
def config(self) -> meshtastic.protobuf.localonly_pb2.LocalConfig:
"""
@@ -78,9 +88,11 @@ class DeviceProfile(google.protobuf.message.Message):
fixed_position: meshtastic.protobuf.mesh_pb2.Position | None = ...,
ringtone: builtins.str | None = ...,
canned_messages: builtins.str | None = ...,
is_unmessagable: builtins.bool | None = ...,
is_licensed: builtins.bool | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["_canned_messages", b"_canned_messages", "_channel_url", b"_channel_url", "_config", b"_config", "_fixed_position", b"_fixed_position", "_long_name", b"_long_name", "_module_config", b"_module_config", "_ringtone", b"_ringtone", "_short_name", b"_short_name", "canned_messages", b"canned_messages", "channel_url", b"channel_url", "config", b"config", "fixed_position", b"fixed_position", "long_name", b"long_name", "module_config", b"module_config", "ringtone", b"ringtone", "short_name", b"short_name"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["_canned_messages", b"_canned_messages", "_channel_url", b"_channel_url", "_config", b"_config", "_fixed_position", b"_fixed_position", "_long_name", b"_long_name", "_module_config", b"_module_config", "_ringtone", b"_ringtone", "_short_name", b"_short_name", "canned_messages", b"canned_messages", "channel_url", b"channel_url", "config", b"config", "fixed_position", b"fixed_position", "long_name", b"long_name", "module_config", b"module_config", "ringtone", b"ringtone", "short_name", b"short_name"]) -> None: ...
def HasField(self, field_name: typing.Literal["_canned_messages", b"_canned_messages", "_channel_url", b"_channel_url", "_config", b"_config", "_fixed_position", b"_fixed_position", "_is_licensed", b"_is_licensed", "_is_unmessagable", b"_is_unmessagable", "_long_name", b"_long_name", "_module_config", b"_module_config", "_ringtone", b"_ringtone", "_short_name", b"_short_name", "canned_messages", b"canned_messages", "channel_url", b"channel_url", "config", b"config", "fixed_position", b"fixed_position", "is_licensed", b"is_licensed", "is_unmessagable", b"is_unmessagable", "long_name", b"long_name", "module_config", b"module_config", "ringtone", b"ringtone", "short_name", b"short_name"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["_canned_messages", b"_canned_messages", "_channel_url", b"_channel_url", "_config", b"_config", "_fixed_position", b"_fixed_position", "_is_licensed", b"_is_licensed", "_is_unmessagable", b"_is_unmessagable", "_long_name", b"_long_name", "_module_config", b"_module_config", "_ringtone", b"_ringtone", "_short_name", b"_short_name", "canned_messages", b"canned_messages", "channel_url", b"channel_url", "config", b"config", "fixed_position", b"fixed_position", "is_licensed", b"is_licensed", "is_unmessagable", b"is_unmessagable", "long_name", b"long_name", "module_config", b"module_config", "ringtone", b"ringtone", "short_name", b"short_name"]) -> None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_canned_messages", b"_canned_messages"]) -> typing.Literal["canned_messages"] | None: ...
@typing.overload
@@ -90,6 +102,10 @@ class DeviceProfile(google.protobuf.message.Message):
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_fixed_position", b"_fixed_position"]) -> typing.Literal["fixed_position"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_is_licensed", b"_is_licensed"]) -> typing.Literal["is_licensed"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_is_unmessagable", b"_is_unmessagable"]) -> typing.Literal["is_unmessagable"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_long_name", b"_long_name"]) -> typing.Literal["long_name"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_module_config", b"_module_config"]) -> typing.Literal["module_config"] | None: ...
+99 -52
View File
File diff suppressed because one or more lines are too long.
+491 -82
View File
@@ -64,6 +64,7 @@ class Config(google.protobuf.message.Message):
Description: Infrastructure node for extending network coverage by relaying messages with minimal overhead. Not visible in Nodes list.
Technical Details: Mesh packets will simply be rebroadcasted over this node. Nodes configured with this role will not originate NodeInfo, Position, Telemetry
or any other packet type. They will simply rebroadcast any mesh packets on the same frequency, channel num, spread factor, and coding rate.
Deprecated in v2.7.11 because it creates "holes" in the mesh rebroadcast chain.
"""
TRACKER: Config.DeviceConfig._Role.ValueType # 5
"""
@@ -116,6 +117,13 @@ class Config(google.protobuf.message.Message):
but should not be given priority over other routers in order to avoid unnecessaraily
consuming hops.
"""
CLIENT_BASE: Config.DeviceConfig._Role.ValueType # 12
"""
Description: Treats packets from or to favorited nodes as ROUTER_LATE, and all other packets as CLIENT.
Technical Details: Used for stronger attic/roof nodes to distribute messages more widely
from weaker, indoor, or less-well-positioned nodes. Recommended for users with multiple nodes
where one CLIENT_BASE acts as a more powerful base station, such as an attic/roof node.
"""
class Role(_Role, metaclass=_RoleEnumTypeWrapper):
"""
@@ -148,6 +156,7 @@ class Config(google.protobuf.message.Message):
Description: Infrastructure node for extending network coverage by relaying messages with minimal overhead. Not visible in Nodes list.
Technical Details: Mesh packets will simply be rebroadcasted over this node. Nodes configured with this role will not originate NodeInfo, Position, Telemetry
or any other packet type. They will simply rebroadcast any mesh packets on the same frequency, channel num, spread factor, and coding rate.
Deprecated in v2.7.11 because it creates "holes" in the mesh rebroadcast chain.
"""
TRACKER: Config.DeviceConfig.Role.ValueType # 5
"""
@@ -200,6 +209,13 @@ class Config(google.protobuf.message.Message):
but should not be given priority over other routers in order to avoid unnecessaraily
consuming hops.
"""
CLIENT_BASE: Config.DeviceConfig.Role.ValueType # 12
"""
Description: Treats packets from or to favorited nodes as ROUTER_LATE, and all other packets as CLIENT.
Technical Details: Used for stronger attic/roof nodes to distribute messages more widely
from weaker, indoor, or less-well-positioned nodes. Recommended for users with multiple nodes
where one CLIENT_BASE acts as a more powerful base station, such as an attic/roof node.
"""
class _RebroadcastMode:
ValueType = typing.NewType("ValueType", builtins.int)
@@ -272,6 +288,73 @@ class Config(google.protobuf.message.Message):
Only rebroadcasts packets with standard portnums: NodeInfo, Text, Position, Telemetry, and Routing.
"""
class _BuzzerMode:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
class _BuzzerModeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[Config.DeviceConfig._BuzzerMode.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
ALL_ENABLED: Config.DeviceConfig._BuzzerMode.ValueType # 0
"""
Default behavior.
Buzzer is enabled for all audio feedback including button presses and alerts.
"""
DISABLED: Config.DeviceConfig._BuzzerMode.ValueType # 1
"""
Disabled.
All buzzer audio feedback is disabled.
"""
NOTIFICATIONS_ONLY: Config.DeviceConfig._BuzzerMode.ValueType # 2
"""
Notifications Only.
Buzzer is enabled only for notifications and alerts, but not for button presses.
External notification config determines the specifics of the notification behavior.
"""
SYSTEM_ONLY: Config.DeviceConfig._BuzzerMode.ValueType # 3
"""
Non-notification system buzzer tones only.
Buzzer is enabled only for non-notification tones such as button presses, startup, shutdown, but not for alerts.
"""
DIRECT_MSG_ONLY: Config.DeviceConfig._BuzzerMode.ValueType # 4
"""
Direct Message notifications only.
Buzzer is enabled only for direct messages and alerts, but not for button presses.
External notification config determines the specifics of the notification behavior.
"""
class BuzzerMode(_BuzzerMode, metaclass=_BuzzerModeEnumTypeWrapper):
"""
Defines buzzer behavior for audio feedback
"""
ALL_ENABLED: Config.DeviceConfig.BuzzerMode.ValueType # 0
"""
Default behavior.
Buzzer is enabled for all audio feedback including button presses and alerts.
"""
DISABLED: Config.DeviceConfig.BuzzerMode.ValueType # 1
"""
Disabled.
All buzzer audio feedback is disabled.
"""
NOTIFICATIONS_ONLY: Config.DeviceConfig.BuzzerMode.ValueType # 2
"""
Notifications Only.
Buzzer is enabled only for notifications and alerts, but not for button presses.
External notification config determines the specifics of the notification behavior.
"""
SYSTEM_ONLY: Config.DeviceConfig.BuzzerMode.ValueType # 3
"""
Non-notification system buzzer tones only.
Buzzer is enabled only for non-notification tones such as button presses, startup, shutdown, but not for alerts.
"""
DIRECT_MSG_ONLY: Config.DeviceConfig.BuzzerMode.ValueType # 4
"""
Direct Message notifications only.
Buzzer is enabled only for direct messages and alerts, but not for button presses.
External notification config determines the specifics of the notification behavior.
"""
ROLE_FIELD_NUMBER: builtins.int
SERIAL_ENABLED_FIELD_NUMBER: builtins.int
BUTTON_GPIO_FIELD_NUMBER: builtins.int
@@ -283,6 +366,7 @@ class Config(google.protobuf.message.Message):
DISABLE_TRIPLE_CLICK_FIELD_NUMBER: builtins.int
TZDEF_FIELD_NUMBER: builtins.int
LED_HEARTBEAT_DISABLED_FIELD_NUMBER: builtins.int
BUZZER_MODE_FIELD_NUMBER: builtins.int
role: global___Config.DeviceConfig.Role.ValueType
"""
Sets the role of node
@@ -333,6 +417,11 @@ class Config(google.protobuf.message.Message):
"""
If true, disable the default blinking LED (LED_PIN) behavior on the device
"""
buzzer_mode: global___Config.DeviceConfig.BuzzerMode.ValueType
"""
Controls buzzer behavior for audio feedback
Defaults to ENABLED
"""
def __init__(
self,
*,
@@ -347,8 +436,9 @@ class Config(google.protobuf.message.Message):
disable_triple_click: builtins.bool = ...,
tzdef: builtins.str = ...,
led_heartbeat_disabled: builtins.bool = ...,
buzzer_mode: global___Config.DeviceConfig.BuzzerMode.ValueType = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["button_gpio", b"button_gpio", "buzzer_gpio", b"buzzer_gpio", "disable_triple_click", b"disable_triple_click", "double_tap_as_button_press", b"double_tap_as_button_press", "is_managed", b"is_managed", "led_heartbeat_disabled", b"led_heartbeat_disabled", "node_info_broadcast_secs", b"node_info_broadcast_secs", "rebroadcast_mode", b"rebroadcast_mode", "role", b"role", "serial_enabled", b"serial_enabled", "tzdef", b"tzdef"]) -> None: ...
def ClearField(self, field_name: typing.Literal["button_gpio", b"button_gpio", "buzzer_gpio", b"buzzer_gpio", "buzzer_mode", b"buzzer_mode", "disable_triple_click", b"disable_triple_click", "double_tap_as_button_press", b"double_tap_as_button_press", "is_managed", b"is_managed", "led_heartbeat_disabled", b"led_heartbeat_disabled", "node_info_broadcast_secs", b"node_info_broadcast_secs", "rebroadcast_mode", b"rebroadcast_mode", "role", b"role", "serial_enabled", b"serial_enabled", "tzdef", b"tzdef"]) -> None: ...
@typing.final
class PositionConfig(google.protobuf.message.Message):
@@ -615,7 +705,7 @@ class Config(google.protobuf.message.Message):
POWERMON_ENABLES_FIELD_NUMBER: builtins.int
is_power_saving: builtins.bool
"""
Description: Will sleep everything as much as possible, for the tracker and sensor role this will also include the lora radio.
Description: Will sleep everything as much as possible, for the tracker and sensor role this will also include the lora radio.
Don't use this setting if you want to use your device with the phone apps or are using a device without a user button.
Technical Details: Works for ESP32 devices and NRF52 devices in the Sensor or Tracker roles
"""
@@ -781,6 +871,7 @@ class Config(google.protobuf.message.Message):
IPV4_CONFIG_FIELD_NUMBER: builtins.int
RSYSLOG_SERVER_FIELD_NUMBER: builtins.int
ENABLED_PROTOCOLS_FIELD_NUMBER: builtins.int
IPV6_ENABLED_FIELD_NUMBER: builtins.int
wifi_enabled: builtins.bool
"""
Enable WiFi (disables Bluetooth)
@@ -796,7 +887,7 @@ class Config(google.protobuf.message.Message):
"""
ntp_server: builtins.str
"""
NTP server to use if WiFi is conneced, defaults to `0.pool.ntp.org`
NTP server to use if WiFi is conneced, defaults to `meshtastic.pool.ntp.org`
"""
eth_enabled: builtins.bool
"""
@@ -814,6 +905,10 @@ class Config(google.protobuf.message.Message):
"""
Flags for enabling/disabling network protocols
"""
ipv6_enabled: builtins.bool
"""
Enable/Disable ipv6 support
"""
@property
def ipv4_config(self) -> global___Config.NetworkConfig.IpV4Config:
"""
@@ -832,9 +927,10 @@ class Config(google.protobuf.message.Message):
ipv4_config: global___Config.NetworkConfig.IpV4Config | None = ...,
rsyslog_server: builtins.str = ...,
enabled_protocols: builtins.int = ...,
ipv6_enabled: builtins.bool = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["ipv4_config", b"ipv4_config"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["address_mode", b"address_mode", "enabled_protocols", b"enabled_protocols", "eth_enabled", b"eth_enabled", "ipv4_config", b"ipv4_config", "ntp_server", b"ntp_server", "rsyslog_server", b"rsyslog_server", "wifi_enabled", b"wifi_enabled", "wifi_psk", b"wifi_psk", "wifi_ssid", b"wifi_ssid"]) -> None: ...
def ClearField(self, field_name: typing.Literal["address_mode", b"address_mode", "enabled_protocols", b"enabled_protocols", "eth_enabled", b"eth_enabled", "ipv4_config", b"ipv4_config", "ipv6_enabled", b"ipv6_enabled", "ntp_server", b"ntp_server", "rsyslog_server", b"rsyslog_server", "wifi_enabled", b"wifi_enabled", "wifi_psk", b"wifi_psk", "wifi_ssid", b"wifi_ssid"]) -> None: ...
@typing.final
class DisplayConfig(google.protobuf.message.Message):
@@ -844,80 +940,20 @@ class Config(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
class _GpsCoordinateFormat:
class _DeprecatedGpsCoordinateFormat:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
class _GpsCoordinateFormatEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[Config.DisplayConfig._GpsCoordinateFormat.ValueType], builtins.type):
class _DeprecatedGpsCoordinateFormatEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[Config.DisplayConfig._DeprecatedGpsCoordinateFormat.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
DEC: Config.DisplayConfig._GpsCoordinateFormat.ValueType # 0
UNUSED: Config.DisplayConfig._DeprecatedGpsCoordinateFormat.ValueType # 0
class DeprecatedGpsCoordinateFormat(_DeprecatedGpsCoordinateFormat, metaclass=_DeprecatedGpsCoordinateFormatEnumTypeWrapper):
"""
GPS coordinates are displayed in the normal decimal degrees format:
DD.DDDDDD DDD.DDDDDD
"""
DMS: Config.DisplayConfig._GpsCoordinateFormat.ValueType # 1
"""
GPS coordinates are displayed in the degrees minutes seconds format:
DD°MM'SS"C DDD°MM'SS"C, where C is the compass point representing the locations quadrant
"""
UTM: Config.DisplayConfig._GpsCoordinateFormat.ValueType # 2
"""
Universal Transverse Mercator format:
ZZB EEEEEE NNNNNNN, where Z is zone, B is band, E is easting, N is northing
"""
MGRS: Config.DisplayConfig._GpsCoordinateFormat.ValueType # 3
"""
Military Grid Reference System format:
ZZB CD EEEEE NNNNN, where Z is zone, B is band, C is the east 100k square, D is the north 100k square,
E is easting, N is northing
"""
OLC: Config.DisplayConfig._GpsCoordinateFormat.ValueType # 4
"""
Open Location Code (aka Plus Codes).
"""
OSGR: Config.DisplayConfig._GpsCoordinateFormat.ValueType # 5
"""
Ordnance Survey Grid Reference (the National Grid System of the UK).
Format: AB EEEEE NNNNN, where A is the east 100k square, B is the north 100k square,
E is the easting, N is the northing
Deprecated in 2.7.4: Unused
"""
class GpsCoordinateFormat(_GpsCoordinateFormat, metaclass=_GpsCoordinateFormatEnumTypeWrapper):
"""
How the GPS coordinates are displayed on the OLED screen.
"""
DEC: Config.DisplayConfig.GpsCoordinateFormat.ValueType # 0
"""
GPS coordinates are displayed in the normal decimal degrees format:
DD.DDDDDD DDD.DDDDDD
"""
DMS: Config.DisplayConfig.GpsCoordinateFormat.ValueType # 1
"""
GPS coordinates are displayed in the degrees minutes seconds format:
DD°MM'SS"C DDD°MM'SS"C, where C is the compass point representing the locations quadrant
"""
UTM: Config.DisplayConfig.GpsCoordinateFormat.ValueType # 2
"""
Universal Transverse Mercator format:
ZZB EEEEEE NNNNNNN, where Z is zone, B is band, E is easting, N is northing
"""
MGRS: Config.DisplayConfig.GpsCoordinateFormat.ValueType # 3
"""
Military Grid Reference System format:
ZZB CD EEEEE NNNNN, where Z is zone, B is band, C is the east 100k square, D is the north 100k square,
E is easting, N is northing
"""
OLC: Config.DisplayConfig.GpsCoordinateFormat.ValueType # 4
"""
Open Location Code (aka Plus Codes).
"""
OSGR: Config.DisplayConfig.GpsCoordinateFormat.ValueType # 5
"""
Ordnance Survey Grid Reference (the National Grid System of the UK).
Format: AB EEEEE NNNNN, where A is the east 100k square, B is the north 100k square,
E is the easting, N is the northing
"""
UNUSED: Config.DisplayConfig.DeprecatedGpsCoordinateFormat.ValueType # 0
class _DisplayUnits:
ValueType = typing.NewType("ValueType", builtins.int)
@@ -956,20 +992,28 @@ class Config(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
OLED_AUTO: Config.DisplayConfig._OledType.ValueType # 0
"""
Default / Auto
Default / Autodetect
"""
OLED_SSD1306: Config.DisplayConfig._OledType.ValueType # 1
"""
Default / Auto
Default / Autodetect
"""
OLED_SH1106: Config.DisplayConfig._OledType.ValueType # 2
"""
Default / Auto
Default / Autodetect
"""
OLED_SH1107: Config.DisplayConfig._OledType.ValueType # 3
"""
Can not be auto detected but set by proto. Used for 128x64 screens
"""
OLED_SH1107_128_128: Config.DisplayConfig._OledType.ValueType # 4
"""
Can not be auto detected but set by proto. Used for 128x128 screens
"""
OLED_SH1107_ROTATED: Config.DisplayConfig._OledType.ValueType # 5
"""
Can not be auto detected but set by proto. Used for 64x128 rotated screens
"""
class OledType(_OledType, metaclass=_OledTypeEnumTypeWrapper):
"""
@@ -978,20 +1022,28 @@ class Config(google.protobuf.message.Message):
OLED_AUTO: Config.DisplayConfig.OledType.ValueType # 0
"""
Default / Auto
Default / Autodetect
"""
OLED_SSD1306: Config.DisplayConfig.OledType.ValueType # 1
"""
Default / Auto
Default / Autodetect
"""
OLED_SH1106: Config.DisplayConfig.OledType.ValueType # 2
"""
Default / Auto
Default / Autodetect
"""
OLED_SH1107: Config.DisplayConfig.OledType.ValueType # 3
"""
Can not be auto detected but set by proto. Used for 128x64 screens
"""
OLED_SH1107_128_128: Config.DisplayConfig.OledType.ValueType # 4
"""
Can not be auto detected but set by proto. Used for 128x128 screens
"""
OLED_SH1107_ROTATED: Config.DisplayConfig.OledType.ValueType # 5
"""
Can not be auto detected but set by proto. Used for 64x128 rotated screens
"""
class _DisplayMode:
ValueType = typing.NewType("ValueType", builtins.int)
@@ -1118,13 +1170,17 @@ class Config(google.protobuf.message.Message):
HEADING_BOLD_FIELD_NUMBER: builtins.int
WAKE_ON_TAP_OR_MOTION_FIELD_NUMBER: builtins.int
COMPASS_ORIENTATION_FIELD_NUMBER: builtins.int
USE_12H_CLOCK_FIELD_NUMBER: builtins.int
USE_LONG_NODE_NAME_FIELD_NUMBER: builtins.int
ENABLE_MESSAGE_BUBBLES_FIELD_NUMBER: builtins.int
screen_on_secs: builtins.int
"""
Number of seconds the screen stays on after pressing the user button or receiving a message
0 for default of one minute MAXUINT for always on
"""
gps_format: global___Config.DisplayConfig.GpsCoordinateFormat.ValueType
gps_format: global___Config.DisplayConfig.DeprecatedGpsCoordinateFormat.ValueType
"""
Deprecated in 2.7.4: Unused
How the GPS coordinates are formatted on the OLED screen.
"""
auto_screen_carousel_secs: builtins.int
@@ -1165,11 +1221,25 @@ class Config(google.protobuf.message.Message):
"""
Indicates how to rotate or invert the compass output to accurate display on the display.
"""
use_12h_clock: builtins.bool
"""
If false (default), the device will display the time in 24-hour format on screen.
If true, the device will display the time in 12-hour format on screen.
"""
use_long_node_name: builtins.bool
"""
If false (default), the device will use short names for various display screens.
If true, node names will show in long format
"""
enable_message_bubbles: builtins.bool
"""
If true, the device will display message bubbles on screen.
"""
def __init__(
self,
*,
screen_on_secs: builtins.int = ...,
gps_format: global___Config.DisplayConfig.GpsCoordinateFormat.ValueType = ...,
gps_format: global___Config.DisplayConfig.DeprecatedGpsCoordinateFormat.ValueType = ...,
auto_screen_carousel_secs: builtins.int = ...,
compass_north_top: builtins.bool = ...,
flip_screen: builtins.bool = ...,
@@ -1179,8 +1249,11 @@ class Config(google.protobuf.message.Message):
heading_bold: builtins.bool = ...,
wake_on_tap_or_motion: builtins.bool = ...,
compass_orientation: global___Config.DisplayConfig.CompassOrientation.ValueType = ...,
use_12h_clock: builtins.bool = ...,
use_long_node_name: builtins.bool = ...,
enable_message_bubbles: builtins.bool = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["auto_screen_carousel_secs", b"auto_screen_carousel_secs", "compass_north_top", b"compass_north_top", "compass_orientation", b"compass_orientation", "displaymode", b"displaymode", "flip_screen", b"flip_screen", "gps_format", b"gps_format", "heading_bold", b"heading_bold", "oled", b"oled", "screen_on_secs", b"screen_on_secs", "units", b"units", "wake_on_tap_or_motion", b"wake_on_tap_or_motion"]) -> None: ...
def ClearField(self, field_name: typing.Literal["auto_screen_carousel_secs", b"auto_screen_carousel_secs", "compass_north_top", b"compass_north_top", "compass_orientation", b"compass_orientation", "displaymode", b"displaymode", "enable_message_bubbles", b"enable_message_bubbles", "flip_screen", b"flip_screen", "gps_format", b"gps_format", "heading_bold", b"heading_bold", "oled", b"oled", "screen_on_secs", b"screen_on_secs", "units", b"units", "use_12h_clock", b"use_12h_clock", "use_long_node_name", b"use_long_node_name", "wake_on_tap_or_motion", b"wake_on_tap_or_motion"]) -> None: ...
@typing.final
class LoRaConfig(google.protobuf.message.Message):
@@ -1284,6 +1357,72 @@ class Config(google.protobuf.message.Message):
"""
Philippines 915mhz
"""
ANZ_433: Config.LoRaConfig._RegionCode.ValueType # 22
"""
Australia / New Zealand 433MHz
"""
KZ_433: Config.LoRaConfig._RegionCode.ValueType # 23
"""
Kazakhstan 433MHz
"""
KZ_863: Config.LoRaConfig._RegionCode.ValueType # 24
"""
Kazakhstan 863MHz
"""
NP_865: Config.LoRaConfig._RegionCode.ValueType # 25
"""
Nepal 865MHz
"""
BR_902: Config.LoRaConfig._RegionCode.ValueType # 26
"""
Brazil 902MHz
"""
ITU1_2M: Config.LoRaConfig._RegionCode.ValueType # 27
"""
ITU Region 1 Amateur Radio 2m band (144-146 MHz)
"""
ITU2_2M: Config.LoRaConfig._RegionCode.ValueType # 28
"""
ITU Region 2 Amateur Radio 2m band (144-148 MHz)
"""
EU_866: Config.LoRaConfig._RegionCode.ValueType # 29
"""
EU 866MHz band (Band no. 47b of 2006/771/EC and subsequent amendments) for Non-specific short-range devices (SRD)
"""
EU_874: Config.LoRaConfig._RegionCode.ValueType # 30
"""
EU 874MHz and 917MHz bands (Band no. 1 and 4 of 2022/172/EC and subsequent amendments) for Non-specific short-range devices (SRD)
"""
EU_917: Config.LoRaConfig._RegionCode.ValueType # 31
EU_N_868: Config.LoRaConfig._RegionCode.ValueType # 32
"""
EU 868MHz band, with narrow presets
"""
ITU3_2M: Config.LoRaConfig._RegionCode.ValueType # 33
"""
ITU Region 3 Amateur Radio 2m band (144-148 MHz)
"""
ITU1_70CM: Config.LoRaConfig._RegionCode.ValueType # 34
"""
ITU Region 1 Amateur Radio 70cm band (430-440 MHz)
"""
ITU2_70CM: Config.LoRaConfig._RegionCode.ValueType # 35
"""
ITU Region 2 Amateur Radio 70cm band (420-450 MHz)
Note: Some countries do not allocate 420-430 MHz or 440-450 MHz.
Check local law!
"""
ITU3_70CM: Config.LoRaConfig._RegionCode.ValueType # 36
"""
ITU Region 3 Amateur Radio 70cm band (430-450 MHz)
Note: Some countries do not allocate 440-450 MHz. Check local law!
"""
ITU2_125CM: Config.LoRaConfig._RegionCode.ValueType # 37
"""
ITU Region 2 Amateur Radio 1.25m '125cm' band (220-225 MHz)
Note: Some countries do not allocate 220-222 MHz (Ex: USA/Canada).
Check local law!
"""
class RegionCode(_RegionCode, metaclass=_RegionCodeEnumTypeWrapper): ...
UNSET: Config.LoRaConfig.RegionCode.ValueType # 0
@@ -1374,6 +1513,72 @@ class Config(google.protobuf.message.Message):
"""
Philippines 915mhz
"""
ANZ_433: Config.LoRaConfig.RegionCode.ValueType # 22
"""
Australia / New Zealand 433MHz
"""
KZ_433: Config.LoRaConfig.RegionCode.ValueType # 23
"""
Kazakhstan 433MHz
"""
KZ_863: Config.LoRaConfig.RegionCode.ValueType # 24
"""
Kazakhstan 863MHz
"""
NP_865: Config.LoRaConfig.RegionCode.ValueType # 25
"""
Nepal 865MHz
"""
BR_902: Config.LoRaConfig.RegionCode.ValueType # 26
"""
Brazil 902MHz
"""
ITU1_2M: Config.LoRaConfig.RegionCode.ValueType # 27
"""
ITU Region 1 Amateur Radio 2m band (144-146 MHz)
"""
ITU2_2M: Config.LoRaConfig.RegionCode.ValueType # 28
"""
ITU Region 2 Amateur Radio 2m band (144-148 MHz)
"""
EU_866: Config.LoRaConfig.RegionCode.ValueType # 29
"""
EU 866MHz band (Band no. 47b of 2006/771/EC and subsequent amendments) for Non-specific short-range devices (SRD)
"""
EU_874: Config.LoRaConfig.RegionCode.ValueType # 30
"""
EU 874MHz and 917MHz bands (Band no. 1 and 4 of 2022/172/EC and subsequent amendments) for Non-specific short-range devices (SRD)
"""
EU_917: Config.LoRaConfig.RegionCode.ValueType # 31
EU_N_868: Config.LoRaConfig.RegionCode.ValueType # 32
"""
EU 868MHz band, with narrow presets
"""
ITU3_2M: Config.LoRaConfig.RegionCode.ValueType # 33
"""
ITU Region 3 Amateur Radio 2m band (144-148 MHz)
"""
ITU1_70CM: Config.LoRaConfig.RegionCode.ValueType # 34
"""
ITU Region 1 Amateur Radio 70cm band (430-440 MHz)
"""
ITU2_70CM: Config.LoRaConfig.RegionCode.ValueType # 35
"""
ITU Region 2 Amateur Radio 70cm band (420-450 MHz)
Note: Some countries do not allocate 420-430 MHz or 440-450 MHz.
Check local law!
"""
ITU3_70CM: Config.LoRaConfig.RegionCode.ValueType # 36
"""
ITU Region 3 Amateur Radio 70cm band (430-450 MHz)
Note: Some countries do not allocate 440-450 MHz. Check local law!
"""
ITU2_125CM: Config.LoRaConfig.RegionCode.ValueType # 37
"""
ITU Region 2 Amateur Radio 1.25m '125cm' band (220-225 MHz)
Note: Some countries do not allocate 220-222 MHz (Ex: USA/Canada).
Check local law!
"""
class _ModemPreset:
ValueType = typing.NewType("ValueType", builtins.int)
@@ -1388,6 +1593,7 @@ class Config(google.protobuf.message.Message):
LONG_SLOW: Config.LoRaConfig._ModemPreset.ValueType # 1
"""
Long Range - Slow
Deprecated in 2.7: Unpopular slow preset.
"""
VERY_LONG_SLOW: Config.LoRaConfig._ModemPreset.ValueType # 2
"""
@@ -1420,6 +1626,60 @@ class Config(google.protobuf.message.Message):
This is the fastest preset and the only one with 500kHz bandwidth.
It is not legal to use in all regions due to this wider bandwidth.
"""
LONG_TURBO: Config.LoRaConfig._ModemPreset.ValueType # 9
"""
Long Range - Turbo
This preset performs similarly to LongFast, but with 500Khz bandwidth.
"""
LITE_FAST: Config.LoRaConfig._ModemPreset.ValueType # 10
"""
Lite Fast
Medium range preset optimized for EU 866MHz SRD band with 125kHz bandwidth.
Comparable link budget to MEDIUM_FAST but compliant with Band no. 47b of 2006/771/EC.
"""
LITE_SLOW: Config.LoRaConfig._ModemPreset.ValueType # 11
"""
Lite Slow
Medium-to-moderate range preset optimized for EU 866MHz SRD band with 125kHz bandwidth.
Comparable link budget to LONG_FAST but compliant with Band no. 47b of 2006/771/EC.
"""
NARROW_FAST: Config.LoRaConfig._ModemPreset.ValueType # 12
"""
Narrow Fast
Medium-to-moderate range preset optimized for EU 868MHz band with 62.5kHz bandwidth.
Comparable link budget to SHORT_SLOW, but with half the data rate.
Intended to avoid interference with other devices.
"""
NARROW_SLOW: Config.LoRaConfig._ModemPreset.ValueType # 13
"""
Narrow Slow
Moderate range preset optimized for EU 868MHz band with 62.5kHz bandwidth.
Comparable link budget and data rate to LONG_FAST.
"""
TINY_FAST: Config.LoRaConfig._ModemPreset.ValueType # 14
"""
Tiny Fast
Preset optimized for compliance with Amateur Radio restrictions with 20kHz bandwidth.
Many regions limit data transmission bandwidth in lower amateur bands (2 Meter).
Note: TCXO with tight tolerances (±5 ppm or better) is *absolutely required* at these narrow bandwidths.
Only compatible with SX127x and SX126x chipsets.
Comparable link budget and data rate to LONG_FAST.
"""
TINY_SLOW: Config.LoRaConfig._ModemPreset.ValueType # 15
"""
Tiny Slow
Preset optimized for compliance with Amateur Radio restrictions with 20kHz bandwidth.
Many regions limit data transmission bandwidth in lower amateur bands (2 Meter).
Note: TCXO with tight tolerances (±5 ppm or better) is *absolutely required* at these narrow bandwidths.
Only compatible with SX127x and SX126x chipsets.
Comparable link budget and data rate to LONG_MODERATE.
"""
MEDIUM_TURBO: Config.LoRaConfig._ModemPreset.ValueType # 16
"""
Medium Range - Turbo
This preset performs similarly to MEDIUM_FAST, but with 500kHz bandwidth.
It is not legal to use in all regions due to this wider bandwidth.
"""
class ModemPreset(_ModemPreset, metaclass=_ModemPresetEnumTypeWrapper):
"""
@@ -1434,6 +1694,7 @@ class Config(google.protobuf.message.Message):
LONG_SLOW: Config.LoRaConfig.ModemPreset.ValueType # 1
"""
Long Range - Slow
Deprecated in 2.7: Unpopular slow preset.
"""
VERY_LONG_SLOW: Config.LoRaConfig.ModemPreset.ValueType # 2
"""
@@ -1466,6 +1727,93 @@ class Config(google.protobuf.message.Message):
This is the fastest preset and the only one with 500kHz bandwidth.
It is not legal to use in all regions due to this wider bandwidth.
"""
LONG_TURBO: Config.LoRaConfig.ModemPreset.ValueType # 9
"""
Long Range - Turbo
This preset performs similarly to LongFast, but with 500Khz bandwidth.
"""
LITE_FAST: Config.LoRaConfig.ModemPreset.ValueType # 10
"""
Lite Fast
Medium range preset optimized for EU 866MHz SRD band with 125kHz bandwidth.
Comparable link budget to MEDIUM_FAST but compliant with Band no. 47b of 2006/771/EC.
"""
LITE_SLOW: Config.LoRaConfig.ModemPreset.ValueType # 11
"""
Lite Slow
Medium-to-moderate range preset optimized for EU 866MHz SRD band with 125kHz bandwidth.
Comparable link budget to LONG_FAST but compliant with Band no. 47b of 2006/771/EC.
"""
NARROW_FAST: Config.LoRaConfig.ModemPreset.ValueType # 12
"""
Narrow Fast
Medium-to-moderate range preset optimized for EU 868MHz band with 62.5kHz bandwidth.
Comparable link budget to SHORT_SLOW, but with half the data rate.
Intended to avoid interference with other devices.
"""
NARROW_SLOW: Config.LoRaConfig.ModemPreset.ValueType # 13
"""
Narrow Slow
Moderate range preset optimized for EU 868MHz band with 62.5kHz bandwidth.
Comparable link budget and data rate to LONG_FAST.
"""
TINY_FAST: Config.LoRaConfig.ModemPreset.ValueType # 14
"""
Tiny Fast
Preset optimized for compliance with Amateur Radio restrictions with 20kHz bandwidth.
Many regions limit data transmission bandwidth in lower amateur bands (2 Meter).
Note: TCXO with tight tolerances (±5 ppm or better) is *absolutely required* at these narrow bandwidths.
Only compatible with SX127x and SX126x chipsets.
Comparable link budget and data rate to LONG_FAST.
"""
TINY_SLOW: Config.LoRaConfig.ModemPreset.ValueType # 15
"""
Tiny Slow
Preset optimized for compliance with Amateur Radio restrictions with 20kHz bandwidth.
Many regions limit data transmission bandwidth in lower amateur bands (2 Meter).
Note: TCXO with tight tolerances (±5 ppm or better) is *absolutely required* at these narrow bandwidths.
Only compatible with SX127x and SX126x chipsets.
Comparable link budget and data rate to LONG_MODERATE.
"""
MEDIUM_TURBO: Config.LoRaConfig.ModemPreset.ValueType # 16
"""
Medium Range - Turbo
This preset performs similarly to MEDIUM_FAST, but with 500kHz bandwidth.
It is not legal to use in all regions due to this wider bandwidth.
"""
class _FEM_LNA_Mode:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
class _FEM_LNA_ModeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[Config.LoRaConfig._FEM_LNA_Mode.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
DISABLED: Config.LoRaConfig._FEM_LNA_Mode.ValueType # 0
"""
FEM_LNA is present but disabled
"""
ENABLED: Config.LoRaConfig._FEM_LNA_Mode.ValueType # 1
"""
FEM_LNA is present and enabled
"""
NOT_PRESENT: Config.LoRaConfig._FEM_LNA_Mode.ValueType # 2
"""
FEM_LNA is not present on the device
"""
class FEM_LNA_Mode(_FEM_LNA_Mode, metaclass=_FEM_LNA_ModeEnumTypeWrapper): ...
DISABLED: Config.LoRaConfig.FEM_LNA_Mode.ValueType # 0
"""
FEM_LNA is present but disabled
"""
ENABLED: Config.LoRaConfig.FEM_LNA_Mode.ValueType # 1
"""
FEM_LNA is present and enabled
"""
NOT_PRESENT: Config.LoRaConfig.FEM_LNA_Mode.ValueType # 2
"""
FEM_LNA is not present on the device
"""
USE_PRESET_FIELD_NUMBER: builtins.int
MODEM_PRESET_FIELD_NUMBER: builtins.int
@@ -1485,6 +1833,8 @@ class Config(google.protobuf.message.Message):
IGNORE_INCOMING_FIELD_NUMBER: builtins.int
IGNORE_MQTT_FIELD_NUMBER: builtins.int
CONFIG_OK_TO_MQTT_FIELD_NUMBER: builtins.int
FEM_LNA_MODE_FIELD_NUMBER: builtins.int
SERIAL_HAL_ONLY_FIELD_NUMBER: builtins.int
use_preset: builtins.bool
"""
When enabled, the `modem_preset` fields will be adhered to, else the `bandwidth`/`spread_factor`/`coding_rate`
@@ -1582,6 +1932,14 @@ class Config(google.protobuf.message.Message):
"""
Sets the ok_to_mqtt bit on outgoing packets
"""
fem_lna_mode: global___Config.LoRaConfig.FEM_LNA_Mode.ValueType
"""
Set where LORA FEM is enabled, disabled, or not present
"""
serial_hal_only: builtins.bool
"""
Don't use radiolib to initialize the radio, instead listen for a serialHal connection
"""
@property
def ignore_incoming(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]:
"""
@@ -1611,8 +1969,10 @@ class Config(google.protobuf.message.Message):
ignore_incoming: collections.abc.Iterable[builtins.int] | None = ...,
ignore_mqtt: builtins.bool = ...,
config_ok_to_mqtt: builtins.bool = ...,
fem_lna_mode: global___Config.LoRaConfig.FEM_LNA_Mode.ValueType = ...,
serial_hal_only: builtins.bool = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["bandwidth", b"bandwidth", "channel_num", b"channel_num", "coding_rate", b"coding_rate", "config_ok_to_mqtt", b"config_ok_to_mqtt", "frequency_offset", b"frequency_offset", "hop_limit", b"hop_limit", "ignore_incoming", b"ignore_incoming", "ignore_mqtt", b"ignore_mqtt", "modem_preset", b"modem_preset", "override_duty_cycle", b"override_duty_cycle", "override_frequency", b"override_frequency", "pa_fan_disabled", b"pa_fan_disabled", "region", b"region", "spread_factor", b"spread_factor", "sx126x_rx_boosted_gain", b"sx126x_rx_boosted_gain", "tx_enabled", b"tx_enabled", "tx_power", b"tx_power", "use_preset", b"use_preset"]) -> None: ...
def ClearField(self, field_name: typing.Literal["bandwidth", b"bandwidth", "channel_num", b"channel_num", "coding_rate", b"coding_rate", "config_ok_to_mqtt", b"config_ok_to_mqtt", "fem_lna_mode", b"fem_lna_mode", "frequency_offset", b"frequency_offset", "hop_limit", b"hop_limit", "ignore_incoming", b"ignore_incoming", "ignore_mqtt", b"ignore_mqtt", "modem_preset", b"modem_preset", "override_duty_cycle", b"override_duty_cycle", "override_frequency", b"override_frequency", "pa_fan_disabled", b"pa_fan_disabled", "region", b"region", "serial_hal_only", b"serial_hal_only", "spread_factor", b"spread_factor", "sx126x_rx_boosted_gain", b"sx126x_rx_boosted_gain", "tx_enabled", b"tx_enabled", "tx_power", b"tx_power", "use_preset", b"use_preset"]) -> None: ...
@typing.final
class BluetoothConfig(google.protobuf.message.Message):
@@ -1679,6 +2039,49 @@ class Config(google.protobuf.message.Message):
class SecurityConfig(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
class _PacketSignaturePolicy:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
class _PacketSignaturePolicyEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[Config.SecurityConfig._PacketSignaturePolicy.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
PACKET_SIGNATURE_POLICY_COMPATIBLE: Config.SecurityConfig._PacketSignaturePolicy.ValueType # 0
"""
Accept unsigned packets for maximum compatibility while still rejecting malformed or invalid signatures.
This is the default to avoid legacy nodes dropping signed packets during rebroadcast.
"""
PACKET_SIGNATURE_POLICY_BALANCED: Config.SecurityConfig._PacketSignaturePolicy.ValueType # 1
"""
Prefer authenticated packets while retaining compatibility with unsigned packets from nodes not known to sign.
Rejects unsigned, signable broadcasts from nodes that have previously signed.
"""
PACKET_SIGNATURE_POLICY_STRICT: Config.SecurityConfig._PacketSignaturePolicy.ValueType # 2
"""
Accept only packets authenticated by a verified XEdDSA signature or successful PKI decryption.
Unsigned, malformed, invalid, or unverifiable packets are ignored.
"""
class PacketSignaturePolicy(_PacketSignaturePolicy, metaclass=_PacketSignaturePolicyEnumTypeWrapper):
"""
Controls how the device authenticates remotely received mesh packets.
"""
PACKET_SIGNATURE_POLICY_COMPATIBLE: Config.SecurityConfig.PacketSignaturePolicy.ValueType # 0
"""
Accept unsigned packets for maximum compatibility while still rejecting malformed or invalid signatures.
This is the default to avoid legacy nodes dropping signed packets during rebroadcast.
"""
PACKET_SIGNATURE_POLICY_BALANCED: Config.SecurityConfig.PacketSignaturePolicy.ValueType # 1
"""
Prefer authenticated packets while retaining compatibility with unsigned packets from nodes not known to sign.
Rejects unsigned, signable broadcasts from nodes that have previously signed.
"""
PACKET_SIGNATURE_POLICY_STRICT: Config.SecurityConfig.PacketSignaturePolicy.ValueType # 2
"""
Accept only packets authenticated by a verified XEdDSA signature or successful PKI decryption.
Unsigned, malformed, invalid, or unverifiable packets are ignored.
"""
PUBLIC_KEY_FIELD_NUMBER: builtins.int
PRIVATE_KEY_FIELD_NUMBER: builtins.int
ADMIN_KEY_FIELD_NUMBER: builtins.int
@@ -1686,6 +2089,7 @@ class Config(google.protobuf.message.Message):
SERIAL_ENABLED_FIELD_NUMBER: builtins.int
DEBUG_LOG_API_ENABLED_FIELD_NUMBER: builtins.int
ADMIN_CHANNEL_ENABLED_FIELD_NUMBER: builtins.int
PACKET_SIGNATURE_POLICY_FIELD_NUMBER: builtins.int
public_key: builtins.bytes
"""
The public key of the user's device.
@@ -1714,6 +2118,10 @@ class Config(google.protobuf.message.Message):
"""
Allow incoming device control over the insecure legacy admin channel.
"""
packet_signature_policy: global___Config.SecurityConfig.PacketSignaturePolicy.ValueType
"""
Determines the packet signature policy applied to remotely received mesh packets.
"""
@property
def admin_key(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.bytes]:
"""
@@ -1730,8 +2138,9 @@ class Config(google.protobuf.message.Message):
serial_enabled: builtins.bool = ...,
debug_log_api_enabled: builtins.bool = ...,
admin_channel_enabled: builtins.bool = ...,
packet_signature_policy: global___Config.SecurityConfig.PacketSignaturePolicy.ValueType = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["admin_channel_enabled", b"admin_channel_enabled", "admin_key", b"admin_key", "debug_log_api_enabled", b"debug_log_api_enabled", "is_managed", b"is_managed", "private_key", b"private_key", "public_key", b"public_key", "serial_enabled", b"serial_enabled"]) -> None: ...
def ClearField(self, field_name: typing.Literal["admin_channel_enabled", b"admin_channel_enabled", "admin_key", b"admin_key", "debug_log_api_enabled", b"debug_log_api_enabled", "is_managed", b"is_managed", "packet_signature_policy", b"packet_signature_policy", "private_key", b"private_key", "public_key", b"public_key", "serial_enabled", b"serial_enabled"]) -> None: ...
@typing.final
class SessionkeyConfig(google.protobuf.message.Message):
+17 -14
View File
@@ -11,26 +11,29 @@ from google.protobuf.internal import builder as _builder
_sym_db = _symbol_database.Default()
from meshtastic.protobuf import nanopb_pb2 as meshtastic_dot_protobuf_dot_nanopb__pb2
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n+meshtastic/protobuf/connection_status.proto\x12\x13meshtastic.protobuf\"\xd5\x02\n\x16\x44\x65viceConnectionStatus\x12<\n\x04wifi\x18\x01 \x01(\x0b\x32).meshtastic.protobuf.WifiConnectionStatusH\x00\x88\x01\x01\x12\x44\n\x08\x65thernet\x18\x02 \x01(\x0b\x32-.meshtastic.protobuf.EthernetConnectionStatusH\x01\x88\x01\x01\x12\x46\n\tbluetooth\x18\x03 \x01(\x0b\x32..meshtastic.protobuf.BluetoothConnectionStatusH\x02\x88\x01\x01\x12@\n\x06serial\x18\x04 \x01(\x0b\x32+.meshtastic.protobuf.SerialConnectionStatusH\x03\x88\x01\x01\x42\x07\n\x05_wifiB\x0b\n\t_ethernetB\x0c\n\n_bluetoothB\t\n\x07_serial\"p\n\x14WifiConnectionStatus\x12<\n\x06status\x18\x01 \x01(\x0b\x32,.meshtastic.protobuf.NetworkConnectionStatus\x12\x0c\n\x04ssid\x18\x02 \x01(\t\x12\x0c\n\x04rssi\x18\x03 \x01(\x05\"X\n\x18\x45thernetConnectionStatus\x12<\n\x06status\x18\x01 \x01(\x0b\x32,.meshtastic.protobuf.NetworkConnectionStatus\"{\n\x17NetworkConnectionStatus\x12\x12\n\nip_address\x18\x01 \x01(\x07\x12\x14\n\x0cis_connected\x18\x02 \x01(\x08\x12\x19\n\x11is_mqtt_connected\x18\x03 \x01(\x08\x12\x1b\n\x13is_syslog_connected\x18\x04 \x01(\x08\"L\n\x19\x42luetoothConnectionStatus\x12\x0b\n\x03pin\x18\x01 \x01(\r\x12\x0c\n\x04rssi\x18\x02 \x01(\x05\x12\x14\n\x0cis_connected\x18\x03 \x01(\x08\"<\n\x16SerialConnectionStatus\x12\x0c\n\x04\x62\x61ud\x18\x01 \x01(\r\x12\x14\n\x0cis_connected\x18\x02 \x01(\x08\x42\x65\n\x13\x63om.geeksville.meshB\x10\x43onnStatusProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n+meshtastic/protobuf/connection_status.proto\x12\x13meshtastic.protobuf\x1a meshtastic/protobuf/nanopb.proto\"\xd5\x02\n\x16\x44\x65viceConnectionStatus\x12<\n\x04wifi\x18\x01 \x01(\x0b\x32).meshtastic.protobuf.WifiConnectionStatusH\x00\x88\x01\x01\x12\x44\n\x08\x65thernet\x18\x02 \x01(\x0b\x32-.meshtastic.protobuf.EthernetConnectionStatusH\x01\x88\x01\x01\x12\x46\n\tbluetooth\x18\x03 \x01(\x0b\x32..meshtastic.protobuf.BluetoothConnectionStatusH\x02\x88\x01\x01\x12@\n\x06serial\x18\x04 \x01(\x0b\x32+.meshtastic.protobuf.SerialConnectionStatusH\x03\x88\x01\x01\x42\x07\n\x05_wifiB\x0b\n\t_ethernetB\x0c\n\n_bluetoothB\t\n\x07_serial\"w\n\x14WifiConnectionStatus\x12<\n\x06status\x18\x01 \x01(\x0b\x32,.meshtastic.protobuf.NetworkConnectionStatus\x12\x13\n\x04ssid\x18\x02 \x01(\tB\x05\x92?\x02\x08!\x12\x0c\n\x04rssi\x18\x03 \x01(\x05\"X\n\x18\x45thernetConnectionStatus\x12<\n\x06status\x18\x01 \x01(\x0b\x32,.meshtastic.protobuf.NetworkConnectionStatus\"{\n\x17NetworkConnectionStatus\x12\x12\n\nip_address\x18\x01 \x01(\x07\x12\x14\n\x0cis_connected\x18\x02 \x01(\x08\x12\x19\n\x11is_mqtt_connected\x18\x03 \x01(\x08\x12\x1b\n\x13is_syslog_connected\x18\x04 \x01(\x08\"L\n\x19\x42luetoothConnectionStatus\x12\x0b\n\x03pin\x18\x01 \x01(\r\x12\x0c\n\x04rssi\x18\x02 \x01(\x05\x12\x14\n\x0cis_connected\x18\x03 \x01(\x08\"<\n\x16SerialConnectionStatus\x12\x0c\n\x04\x62\x61ud\x18\x01 \x01(\r\x12\x14\n\x0cis_connected\x18\x02 \x01(\x08\x42\x66\n\x14org.meshtastic.protoB\x10\x43onnStatusProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.connection_status_pb2', _globals)
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\020ConnStatusProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
_globals['_DEVICECONNECTIONSTATUS']._serialized_start=69
_globals['_DEVICECONNECTIONSTATUS']._serialized_end=410
_globals['_WIFICONNECTIONSTATUS']._serialized_start=412
_globals['_WIFICONNECTIONSTATUS']._serialized_end=524
_globals['_ETHERNETCONNECTIONSTATUS']._serialized_start=526
_globals['_ETHERNETCONNECTIONSTATUS']._serialized_end=614
_globals['_NETWORKCONNECTIONSTATUS']._serialized_start=616
_globals['_NETWORKCONNECTIONSTATUS']._serialized_end=739
_globals['_BLUETOOTHCONNECTIONSTATUS']._serialized_start=741
_globals['_BLUETOOTHCONNECTIONSTATUS']._serialized_end=817
_globals['_SERIALCONNECTIONSTATUS']._serialized_start=819
_globals['_SERIALCONNECTIONSTATUS']._serialized_end=879
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\020ConnStatusProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
_WIFICONNECTIONSTATUS.fields_by_name['ssid']._options = None
_WIFICONNECTIONSTATUS.fields_by_name['ssid']._serialized_options = b'\222?\002\010!'
_globals['_DEVICECONNECTIONSTATUS']._serialized_start=103
_globals['_DEVICECONNECTIONSTATUS']._serialized_end=444
_globals['_WIFICONNECTIONSTATUS']._serialized_start=446
_globals['_WIFICONNECTIONSTATUS']._serialized_end=565
_globals['_ETHERNETCONNECTIONSTATUS']._serialized_start=567
_globals['_ETHERNETCONNECTIONSTATUS']._serialized_end=655
_globals['_NETWORKCONNECTIONSTATUS']._serialized_start=657
_globals['_NETWORKCONNECTIONSTATUS']._serialized_end=780
_globals['_BLUETOOTHCONNECTIONSTATUS']._serialized_start=782
_globals['_BLUETOOTHCONNECTIONSTATUS']._serialized_end=858
_globals['_SERIALCONNECTIONSTATUS']._serialized_start=860
_globals['_SERIALCONNECTIONSTATUS']._serialized_end=920
# @@protoc_insertion_point(module_scope)
+45 -12
View File
@@ -11,24 +11,57 @@ from google.protobuf.internal import builder as _builder
_sym_db = _symbol_database.Default()
from meshtastic.protobuf import nanopb_pb2 as meshtastic_dot_protobuf_dot_nanopb__pb2
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n#meshtastic/protobuf/device_ui.proto\x12\x13meshtastic.protobuf\"\xbf\x03\n\x0e\x44\x65viceUIConfig\x12\x0f\n\x07version\x18\x01 \x01(\r\x12\x19\n\x11screen_brightness\x18\x02 \x01(\r\x12\x16\n\x0escreen_timeout\x18\x03 \x01(\r\x12\x13\n\x0bscreen_lock\x18\x04 \x01(\x08\x12\x15\n\rsettings_lock\x18\x05 \x01(\x08\x12\x10\n\x08pin_code\x18\x06 \x01(\r\x12)\n\x05theme\x18\x07 \x01(\x0e\x32\x1a.meshtastic.protobuf.Theme\x12\x15\n\ralert_enabled\x18\x08 \x01(\x08\x12\x16\n\x0e\x62\x61nner_enabled\x18\t \x01(\x08\x12\x14\n\x0cring_tone_id\x18\n \x01(\r\x12/\n\x08language\x18\x0b \x01(\x0e\x32\x1d.meshtastic.protobuf.Language\x12\x34\n\x0bnode_filter\x18\x0c \x01(\x0b\x32\x1f.meshtastic.protobuf.NodeFilter\x12:\n\x0enode_highlight\x18\r \x01(\x0b\x32\".meshtastic.protobuf.NodeHighlight\x12\x18\n\x10\x63\x61libration_data\x18\x0e \x01(\x0c\"\xa7\x01\n\nNodeFilter\x12\x16\n\x0eunknown_switch\x18\x01 \x01(\x08\x12\x16\n\x0eoffline_switch\x18\x02 \x01(\x08\x12\x19\n\x11public_key_switch\x18\x03 \x01(\x08\x12\x11\n\thops_away\x18\x04 \x01(\x05\x12\x17\n\x0fposition_switch\x18\x05 \x01(\x08\x12\x11\n\tnode_name\x18\x06 \x01(\t\x12\x0f\n\x07\x63hannel\x18\x07 \x01(\x05\"~\n\rNodeHighlight\x12\x13\n\x0b\x63hat_switch\x18\x01 \x01(\x08\x12\x17\n\x0fposition_switch\x18\x02 \x01(\x08\x12\x18\n\x10telemetry_switch\x18\x03 \x01(\x08\x12\x12\n\niaq_switch\x18\x04 \x01(\x08\x12\x11\n\tnode_name\x18\x05 \x01(\t*%\n\x05Theme\x12\x08\n\x04\x44\x41RK\x10\x00\x12\t\n\x05LIGHT\x10\x01\x12\x07\n\x03RED\x10\x02*\x8b\x02\n\x08Language\x12\x0b\n\x07\x45NGLISH\x10\x00\x12\n\n\x06\x46RENCH\x10\x01\x12\n\n\x06GERMAN\x10\x02\x12\x0b\n\x07ITALIAN\x10\x03\x12\x0e\n\nPORTUGUESE\x10\x04\x12\x0b\n\x07SPANISH\x10\x05\x12\x0b\n\x07SWEDISH\x10\x06\x12\x0b\n\x07\x46INNISH\x10\x07\x12\n\n\x06POLISH\x10\x08\x12\x0b\n\x07TURKISH\x10\t\x12\x0b\n\x07SERBIAN\x10\n\x12\x0b\n\x07RUSSIAN\x10\x0b\x12\t\n\x05\x44UTCH\x10\x0c\x12\t\n\x05GREEK\x10\r\x12\r\n\tNORWEGIAN\x10\x0e\x12\r\n\tSLOVENIAN\x10\x0f\x12\x16\n\x12SIMPLIFIED_CHINESE\x10\x1e\x12\x17\n\x13TRADITIONAL_CHINESE\x10\x1f\x42\x63\n\x13\x63om.geeksville.meshB\x0e\x44\x65viceUIProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n#meshtastic/protobuf/device_ui.proto\x12\x13meshtastic.protobuf\x1a meshtastic/protobuf/nanopb.proto\"\xa9\x06\n\x0e\x44\x65viceUIConfig\x12\x0f\n\x07version\x18\x01 \x01(\r\x12 \n\x11screen_brightness\x18\x02 \x01(\rB\x05\x92?\x02\x38\x08\x12\x1d\n\x0escreen_timeout\x18\x03 \x01(\rB\x05\x92?\x02\x38\x10\x12\x13\n\x0bscreen_lock\x18\x04 \x01(\x08\x12\x15\n\rsettings_lock\x18\x05 \x01(\x08\x12\x10\n\x08pin_code\x18\x06 \x01(\r\x12)\n\x05theme\x18\x07 \x01(\x0e\x32\x1a.meshtastic.protobuf.Theme\x12\x15\n\ralert_enabled\x18\x08 \x01(\x08\x12\x16\n\x0e\x62\x61nner_enabled\x18\t \x01(\x08\x12\x1b\n\x0cring_tone_id\x18\n \x01(\rB\x05\x92?\x02\x38\x08\x12/\n\x08language\x18\x0b \x01(\x0e\x32\x1d.meshtastic.protobuf.Language\x12\x34\n\x0bnode_filter\x18\x0c \x01(\x0b\x32\x1f.meshtastic.protobuf.NodeFilter\x12:\n\x0enode_highlight\x18\r \x01(\x0b\x32\".meshtastic.protobuf.NodeHighlight\x12\x1f\n\x10\x63\x61libration_data\x18\x0e \x01(\x0c\x42\x05\x92?\x02\x08\x10\x12*\n\x08map_data\x18\x0f \x01(\x0b\x32\x18.meshtastic.protobuf.Map\x12=\n\x0c\x63ompass_mode\x18\x10 \x01(\x0e\x32 .meshtastic.protobuf.CompassModeB\x05\x92?\x02\x38\x08\x12\x18\n\x10screen_rgb_color\x18\x11 \x01(\r\x12\x1b\n\x13is_clockface_analog\x18\x12 \x01(\x08\x12R\n\ngps_format\x18\x13 \x01(\x0e\x32\x37.meshtastic.protobuf.DeviceUIConfig.GpsCoordinateFormatB\x05\x92?\x02\x38\x08\"V\n\x13GpsCoordinateFormat\x12\x07\n\x03\x44\x45\x43\x10\x00\x12\x07\n\x03\x44MS\x10\x01\x12\x07\n\x03UTM\x10\x02\x12\x08\n\x04MGRS\x10\x03\x12\x07\n\x03OLC\x10\x04\x12\x08\n\x04OSGR\x10\x05\x12\x07\n\x03MLS\x10\x06\"\xbc\x01\n\nNodeFilter\x12\x16\n\x0eunknown_switch\x18\x01 \x01(\x08\x12\x16\n\x0eoffline_switch\x18\x02 \x01(\x08\x12\x19\n\x11public_key_switch\x18\x03 \x01(\x08\x12\x18\n\thops_away\x18\x04 \x01(\x05\x42\x05\x92?\x02\x38\x08\x12\x17\n\x0fposition_switch\x18\x05 \x01(\x08\x12\x18\n\tnode_name\x18\x06 \x01(\tB\x05\x92?\x02\x08\x10\x12\x16\n\x07\x63hannel\x18\x07 \x01(\x05\x42\x05\x92?\x02\x38\x08\"\x85\x01\n\rNodeHighlight\x12\x13\n\x0b\x63hat_switch\x18\x01 \x01(\x08\x12\x17\n\x0fposition_switch\x18\x02 \x01(\x08\x12\x18\n\x10telemetry_switch\x18\x03 \x01(\x08\x12\x12\n\niaq_switch\x18\x04 \x01(\x08\x12\x18\n\tnode_name\x18\x05 \x01(\tB\x05\x92?\x02\x08\x10\"D\n\x08GeoPoint\x12\x13\n\x04zoom\x18\x01 \x01(\x05\x42\x05\x92?\x02\x38\x08\x12\x10\n\x08latitude\x18\x02 \x01(\x05\x12\x11\n\tlongitude\x18\x03 \x01(\x05\"\\\n\x03Map\x12+\n\x04home\x18\x01 \x01(\x0b\x32\x1d.meshtastic.protobuf.GeoPoint\x12\x14\n\x05style\x18\x02 \x01(\tB\x05\x92?\x02\x08\x14\x12\x12\n\nfollow_gps\x18\x03 \x01(\x08*>\n\x0b\x43ompassMode\x12\x0b\n\x07\x44YNAMIC\x10\x00\x12\x0e\n\nFIXED_RING\x10\x01\x12\x12\n\x0e\x46REEZE_HEADING\x10\x02*%\n\x05Theme\x12\x08\n\x04\x44\x41RK\x10\x00\x12\t\n\x05LIGHT\x10\x01\x12\x07\n\x03RED\x10\x02*\xc0\x02\n\x08Language\x12\x0b\n\x07\x45NGLISH\x10\x00\x12\n\n\x06\x46RENCH\x10\x01\x12\n\n\x06GERMAN\x10\x02\x12\x0b\n\x07ITALIAN\x10\x03\x12\x0e\n\nPORTUGUESE\x10\x04\x12\x0b\n\x07SPANISH\x10\x05\x12\x0b\n\x07SWEDISH\x10\x06\x12\x0b\n\x07\x46INNISH\x10\x07\x12\n\n\x06POLISH\x10\x08\x12\x0b\n\x07TURKISH\x10\t\x12\x0b\n\x07SERBIAN\x10\n\x12\x0b\n\x07RUSSIAN\x10\x0b\x12\t\n\x05\x44UTCH\x10\x0c\x12\t\n\x05GREEK\x10\r\x12\r\n\tNORWEGIAN\x10\x0e\x12\r\n\tSLOVENIAN\x10\x0f\x12\r\n\tUKRAINIAN\x10\x10\x12\r\n\tBULGARIAN\x10\x11\x12\t\n\x05\x43ZECH\x10\x12\x12\n\n\x06\x44\x41NISH\x10\x13\x12\x16\n\x12SIMPLIFIED_CHINESE\x10\x1e\x12\x17\n\x13TRADITIONAL_CHINESE\x10\x1f\x42\x64\n\x14org.meshtastic.protoB\x0e\x44\x65viceUIProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.device_ui_pb2', _globals)
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\016DeviceUIProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
_globals['_THEME']._serialized_start=808
_globals['_THEME']._serialized_end=845
_globals['_LANGUAGE']._serialized_start=848
_globals['_LANGUAGE']._serialized_end=1115
_globals['_DEVICEUICONFIG']._serialized_start=61
_globals['_DEVICEUICONFIG']._serialized_end=508
_globals['_NODEFILTER']._serialized_start=511
_globals['_NODEFILTER']._serialized_end=678
_globals['_NODEHIGHLIGHT']._serialized_start=680
_globals['_NODEHIGHLIGHT']._serialized_end=806
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\016DeviceUIProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
_DEVICEUICONFIG.fields_by_name['screen_brightness']._options = None
_DEVICEUICONFIG.fields_by_name['screen_brightness']._serialized_options = b'\222?\0028\010'
_DEVICEUICONFIG.fields_by_name['screen_timeout']._options = None
_DEVICEUICONFIG.fields_by_name['screen_timeout']._serialized_options = b'\222?\0028\020'
_DEVICEUICONFIG.fields_by_name['ring_tone_id']._options = None
_DEVICEUICONFIG.fields_by_name['ring_tone_id']._serialized_options = b'\222?\0028\010'
_DEVICEUICONFIG.fields_by_name['calibration_data']._options = None
_DEVICEUICONFIG.fields_by_name['calibration_data']._serialized_options = b'\222?\002\010\020'
_DEVICEUICONFIG.fields_by_name['compass_mode']._options = None
_DEVICEUICONFIG.fields_by_name['compass_mode']._serialized_options = b'\222?\0028\010'
_DEVICEUICONFIG.fields_by_name['gps_format']._options = None
_DEVICEUICONFIG.fields_by_name['gps_format']._serialized_options = b'\222?\0028\010'
_NODEFILTER.fields_by_name['hops_away']._options = None
_NODEFILTER.fields_by_name['hops_away']._serialized_options = b'\222?\0028\010'
_NODEFILTER.fields_by_name['node_name']._options = None
_NODEFILTER.fields_by_name['node_name']._serialized_options = b'\222?\002\010\020'
_NODEFILTER.fields_by_name['channel']._options = None
_NODEFILTER.fields_by_name['channel']._serialized_options = b'\222?\0028\010'
_NODEHIGHLIGHT.fields_by_name['node_name']._options = None
_NODEHIGHLIGHT.fields_by_name['node_name']._serialized_options = b'\222?\002\010\020'
_GEOPOINT.fields_by_name['zoom']._options = None
_GEOPOINT.fields_by_name['zoom']._serialized_options = b'\222?\0028\010'
_MAP.fields_by_name['style']._options = None
_MAP.fields_by_name['style']._serialized_options = b'\222?\002\010\024'
_globals['_COMPASSMODE']._serialized_start=1397
_globals['_COMPASSMODE']._serialized_end=1459
_globals['_THEME']._serialized_start=1461
_globals['_THEME']._serialized_end=1498
_globals['_LANGUAGE']._serialized_start=1501
_globals['_LANGUAGE']._serialized_end=1821
_globals['_DEVICEUICONFIG']._serialized_start=95
_globals['_DEVICEUICONFIG']._serialized_end=904
_globals['_DEVICEUICONFIG_GPSCOORDINATEFORMAT']._serialized_start=818
_globals['_DEVICEUICONFIG_GPSCOORDINATEFORMAT']._serialized_end=904
_globals['_NODEFILTER']._serialized_start=907
_globals['_NODEFILTER']._serialized_end=1095
_globals['_NODEHIGHLIGHT']._serialized_start=1098
_globals['_NODEHIGHLIGHT']._serialized_end=1231
_globals['_GEOPOINT']._serialized_start=1233
_globals['_GEOPOINT']._serialized_end=1301
_globals['_MAP']._serialized_start=1303
_globals['_MAP']._serialized_end=1395
# @@protoc_insertion_point(module_scope)
+251 -2
View File
@@ -17,6 +17,41 @@ else:
DESCRIPTOR: google.protobuf.descriptor.FileDescriptor
class _CompassMode:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
class _CompassModeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_CompassMode.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
DYNAMIC: _CompassMode.ValueType # 0
"""
Compass with dynamic ring and heading
"""
FIXED_RING: _CompassMode.ValueType # 1
"""
Compass with fixed ring and heading
"""
FREEZE_HEADING: _CompassMode.ValueType # 2
"""
Compass with heading and freeze option
"""
class CompassMode(_CompassMode, metaclass=_CompassModeEnumTypeWrapper): ...
DYNAMIC: CompassMode.ValueType # 0
"""
Compass with dynamic ring and heading
"""
FIXED_RING: CompassMode.ValueType # 1
"""
Compass with fixed ring and heading
"""
FREEZE_HEADING: CompassMode.ValueType # 2
"""
Compass with heading and freeze option
"""
global___CompassMode = CompassMode
class _Theme:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
@@ -122,6 +157,22 @@ class _LanguageEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumT
"""
Slovenian
"""
UKRAINIAN: _Language.ValueType # 16
"""
Ukrainian
"""
BULGARIAN: _Language.ValueType # 17
"""
Bulgarian
"""
CZECH: _Language.ValueType # 18
"""
Czech
"""
DANISH: _Language.ValueType # 19
"""
Danish
"""
SIMPLIFIED_CHINESE: _Language.ValueType # 30
"""
Simplified Chinese (experimental)
@@ -200,6 +251,22 @@ SLOVENIAN: Language.ValueType # 15
"""
Slovenian
"""
UKRAINIAN: Language.ValueType # 16
"""
Ukrainian
"""
BULGARIAN: Language.ValueType # 17
"""
Bulgarian
"""
CZECH: Language.ValueType # 18
"""
Czech
"""
DANISH: Language.ValueType # 19
"""
Danish
"""
SIMPLIFIED_CHINESE: Language.ValueType # 30
"""
Simplified Chinese (experimental)
@@ -218,6 +285,91 @@ class DeviceUIConfig(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
class _GpsCoordinateFormat:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
class _GpsCoordinateFormatEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[DeviceUIConfig._GpsCoordinateFormat.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
DEC: DeviceUIConfig._GpsCoordinateFormat.ValueType # 0
"""
GPS coordinates are displayed in the normal decimal degrees format:
DD.DDDDDD DDD.DDDDDD
"""
DMS: DeviceUIConfig._GpsCoordinateFormat.ValueType # 1
"""
GPS coordinates are displayed in the degrees minutes seconds format:
DD°MM'SS"C DDD°MM'SS"C, where C is the compass point representing the locations quadrant
"""
UTM: DeviceUIConfig._GpsCoordinateFormat.ValueType # 2
"""
Universal Transverse Mercator format:
ZZB EEEEEE NNNNNNN, where Z is zone, B is band, E is easting, N is northing
"""
MGRS: DeviceUIConfig._GpsCoordinateFormat.ValueType # 3
"""
Military Grid Reference System format:
ZZB CD EEEEE NNNNN, where Z is zone, B is band, C is the east 100k square, D is the north 100k square,
E is easting, N is northing
"""
OLC: DeviceUIConfig._GpsCoordinateFormat.ValueType # 4
"""
Open Location Code (aka Plus Codes).
"""
OSGR: DeviceUIConfig._GpsCoordinateFormat.ValueType # 5
"""
Ordnance Survey Grid Reference (the National Grid System of the UK).
Format: AB EEEEE NNNNN, where A is the east 100k square, B is the north 100k square,
E is the easting, N is the northing
"""
MLS: DeviceUIConfig._GpsCoordinateFormat.ValueType # 6
"""
Maidenhead Locator System
Described here: https://en.wikipedia.org/wiki/Maidenhead_Locator_System
"""
class GpsCoordinateFormat(_GpsCoordinateFormat, metaclass=_GpsCoordinateFormatEnumTypeWrapper):
"""
How the GPS coordinates are displayed on the OLED screen.
"""
DEC: DeviceUIConfig.GpsCoordinateFormat.ValueType # 0
"""
GPS coordinates are displayed in the normal decimal degrees format:
DD.DDDDDD DDD.DDDDDD
"""
DMS: DeviceUIConfig.GpsCoordinateFormat.ValueType # 1
"""
GPS coordinates are displayed in the degrees minutes seconds format:
DD°MM'SS"C DDD°MM'SS"C, where C is the compass point representing the locations quadrant
"""
UTM: DeviceUIConfig.GpsCoordinateFormat.ValueType # 2
"""
Universal Transverse Mercator format:
ZZB EEEEEE NNNNNNN, where Z is zone, B is band, E is easting, N is northing
"""
MGRS: DeviceUIConfig.GpsCoordinateFormat.ValueType # 3
"""
Military Grid Reference System format:
ZZB CD EEEEE NNNNN, where Z is zone, B is band, C is the east 100k square, D is the north 100k square,
E is easting, N is northing
"""
OLC: DeviceUIConfig.GpsCoordinateFormat.ValueType # 4
"""
Open Location Code (aka Plus Codes).
"""
OSGR: DeviceUIConfig.GpsCoordinateFormat.ValueType # 5
"""
Ordnance Survey Grid Reference (the National Grid System of the UK).
Format: AB EEEEE NNNNN, where A is the east 100k square, B is the north 100k square,
E is the easting, N is the northing
"""
MLS: DeviceUIConfig.GpsCoordinateFormat.ValueType # 6
"""
Maidenhead Locator System
Described here: https://en.wikipedia.org/wiki/Maidenhead_Locator_System
"""
VERSION_FIELD_NUMBER: builtins.int
SCREEN_BRIGHTNESS_FIELD_NUMBER: builtins.int
SCREEN_TIMEOUT_FIELD_NUMBER: builtins.int
@@ -232,6 +384,11 @@ class DeviceUIConfig(google.protobuf.message.Message):
NODE_FILTER_FIELD_NUMBER: builtins.int
NODE_HIGHLIGHT_FIELD_NUMBER: builtins.int
CALIBRATION_DATA_FIELD_NUMBER: builtins.int
MAP_DATA_FIELD_NUMBER: builtins.int
COMPASS_MODE_FIELD_NUMBER: builtins.int
SCREEN_RGB_COLOR_FIELD_NUMBER: builtins.int
IS_CLOCKFACE_ANALOG_FIELD_NUMBER: builtins.int
GPS_FORMAT_FIELD_NUMBER: builtins.int
version: builtins.int
"""
A version integer used to invalidate saved files when we make incompatible changes.
@@ -268,6 +425,24 @@ class DeviceUIConfig(google.protobuf.message.Message):
"""
8 integers for screen calibration data
"""
compass_mode: global___CompassMode.ValueType
"""
Compass mode
"""
screen_rgb_color: builtins.int
"""
RGB color for BaseUI
0xRRGGBB format, e.g. 0xFF0000 for red
"""
is_clockface_analog: builtins.bool
"""
Clockface analog style
true for analog clockface, false for digital clockface
"""
gps_format: global___DeviceUIConfig.GpsCoordinateFormat.ValueType
"""
How the GPS coordinates are formatted on the OLED screen.
"""
@property
def node_filter(self) -> global___NodeFilter:
"""
@@ -280,6 +455,12 @@ class DeviceUIConfig(google.protobuf.message.Message):
Node list highlightening
"""
@property
def map_data(self) -> global___Map:
"""
Map related data
"""
def __init__(
self,
*,
@@ -297,9 +478,14 @@ class DeviceUIConfig(google.protobuf.message.Message):
node_filter: global___NodeFilter | None = ...,
node_highlight: global___NodeHighlight | None = ...,
calibration_data: builtins.bytes = ...,
map_data: global___Map | None = ...,
compass_mode: global___CompassMode.ValueType = ...,
screen_rgb_color: builtins.int = ...,
is_clockface_analog: builtins.bool = ...,
gps_format: global___DeviceUIConfig.GpsCoordinateFormat.ValueType = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["node_filter", b"node_filter", "node_highlight", b"node_highlight"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["alert_enabled", b"alert_enabled", "banner_enabled", b"banner_enabled", "calibration_data", b"calibration_data", "language", b"language", "node_filter", b"node_filter", "node_highlight", b"node_highlight", "pin_code", b"pin_code", "ring_tone_id", b"ring_tone_id", "screen_brightness", b"screen_brightness", "screen_lock", b"screen_lock", "screen_timeout", b"screen_timeout", "settings_lock", b"settings_lock", "theme", b"theme", "version", b"version"]) -> None: ...
def HasField(self, field_name: typing.Literal["map_data", b"map_data", "node_filter", b"node_filter", "node_highlight", b"node_highlight"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["alert_enabled", b"alert_enabled", "banner_enabled", b"banner_enabled", "calibration_data", b"calibration_data", "compass_mode", b"compass_mode", "gps_format", b"gps_format", "is_clockface_analog", b"is_clockface_analog", "language", b"language", "map_data", b"map_data", "node_filter", b"node_filter", "node_highlight", b"node_highlight", "pin_code", b"pin_code", "ring_tone_id", b"ring_tone_id", "screen_brightness", b"screen_brightness", "screen_lock", b"screen_lock", "screen_rgb_color", b"screen_rgb_color", "screen_timeout", b"screen_timeout", "settings_lock", b"settings_lock", "theme", b"theme", "version", b"version"]) -> None: ...
global___DeviceUIConfig = DeviceUIConfig
@@ -398,3 +584,66 @@ class NodeHighlight(google.protobuf.message.Message):
def ClearField(self, field_name: typing.Literal["chat_switch", b"chat_switch", "iaq_switch", b"iaq_switch", "node_name", b"node_name", "position_switch", b"position_switch", "telemetry_switch", b"telemetry_switch"]) -> None: ...
global___NodeHighlight = NodeHighlight
@typing.final
class GeoPoint(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
ZOOM_FIELD_NUMBER: builtins.int
LATITUDE_FIELD_NUMBER: builtins.int
LONGITUDE_FIELD_NUMBER: builtins.int
zoom: builtins.int
"""
Zoom level
"""
latitude: builtins.int
"""
Coordinate: latitude
"""
longitude: builtins.int
"""
Coordinate: longitude
"""
def __init__(
self,
*,
zoom: builtins.int = ...,
latitude: builtins.int = ...,
longitude: builtins.int = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["latitude", b"latitude", "longitude", b"longitude", "zoom", b"zoom"]) -> None: ...
global___GeoPoint = GeoPoint
@typing.final
class Map(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
HOME_FIELD_NUMBER: builtins.int
STYLE_FIELD_NUMBER: builtins.int
FOLLOW_GPS_FIELD_NUMBER: builtins.int
style: builtins.str
"""
Map tile style
"""
follow_gps: builtins.bool
"""
Map scroll follows GPS
"""
@property
def home(self) -> global___GeoPoint:
"""
Home coordinates
"""
def __init__(
self,
*,
home: global___GeoPoint | None = ...,
style: builtins.str = ...,
follow_gps: builtins.bool = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["home", b"home"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["follow_gps", b"follow_gps", "home", b"home", "style", b"style"]) -> None: ...
global___Map = Map
+39
View File
@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: meshtastic/protobuf/deviceonly_legacy.proto
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
from meshtastic.protobuf import deviceonly_pb2 as meshtastic_dot_protobuf_dot_deviceonly__pb2
from meshtastic.protobuf import telemetry_pb2 as meshtastic_dot_protobuf_dot_telemetry__pb2
from meshtastic.protobuf import nanopb_pb2 as meshtastic_dot_protobuf_dot_nanopb__pb2
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n+meshtastic/protobuf/deviceonly_legacy.proto\x12\x13meshtastic.protobuf\x1a$meshtastic/protobuf/deviceonly.proto\x1a#meshtastic/protobuf/telemetry.proto\x1a meshtastic/protobuf/nanopb.proto\"\x8c\x03\n\x13NodeInfoLite_Legacy\x12\x0b\n\x03num\x18\x01 \x01(\r\x12+\n\x04user\x18\x02 \x01(\x0b\x32\x1d.meshtastic.protobuf.UserLite\x12\x33\n\x08position\x18\x03 \x01(\x0b\x32!.meshtastic.protobuf.PositionLite\x12\x0b\n\x03snr\x18\x04 \x01(\x02\x12\x12\n\nlast_heard\x18\x05 \x01(\x07\x12:\n\x0e\x64\x65vice_metrics\x18\x06 \x01(\x0b\x32\".meshtastic.protobuf.DeviceMetrics\x12\x16\n\x07\x63hannel\x18\x07 \x01(\rB\x05\x92?\x02\x38\x08\x12\x10\n\x08via_mqtt\x18\x08 \x01(\x08\x12\x1d\n\thops_away\x18\t \x01(\rB\x05\x92?\x02\x38\x08H\x00\x88\x01\x01\x12\x13\n\x0bis_favorite\x18\n \x01(\x08\x12\x12\n\nis_ignored\x18\x0b \x01(\x08\x12\x17\n\x08next_hop\x18\x0c \x01(\rB\x05\x92?\x02\x38\x08\x12\x10\n\x08\x62itfield\x18\r \x01(\rB\x0c\n\n_hops_away\"\x92\x01\n\x13NodeDatabase_Legacy\x12\x0f\n\x07version\x18\x01 \x01(\r\x12j\n\x05nodes\x18\x02 \x03(\x0b\x32(.meshtastic.protobuf.NodeInfoLite_LegacyB1\x92?.\x92\x01+std::vector<meshtastic_NodeInfoLite_Legacy>Bt\n\x14org.meshtastic.protoB\x10\x44\x65viceOnlyLegacyZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x92?\x0b\xc2\x01\x08<vector>b\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.deviceonly_legacy_pb2', _globals)
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\020DeviceOnlyLegacyZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000\222?\013\302\001\010<vector>'
_NODEINFOLITE_LEGACY.fields_by_name['channel']._options = None
_NODEINFOLITE_LEGACY.fields_by_name['channel']._serialized_options = b'\222?\0028\010'
_NODEINFOLITE_LEGACY.fields_by_name['hops_away']._options = None
_NODEINFOLITE_LEGACY.fields_by_name['hops_away']._serialized_options = b'\222?\0028\010'
_NODEINFOLITE_LEGACY.fields_by_name['next_hop']._options = None
_NODEINFOLITE_LEGACY.fields_by_name['next_hop']._serialized_options = b'\222?\0028\010'
_NODEDATABASE_LEGACY.fields_by_name['nodes']._options = None
_NODEDATABASE_LEGACY.fields_by_name['nodes']._serialized_options = b'\222?.\222\001+std::vector<meshtastic_NodeInfoLite_Legacy>'
_globals['_NODEINFOLITE_LEGACY']._serialized_start=178
_globals['_NODEINFOLITE_LEGACY']._serialized_end=574
_globals['_NODEDATABASE_LEGACY']._serialized_start=577
_globals['_NODEDATABASE_LEGACY']._serialized_end=723
# @@protoc_insertion_point(module_scope)
+106
View File
@@ -0,0 +1,106 @@
"""
@generated by mypy-protobuf. Do not edit manually!
isort:skip_file
"""
import builtins
import collections.abc
import google.protobuf.descriptor
import google.protobuf.internal.containers
import google.protobuf.message
import meshtastic.protobuf.deviceonly_pb2
import meshtastic.protobuf.telemetry_pb2
import typing
DESCRIPTOR: google.protobuf.descriptor.FileDescriptor
@typing.final
class NodeInfoLite_Legacy(google.protobuf.message.Message):
"""
Legacy NodeInfoLite descriptor used only to decode pre-split
/prefs/nodes.proto saves during the v24 -> v25 migration boot.
This preserves the original NodeInfoLite-compatible field numbers needed
to parse old wire bytes cleanly, including user (2), position (3),
device_metrics (6), and the legacy-only compatibility fields via_mqtt (8),
is_favorite (10), and is_ignored (11). Steady-state code does not use
this struct; it is dropped after migration completes. This file should be
removed once DEVICESTATE_MIN_VER advances past 24.
"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
NUM_FIELD_NUMBER: builtins.int
USER_FIELD_NUMBER: builtins.int
POSITION_FIELD_NUMBER: builtins.int
SNR_FIELD_NUMBER: builtins.int
LAST_HEARD_FIELD_NUMBER: builtins.int
DEVICE_METRICS_FIELD_NUMBER: builtins.int
CHANNEL_FIELD_NUMBER: builtins.int
VIA_MQTT_FIELD_NUMBER: builtins.int
HOPS_AWAY_FIELD_NUMBER: builtins.int
IS_FAVORITE_FIELD_NUMBER: builtins.int
IS_IGNORED_FIELD_NUMBER: builtins.int
NEXT_HOP_FIELD_NUMBER: builtins.int
BITFIELD_FIELD_NUMBER: builtins.int
num: builtins.int
snr: builtins.float
last_heard: builtins.int
channel: builtins.int
via_mqtt: builtins.bool
hops_away: builtins.int
is_favorite: builtins.bool
is_ignored: builtins.bool
next_hop: builtins.int
bitfield: builtins.int
@property
def user(self) -> meshtastic.protobuf.deviceonly_pb2.UserLite: ...
@property
def position(self) -> meshtastic.protobuf.deviceonly_pb2.PositionLite: ...
@property
def device_metrics(self) -> meshtastic.protobuf.telemetry_pb2.DeviceMetrics: ...
def __init__(
self,
*,
num: builtins.int = ...,
user: meshtastic.protobuf.deviceonly_pb2.UserLite | None = ...,
position: meshtastic.protobuf.deviceonly_pb2.PositionLite | None = ...,
snr: builtins.float = ...,
last_heard: builtins.int = ...,
device_metrics: meshtastic.protobuf.telemetry_pb2.DeviceMetrics | None = ...,
channel: builtins.int = ...,
via_mqtt: builtins.bool = ...,
hops_away: builtins.int | None = ...,
is_favorite: builtins.bool = ...,
is_ignored: builtins.bool = ...,
next_hop: builtins.int = ...,
bitfield: builtins.int = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["_hops_away", b"_hops_away", "device_metrics", b"device_metrics", "hops_away", b"hops_away", "position", b"position", "user", b"user"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["_hops_away", b"_hops_away", "bitfield", b"bitfield", "channel", b"channel", "device_metrics", b"device_metrics", "hops_away", b"hops_away", "is_favorite", b"is_favorite", "is_ignored", b"is_ignored", "last_heard", b"last_heard", "next_hop", b"next_hop", "num", b"num", "position", b"position", "snr", b"snr", "user", b"user", "via_mqtt", b"via_mqtt"]) -> None: ...
def WhichOneof(self, oneof_group: typing.Literal["_hops_away", b"_hops_away"]) -> typing.Literal["hops_away"] | None: ...
global___NodeInfoLite_Legacy = NodeInfoLite_Legacy
@typing.final
class NodeDatabase_Legacy(google.protobuf.message.Message):
"""
Legacy NodeDatabase shape: one repeated array of fat NodeInfoLite_Legacy
with no satellite position/telemetry arrays.
"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
VERSION_FIELD_NUMBER: builtins.int
NODES_FIELD_NUMBER: builtins.int
version: builtins.int
@property
def nodes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NodeInfoLite_Legacy]: ...
def __init__(
self,
*,
version: builtins.int = ...,
nodes: collections.abc.Iterable[global___NodeInfoLite_Legacy] | None = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["nodes", b"nodes", "version", b"version"]) -> None: ...
global___NodeDatabase_Legacy = NodeDatabase_Legacy
+68 -17
View File
File diff suppressed because one or more lines are too long.
+265 -59
View File
@@ -10,6 +10,7 @@ import google.protobuf.internal.containers
import google.protobuf.message
import meshtastic.protobuf.channel_pb2
import meshtastic.protobuf.config_pb2
import meshtastic.protobuf.localonly_pb2
import meshtastic.protobuf.mesh_pb2
import meshtastic.protobuf.telemetry_pb2
import typing
@@ -29,6 +30,7 @@ class PositionLite(google.protobuf.message.Message):
ALTITUDE_FIELD_NUMBER: builtins.int
TIME_FIELD_NUMBER: builtins.int
LOCATION_SOURCE_FIELD_NUMBER: builtins.int
PRECISION_BITS_FIELD_NUMBER: builtins.int
latitude_i: builtins.int
"""
The new preferred location encoding, multiply by 1e-7 to get degrees
@@ -54,6 +56,10 @@ class PositionLite(google.protobuf.message.Message):
"""
TODO: REPLACE
"""
precision_bits: builtins.int
"""
Indicates the bits of precision set by the sending node
"""
def __init__(
self,
*,
@@ -62,8 +68,9 @@ class PositionLite(google.protobuf.message.Message):
altitude: builtins.int = ...,
time: builtins.int = ...,
location_source: meshtastic.protobuf.mesh_pb2.Position.LocSource.ValueType = ...,
precision_bits: builtins.int = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["altitude", b"altitude", "latitude_i", b"latitude_i", "location_source", b"location_source", "longitude_i", b"longitude_i", "time", b"time"]) -> None: ...
def ClearField(self, field_name: typing.Literal["altitude", b"altitude", "latitude_i", b"latitude_i", "location_source", b"location_source", "longitude_i", b"longitude_i", "precision_bits", b"precision_bits", "time", b"time"]) -> None: ...
global___PositionLite = PositionLite
@@ -78,6 +85,7 @@ class UserLite(google.protobuf.message.Message):
IS_LICENSED_FIELD_NUMBER: builtins.int
ROLE_FIELD_NUMBER: builtins.int
PUBLIC_KEY_FIELD_NUMBER: builtins.int
IS_UNMESSAGABLE_FIELD_NUMBER: builtins.int
macaddr: builtins.bytes
"""
This is the addr of the radio.
@@ -113,6 +121,10 @@ class UserLite(google.protobuf.message.Message):
The public key of the user's device.
This is sent out to other nodes on the mesh to allow them to compute a shared secret key.
"""
is_unmessagable: builtins.bool
"""
Whether or not the node can be messaged
"""
def __init__(
self,
*,
@@ -123,8 +135,11 @@ class UserLite(google.protobuf.message.Message):
is_licensed: builtins.bool = ...,
role: meshtastic.protobuf.config_pb2.Config.DeviceConfig.Role.ValueType = ...,
public_key: builtins.bytes = ...,
is_unmessagable: builtins.bool | None = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["hw_model", b"hw_model", "is_licensed", b"is_licensed", "long_name", b"long_name", "macaddr", b"macaddr", "public_key", b"public_key", "role", b"role", "short_name", b"short_name"]) -> None: ...
def HasField(self, field_name: typing.Literal["_is_unmessagable", b"_is_unmessagable", "is_unmessagable", b"is_unmessagable"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["_is_unmessagable", b"_is_unmessagable", "hw_model", b"hw_model", "is_licensed", b"is_licensed", "is_unmessagable", b"is_unmessagable", "long_name", b"long_name", "macaddr", b"macaddr", "public_key", b"public_key", "role", b"role", "short_name", b"short_name"]) -> None: ...
def WhichOneof(self, oneof_group: typing.Literal["_is_unmessagable", b"_is_unmessagable"]) -> typing.Literal["is_unmessagable"] | None: ...
global___UserLite = UserLite
@@ -133,25 +148,26 @@ class NodeInfoLite(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
NUM_FIELD_NUMBER: builtins.int
USER_FIELD_NUMBER: builtins.int
POSITION_FIELD_NUMBER: builtins.int
SNR_FIELD_NUMBER: builtins.int
LAST_HEARD_FIELD_NUMBER: builtins.int
DEVICE_METRICS_FIELD_NUMBER: builtins.int
CHANNEL_FIELD_NUMBER: builtins.int
VIA_MQTT_FIELD_NUMBER: builtins.int
HOPS_AWAY_FIELD_NUMBER: builtins.int
IS_FAVORITE_FIELD_NUMBER: builtins.int
IS_IGNORED_FIELD_NUMBER: builtins.int
NEXT_HOP_FIELD_NUMBER: builtins.int
BITFIELD_FIELD_NUMBER: builtins.int
LONG_NAME_FIELD_NUMBER: builtins.int
SHORT_NAME_FIELD_NUMBER: builtins.int
HW_MODEL_FIELD_NUMBER: builtins.int
ROLE_FIELD_NUMBER: builtins.int
PUBLIC_KEY_FIELD_NUMBER: builtins.int
SNR_Q4_FIELD_NUMBER: builtins.int
num: builtins.int
"""
The node number
"""
snr: builtins.float
"""
Returns the Signal-to-noise ratio (SNR) of the last received message,
as measured by the receiver. Return SNR of the last received message in dB
In-memory SNR of the last received message in dB. Not serialised directly:
always zeroed before encode; persisted as snr_q4 = 19 below.
"""
last_heard: builtins.int
"""
@@ -161,65 +177,69 @@ class NodeInfoLite(google.protobuf.message.Message):
"""
local channel index we heard that node on. Only populated if its not the default channel.
"""
via_mqtt: builtins.bool
"""
True if we witnessed the node over MQTT instead of LoRA transport
"""
hops_away: builtins.int
"""
Number of hops away from us this node is (0 if direct neighbor)
"""
is_favorite: builtins.bool
"""
True if node is in our favorites list
Persists between NodeDB internal clean ups
"""
is_ignored: builtins.bool
"""
True if node is in our ignored list
Persists between NodeDB internal clean ups
"""
next_hop: builtins.int
"""
Last byte of the node number of the node that should be used as the next hop to reach this node.
"""
@property
def user(self) -> global___UserLite:
"""
The user info for this node
"""
bitfield: builtins.int
"""
Bitfield for storing booleans. See NODEINFO_BITFIELD_* in src/mesh/NodeDB.h.
"""
long_name: builtins.str
"""Flattened user fields (formerly UserLite). macaddr dropped (deprecated 1.2.11).
@property
def position(self) -> global___PositionLite:
"""
This position data. Note: before 1.2.14 we would also store the last time we've heard from this node in position.time, that is no longer true.
Position.time now indicates the last time we received a POSITION from that node.
"""
@property
def device_metrics(self) -> meshtastic.protobuf.telemetry_pb2.DeviceMetrics:
"""
The latest device metrics for the node.
"""
A full name for this user, i.e. "Kevin Hester".
"""
short_name: builtins.str
"""
A VERY short name, ideally two characters or an emoji.
Suitable for a tiny OLED screen.
"""
hw_model: meshtastic.protobuf.mesh_pb2.HardwareModel.ValueType
"""
Hardware model the user's device is running.
"""
role: meshtastic.protobuf.config_pb2.Config.DeviceConfig.Role.ValueType
"""
The user's role in the mesh.
"""
public_key: builtins.bytes
"""
The public key of the user's device, for PKI-based encrypted DMs.
"""
snr_q4: builtins.int
"""
Q4-encoded SNR: dB × 4, sint32 zigzag. Matches RouteDiscovery convention.
Encode: snr_q4 = (int32_t)lroundf(snr * 4.0f). Decode: snr = snr_q4 / 4.0f.
float snr is always zeroed on disk; this field carries all persisted SNR.
A stored 0 does not by itself mean "unknown" here - see NODEINFO_BITFIELD_HAS_SNR in
src/mesh/NodeDB.h for the presence bit that disambiguates a genuine 0 dB reading from
"never measured".
"""
def __init__(
self,
*,
num: builtins.int = ...,
user: global___UserLite | None = ...,
position: global___PositionLite | None = ...,
snr: builtins.float = ...,
last_heard: builtins.int = ...,
device_metrics: meshtastic.protobuf.telemetry_pb2.DeviceMetrics | None = ...,
channel: builtins.int = ...,
via_mqtt: builtins.bool = ...,
hops_away: builtins.int | None = ...,
is_favorite: builtins.bool = ...,
is_ignored: builtins.bool = ...,
next_hop: builtins.int = ...,
bitfield: builtins.int = ...,
long_name: builtins.str = ...,
short_name: builtins.str = ...,
hw_model: meshtastic.protobuf.mesh_pb2.HardwareModel.ValueType = ...,
role: meshtastic.protobuf.config_pb2.Config.DeviceConfig.Role.ValueType = ...,
public_key: builtins.bytes = ...,
snr_q4: builtins.int = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["_hops_away", b"_hops_away", "device_metrics", b"device_metrics", "hops_away", b"hops_away", "position", b"position", "user", b"user"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["_hops_away", b"_hops_away", "channel", b"channel", "device_metrics", b"device_metrics", "hops_away", b"hops_away", "is_favorite", b"is_favorite", "is_ignored", b"is_ignored", "last_heard", b"last_heard", "next_hop", b"next_hop", "num", b"num", "position", b"position", "snr", b"snr", "user", b"user", "via_mqtt", b"via_mqtt"]) -> None: ...
def HasField(self, field_name: typing.Literal["_hops_away", b"_hops_away", "hops_away", b"hops_away"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["_hops_away", b"_hops_away", "bitfield", b"bitfield", "channel", b"channel", "hops_away", b"hops_away", "hw_model", b"hw_model", "last_heard", b"last_heard", "long_name", b"long_name", "next_hop", b"next_hop", "num", b"num", "public_key", b"public_key", "role", b"role", "short_name", b"short_name", "snr", b"snr", "snr_q4", b"snr_q4"]) -> None: ...
def WhichOneof(self, oneof_group: typing.Literal["_hops_away", b"_hops_away"]) -> typing.Literal["hops_away"] | None: ...
global___NodeInfoLite = NodeInfoLite
@@ -245,7 +265,6 @@ class DeviceState(google.protobuf.message.Message):
DID_GPS_RESET_FIELD_NUMBER: builtins.int
RX_WAYPOINT_FIELD_NUMBER: builtins.int
NODE_REMOTE_HARDWARE_PINS_FIELD_NUMBER: builtins.int
NODE_DB_LITE_FIELD_NUMBER: builtins.int
version: builtins.int
"""
A version integer used to invalidate old save files when we make
@@ -260,7 +279,8 @@ class DeviceState(google.protobuf.message.Message):
"""
did_gps_reset: builtins.bool
"""
Some GPS receivers seem to have bogus settings from the factory, so we always do one factory reset.
Previously used to manage GPS factory resets.
Deprecated in 2.5.23
"""
@property
def my_node(self) -> meshtastic.protobuf.mesh_pb2.MyNodeInfo:
@@ -302,12 +322,6 @@ class DeviceState(google.protobuf.message.Message):
The mesh's nodes with their available gpio pins for RemoteHardware module
"""
@property
def node_db_lite(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NodeInfoLite]:
"""
New lite version of NodeDB to decrease memory footprint
"""
def __init__(
self,
*,
@@ -320,13 +334,144 @@ class DeviceState(google.protobuf.message.Message):
did_gps_reset: builtins.bool = ...,
rx_waypoint: meshtastic.protobuf.mesh_pb2.MeshPacket | None = ...,
node_remote_hardware_pins: collections.abc.Iterable[meshtastic.protobuf.mesh_pb2.NodeRemoteHardwarePin] | None = ...,
node_db_lite: collections.abc.Iterable[global___NodeInfoLite] | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["my_node", b"my_node", "owner", b"owner", "rx_text_message", b"rx_text_message", "rx_waypoint", b"rx_waypoint"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["did_gps_reset", b"did_gps_reset", "my_node", b"my_node", "no_save", b"no_save", "node_db_lite", b"node_db_lite", "node_remote_hardware_pins", b"node_remote_hardware_pins", "owner", b"owner", "receive_queue", b"receive_queue", "rx_text_message", b"rx_text_message", "rx_waypoint", b"rx_waypoint", "version", b"version"]) -> None: ...
def ClearField(self, field_name: typing.Literal["did_gps_reset", b"did_gps_reset", "my_node", b"my_node", "no_save", b"no_save", "node_remote_hardware_pins", b"node_remote_hardware_pins", "owner", b"owner", "receive_queue", b"receive_queue", "rx_text_message", b"rx_text_message", "rx_waypoint", b"rx_waypoint", "version", b"version"]) -> None: ...
global___DeviceState = DeviceState
@typing.final
class NodePositionEntry(google.protobuf.message.Message):
"""Satellite per-node entries; stored alongside the slim NodeInfoLite so nodes
that never report don't pay the embedded cost.
"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
NUM_FIELD_NUMBER: builtins.int
POSITION_FIELD_NUMBER: builtins.int
num: builtins.int
@property
def position(self) -> global___PositionLite: ...
def __init__(
self,
*,
num: builtins.int = ...,
position: global___PositionLite | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["position", b"position"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["num", b"num", "position", b"position"]) -> None: ...
global___NodePositionEntry = NodePositionEntry
@typing.final
class NodeTelemetryEntry(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
NUM_FIELD_NUMBER: builtins.int
DEVICE_METRICS_FIELD_NUMBER: builtins.int
num: builtins.int
@property
def device_metrics(self) -> meshtastic.protobuf.telemetry_pb2.DeviceMetrics: ...
def __init__(
self,
*,
num: builtins.int = ...,
device_metrics: meshtastic.protobuf.telemetry_pb2.DeviceMetrics | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["device_metrics", b"device_metrics"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["device_metrics", b"device_metrics", "num", b"num"]) -> None: ...
global___NodeTelemetryEntry = NodeTelemetryEntry
@typing.final
class NodeEnvironmentEntry(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
NUM_FIELD_NUMBER: builtins.int
ENVIRONMENT_METRICS_FIELD_NUMBER: builtins.int
num: builtins.int
@property
def environment_metrics(self) -> meshtastic.protobuf.telemetry_pb2.EnvironmentMetrics: ...
def __init__(
self,
*,
num: builtins.int = ...,
environment_metrics: meshtastic.protobuf.telemetry_pb2.EnvironmentMetrics | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["environment_metrics", b"environment_metrics"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["environment_metrics", b"environment_metrics", "num", b"num"]) -> None: ...
global___NodeEnvironmentEntry = NodeEnvironmentEntry
@typing.final
class NodeStatusEntry(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
NUM_FIELD_NUMBER: builtins.int
STATUS_FIELD_NUMBER: builtins.int
num: builtins.int
@property
def status(self) -> meshtastic.protobuf.mesh_pb2.StatusMessage: ...
def __init__(
self,
*,
num: builtins.int = ...,
status: meshtastic.protobuf.mesh_pb2.StatusMessage | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["status", b"status"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["num", b"num", "status", b"status"]) -> None: ...
global___NodeStatusEntry = NodeStatusEntry
@typing.final
class NodeDatabase(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
VERSION_FIELD_NUMBER: builtins.int
NODES_FIELD_NUMBER: builtins.int
POSITIONS_FIELD_NUMBER: builtins.int
TELEMETRY_FIELD_NUMBER: builtins.int
STATUS_FIELD_NUMBER: builtins.int
ENVIRONMENT_FIELD_NUMBER: builtins.int
version: builtins.int
"""
A version integer used to invalidate old save files when we make
incompatible changes This integer is set at build time and is private to
NodeDB.cpp in the device code.
"""
@property
def nodes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NodeInfoLite]:
"""
New lite version of NodeDB to decrease memory footprint
"""
@property
def positions(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NodePositionEntry]:
"""Per-NodeNum satellite arrays. Constrained platforms (e.g. STM32WL) omit
these via MESHTASTIC_EXCLUDE_*DB build flags.
"""
@property
def telemetry(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NodeTelemetryEntry]: ...
@property
def status(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NodeStatusEntry]: ...
@property
def environment(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NodeEnvironmentEntry]: ...
def __init__(
self,
*,
version: builtins.int = ...,
nodes: collections.abc.Iterable[global___NodeInfoLite] | None = ...,
positions: collections.abc.Iterable[global___NodePositionEntry] | None = ...,
telemetry: collections.abc.Iterable[global___NodeTelemetryEntry] | None = ...,
status: collections.abc.Iterable[global___NodeStatusEntry] | None = ...,
environment: collections.abc.Iterable[global___NodeEnvironmentEntry] | None = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["environment", b"environment", "nodes", b"nodes", "positions", b"positions", "status", b"status", "telemetry", b"telemetry", "version", b"version"]) -> None: ...
global___NodeDatabase = NodeDatabase
@typing.final
class ChannelFile(google.protobuf.message.Message):
"""
@@ -358,3 +503,64 @@ class ChannelFile(google.protobuf.message.Message):
def ClearField(self, field_name: typing.Literal["channels", b"channels", "version", b"version"]) -> None: ...
global___ChannelFile = ChannelFile
@typing.final
class BackupPreferences(google.protobuf.message.Message):
"""
The on-disk backup of the node's preferences
"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
VERSION_FIELD_NUMBER: builtins.int
TIMESTAMP_FIELD_NUMBER: builtins.int
CONFIG_FIELD_NUMBER: builtins.int
MODULE_CONFIG_FIELD_NUMBER: builtins.int
CHANNELS_FIELD_NUMBER: builtins.int
OWNER_FIELD_NUMBER: builtins.int
version: builtins.int
"""
The version of the backup
"""
timestamp: builtins.int
"""
The timestamp of the backup (if node has time)
"""
@property
def config(self) -> meshtastic.protobuf.localonly_pb2.LocalConfig:
"""
The node's configuration
"""
@property
def module_config(self) -> meshtastic.protobuf.localonly_pb2.LocalModuleConfig:
"""
The node's module configuration
"""
@property
def channels(self) -> global___ChannelFile:
"""
The node's channels
"""
@property
def owner(self) -> meshtastic.protobuf.mesh_pb2.User:
"""
The node's user (owner) information
"""
def __init__(
self,
*,
version: builtins.int = ...,
timestamp: builtins.int = ...,
config: meshtastic.protobuf.localonly_pb2.LocalConfig | None = ...,
module_config: meshtastic.protobuf.localonly_pb2.LocalModuleConfig | None = ...,
channels: global___ChannelFile | None = ...,
owner: meshtastic.protobuf.mesh_pb2.User | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["channels", b"channels", "config", b"config", "module_config", b"module_config", "owner", b"owner"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["channels", b"channels", "config", b"config", "module_config", b"module_config", "owner", b"owner", "timestamp", b"timestamp", "version", b"version"]) -> None: ...
global___BackupPreferences = BackupPreferences
+73
View File
@@ -0,0 +1,73 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: meshtastic/protobuf/interdevice.proto
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
from meshtastic.protobuf import nanopb_pb2 as meshtastic_dot_protobuf_dot_nanopb__pb2
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n%meshtastic/protobuf/interdevice.proto\x12\x13meshtastic.protobuf\x1a meshtastic/protobuf/nanopb.proto\"\xf6\x01\n\x0c\x46ileTransfer\x12\x35\n\toperation\x18\x01 \x01(\x0e\x32\".meshtastic.protobuf.FileOperation\x12\x18\n\x08\x66ilepath\x18\x02 \x01(\tB\x06\x92?\x03\x08\x80\x02\x12\x18\n\x08\x66iledata\x18\x03 \x01(\x0c\x42\x06\x92?\x03\x08\x80 \x12/\n\x06status\x18\x04 \x01(\x0e\x32\x1f.meshtastic.protobuf.FileStatus\x12\x17\n\x07message\x18\x05 \x01(\tB\x06\x92?\x03\x08\xff\x01\x12\x0e\n\x06offset\x18\x06 \x01(\x04\x12\x0e\n\x06length\x18\x07 \x01(\r\x12\x11\n\tfile_size\x18\x08 \x01(\x04\"\xb9\x01\n\x10\x44irectoryListing\x12\x19\n\tdirectory\x18\x01 \x01(\tB\x06\x92?\x03\x08\x80\x02\x12\x1b\n\tfilenames\x18\x02 \x03(\tB\x08\x92?\x05\x10\x10p\xff\x01\x12/\n\x06status\x18\x03 \x01(\x0e\x32\x1f.meshtastic.protobuf.FileStatus\x12\x17\n\x07message\x18\x04 \x01(\tB\x06\x92?\x03\x08\xff\x01\x12\x0e\n\x06offset\x18\x05 \x01(\r\x12\x13\n\x0btotal_count\x18\x06 \x01(\r\"O\n\x0eI2CTransaction\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\r\x12\x1a\n\nwrite_data\x18\x02 \x01(\x0c\x42\x06\x92?\x03\x08\x80\x02\x12\x10\n\x08read_len\x18\x03 \x01(\r\"\x92\x03\n\nSdCardInfo\x12\x0f\n\x07present\x18\x01 \x01(\x08\x12;\n\tcard_type\x18\x02 \x01(\x0e\x32(.meshtastic.protobuf.SdCardInfo.CardType\x12\x39\n\x08\x66\x61t_type\x18\x03 \x01(\x0e\x32\'.meshtastic.protobuf.SdCardInfo.FatType\x12\x11\n\tcard_size\x18\x04 \x01(\x04\x12\x12\n\nused_bytes\x18\x05 \x01(\x04\x12\x12\n\nfree_bytes\x18\x06 \x01(\x04\x12\x13\n\x0bstats_valid\x18\x07 \x01(\x08\x12\x0c\n\x04\x62usy\x18\x08 \x01(\x08\x12\x13\n\x0bunformatted\x18\t \x01(\x08\"K\n\x08\x43\x61rdType\x12\x08\n\x04NONE\x10\x00\x12\x07\n\x03MMC\x10\x01\x12\x06\n\x02SD\x10\x02\x12\x08\n\x04SDHC\x10\x03\x12\x08\n\x04SDXC\x10\x04\x12\x10\n\x0cUNKNOWN_CARD\x10\x05\";\n\x07\x46\x61tType\x12\x0f\n\x0bUNKNOWN_FAT\x10\x00\x12\t\n\x05\x46\x41T16\x10\x01\x12\t\n\x05\x46\x41T32\x10\x02\x12\t\n\x05\x45XFAT\x10\x03\"\xac\x01\n\tI2CResult\x12\x35\n\x06status\x18\x01 \x01(\x0e\x32%.meshtastic.protobuf.I2CResult.Status\x12\x19\n\tread_data\x18\x02 \x01(\x0c\x42\x06\x92?\x03\x08\x80\x02\"M\n\x06Status\x12\x0f\n\x0bUNSPECIFIED\x10\x00\x12\x06\n\x02OK\x10\x01\x12\x10\n\x0cNACK_ADDRESS\x10\x02\x12\r\n\tNACK_DATA\x10\x03\x12\t\n\x05\x45RROR\x10\x04\"\x87\x05\n\x12InterdeviceMessage\x12\n\n\x02id\x18\x0f \x01(\r\x12\x16\n\x04nmea\x18\x01 \x01(\tB\x06\x92?\x03\x08\x80\x08H\x00\x12\x15\n\x04\x62\x65\x65p\x18\x02 \x01(\rB\x05\x92?\x02\x38\x10H\x00\x12>\n\x0fi2c_transaction\x18\x03 \x01(\x0b\x32#.meshtastic.protobuf.I2CTransactionH\x00\x12\x34\n\ni2c_result\x18\x04 \x01(\x0b\x32\x1e.meshtastic.protobuf.I2CResultH\x00\x12\x12\n\x08i2c_scan\x18\x05 \x01(\x08H\x00\x12!\n\x0fi2c_scan_result\x18\x06 \x01(\x0c\x42\x06\x92?\x03\x08\x80\x01H\x00\x12:\n\rfile_transfer\x18\x07 \x01(\x0b\x32!.meshtastic.protobuf.FileTransferH\x00\x12\x42\n\x11\x64irectory_listing\x18\x08 \x01(\x0b\x32%.meshtastic.protobuf.DirectoryListingH\x00\x12\x15\n\x0bget_sd_info\x18\t \x01(\x08H\x00\x12\x32\n\x07sd_info\x18\n \x01(\x0b\x32\x1f.meshtastic.protobuf.SdCardInfoH\x00\x12\x37\n\x04ping\x18\x0b \x01(\x0e\x32\'.meshtastic.protobuf.InterdeviceVersionH\x00\x12\x37\n\x04pong\x18\x0c \x01(\x0e\x32\'.meshtastic.protobuf.InterdeviceVersionH\x00\x12\x0e\n\x04nack\x18\r \x01(\x08H\x00\x12\x34\n\nsd_command\x18\x0e \x01(\x0e\x32\x1e.meshtastic.protobuf.SdCommandH\x00\x42\x06\n\x04\x64\x61ta*Z\n\x12InterdeviceVersion\x12#\n\x1fINTERDEVICE_VERSION_UNSPECIFIED\x10\x00\x12\x1f\n\x1bINTERDEVICE_VERSION_CURRENT\x10\x02*7\n\rFileOperation\x12\x07\n\x03GET\x10\x00\x12\x08\n\x04POST\x10\x01\x12\x07\n\x03PUT\x10\x02\x12\n\n\x06\x44\x45LETE\x10\x03*\xa6\x01\n\nFileStatus\x12\x14\n\x10\x46ILE_UNSPECIFIED\x10\x00\x12\x0b\n\x07\x46ILE_OK\x10\x01\x12\r\n\tFILE_BUSY\x10\x02\x12\x10\n\x0c\x46ILE_NO_CARD\x10\x03\x12\x12\n\x0e\x46ILE_NOT_FOUND\x10\x04\x12\x18\n\x14\x46ILE_OFFSET_CONFLICT\x10\x05\x12\x11\n\rFILE_IO_ERROR\x10\x06\x12\x13\n\x0f\x46ILE_NOT_A_FILE\x10\x07*R\n\tSdCommand\x12\x1a\n\x16SD_COMMAND_UNSPECIFIED\x10\x00\x12\x0c\n\x08SD_MOUNT\x10\x01\x12\x0c\n\x08SD_EJECT\x10\x02\x12\r\n\tSD_FORMAT\x10\x03\x42g\n\x14org.meshtastic.protoB\x11InterdeviceProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.interdevice_pb2', _globals)
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\021InterdeviceProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
_FILETRANSFER.fields_by_name['filepath']._options = None
_FILETRANSFER.fields_by_name['filepath']._serialized_options = b'\222?\003\010\200\002'
_FILETRANSFER.fields_by_name['filedata']._options = None
_FILETRANSFER.fields_by_name['filedata']._serialized_options = b'\222?\003\010\200 '
_FILETRANSFER.fields_by_name['message']._options = None
_FILETRANSFER.fields_by_name['message']._serialized_options = b'\222?\003\010\377\001'
_DIRECTORYLISTING.fields_by_name['directory']._options = None
_DIRECTORYLISTING.fields_by_name['directory']._serialized_options = b'\222?\003\010\200\002'
_DIRECTORYLISTING.fields_by_name['filenames']._options = None
_DIRECTORYLISTING.fields_by_name['filenames']._serialized_options = b'\222?\005\020\020p\377\001'
_DIRECTORYLISTING.fields_by_name['message']._options = None
_DIRECTORYLISTING.fields_by_name['message']._serialized_options = b'\222?\003\010\377\001'
_I2CTRANSACTION.fields_by_name['write_data']._options = None
_I2CTRANSACTION.fields_by_name['write_data']._serialized_options = b'\222?\003\010\200\002'
_I2CRESULT.fields_by_name['read_data']._options = None
_I2CRESULT.fields_by_name['read_data']._serialized_options = b'\222?\003\010\200\002'
_INTERDEVICEMESSAGE.fields_by_name['nmea']._options = None
_INTERDEVICEMESSAGE.fields_by_name['nmea']._serialized_options = b'\222?\003\010\200\010'
_INTERDEVICEMESSAGE.fields_by_name['beep']._options = None
_INTERDEVICEMESSAGE.fields_by_name['beep']._serialized_options = b'\222?\0028\020'
_INTERDEVICEMESSAGE.fields_by_name['i2c_scan_result']._options = None
_INTERDEVICEMESSAGE.fields_by_name['i2c_scan_result']._serialized_options = b'\222?\003\010\200\001'
_globals['_INTERDEVICEVERSION']._serialized_start=1844
_globals['_INTERDEVICEVERSION']._serialized_end=1934
_globals['_FILEOPERATION']._serialized_start=1936
_globals['_FILEOPERATION']._serialized_end=1991
_globals['_FILESTATUS']._serialized_start=1994
_globals['_FILESTATUS']._serialized_end=2160
_globals['_SDCOMMAND']._serialized_start=2162
_globals['_SDCOMMAND']._serialized_end=2244
_globals['_FILETRANSFER']._serialized_start=97
_globals['_FILETRANSFER']._serialized_end=343
_globals['_DIRECTORYLISTING']._serialized_start=346
_globals['_DIRECTORYLISTING']._serialized_end=531
_globals['_I2CTRANSACTION']._serialized_start=533
_globals['_I2CTRANSACTION']._serialized_end=612
_globals['_SDCARDINFO']._serialized_start=615
_globals['_SDCARDINFO']._serialized_end=1017
_globals['_SDCARDINFO_CARDTYPE']._serialized_start=881
_globals['_SDCARDINFO_CARDTYPE']._serialized_end=956
_globals['_SDCARDINFO_FATTYPE']._serialized_start=958
_globals['_SDCARDINFO_FATTYPE']._serialized_end=1017
_globals['_I2CRESULT']._serialized_start=1020
_globals['_I2CRESULT']._serialized_end=1192
_globals['_I2CRESULT_STATUS']._serialized_start=1115
_globals['_I2CRESULT_STATUS']._serialized_end=1192
_globals['_INTERDEVICEMESSAGE']._serialized_start=1195
_globals['_INTERDEVICEMESSAGE']._serialized_end=1842
# @@protoc_insertion_point(module_scope)
+494
View File
@@ -0,0 +1,494 @@
"""
@generated by mypy-protobuf. Do not edit manually!
isort:skip_file
"""
import builtins
import collections.abc
import google.protobuf.descriptor
import google.protobuf.internal.containers
import google.protobuf.internal.enum_type_wrapper
import google.protobuf.message
import sys
import typing
if sys.version_info >= (3, 10):
import typing as typing_extensions
else:
import typing_extensions
DESCRIPTOR: google.protobuf.descriptor.FileDescriptor
class _InterdeviceVersion:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
class _InterdeviceVersionEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_InterdeviceVersion.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
INTERDEVICE_VERSION_UNSPECIFIED: _InterdeviceVersion.ValueType # 0
INTERDEVICE_VERSION_CURRENT: _InterdeviceVersion.ValueType # 2
"""Never use 1: ping/pong were bools before the handshake existed, and a
bool true is the same varint on the wire as the number 1, so firmware
predating the handshake would pass it.
"""
class InterdeviceVersion(_InterdeviceVersion, metaclass=_InterdeviceVersionEnumTypeWrapper):
"""Version of the interdevice protocol spoken on the link. Both sides send
theirs in the ping/pong handshake; a peer reporting a different one runs
firmware that does not match and is not talked to.
On a change that breaks the other side (renumbered fields, changed
semantics, removed messages), raise the value of CURRENT. Do not add
another entry: this enum carries a single constant, not a history.
"""
INTERDEVICE_VERSION_UNSPECIFIED: InterdeviceVersion.ValueType # 0
INTERDEVICE_VERSION_CURRENT: InterdeviceVersion.ValueType # 2
"""Never use 1: ping/pong were bools before the handshake existed, and a
bool true is the same varint on the wire as the number 1, so firmware
predating the handshake would pass it.
"""
global___InterdeviceVersion = InterdeviceVersion
class _FileOperation:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
class _FileOperationEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_FileOperation.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
GET: _FileOperation.ValueType # 0
POST: _FileOperation.ValueType # 1
PUT: _FileOperation.ValueType # 2
DELETE: _FileOperation.ValueType # 3
class FileOperation(_FileOperation, metaclass=_FileOperationEnumTypeWrapper):
"""Defines the supported file operations"""
GET: FileOperation.ValueType # 0
POST: FileOperation.ValueType # 1
PUT: FileOperation.ValueType # 2
DELETE: FileOperation.ValueType # 3
global___FileOperation = FileOperation
class _FileStatus:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
class _FileStatusEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_FileStatus.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
FILE_UNSPECIFIED: _FileStatus.ValueType # 0
FILE_OK: _FileStatus.ValueType # 1
FILE_BUSY: _FileStatus.ValueType # 2
"""Retry later: the co-processor is doing card maintenance (mount,
free space scan) and cannot serve the request right now
"""
FILE_NO_CARD: _FileStatus.ValueType # 3
FILE_NOT_FOUND: _FileStatus.ValueType # 4
FILE_OFFSET_CONFLICT: _FileStatus.ValueType # 5
"""PUT only: offset did not match the current end of the file. file_size
carries the size the file actually has, so the writer can resync (or
recognize its own chunk as already written after a lost response).
"""
FILE_IO_ERROR: _FileStatus.ValueType # 6
FILE_NOT_A_FILE: _FileStatus.ValueType # 7
"""path is a directory (GET) or not one (listing)"""
class FileStatus(_FileStatus, metaclass=_FileStatusEnumTypeWrapper):
"""Outcome of a file or directory operation. The requester must be able to
tell a transient condition from a definitive one: BUSY is worth another
try, NOT_FOUND is not.
"""
FILE_UNSPECIFIED: FileStatus.ValueType # 0
FILE_OK: FileStatus.ValueType # 1
FILE_BUSY: FileStatus.ValueType # 2
"""Retry later: the co-processor is doing card maintenance (mount,
free space scan) and cannot serve the request right now
"""
FILE_NO_CARD: FileStatus.ValueType # 3
FILE_NOT_FOUND: FileStatus.ValueType # 4
FILE_OFFSET_CONFLICT: FileStatus.ValueType # 5
"""PUT only: offset did not match the current end of the file. file_size
carries the size the file actually has, so the writer can resync (or
recognize its own chunk as already written after a lost response).
"""
FILE_IO_ERROR: FileStatus.ValueType # 6
FILE_NOT_A_FILE: FileStatus.ValueType # 7
"""path is a directory (GET) or not one (listing)"""
global___FileStatus = FileStatus
class _SdCommand:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
class _SdCommandEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_SdCommand.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
SD_COMMAND_UNSPECIFIED: _SdCommand.ValueType # 0
SD_MOUNT: _SdCommand.ValueType # 1
"""mount a card that is in the slot, also after an eject"""
SD_EJECT: _SdCommand.ValueType # 2
"""flush and release the card so it can be pulled safely"""
SD_FORMAT: _SdCommand.ValueType # 3
"""wipe the card and put a fresh FAT on it, then mount it"""
class SdCommand(_SdCommand, metaclass=_SdCommandEnumTypeWrapper):
"""What to do with the SD card of the co-processor"""
SD_COMMAND_UNSPECIFIED: SdCommand.ValueType # 0
SD_MOUNT: SdCommand.ValueType # 1
"""mount a card that is in the slot, also after an eject"""
SD_EJECT: SdCommand.ValueType # 2
"""flush and release the card so it can be pulled safely"""
SD_FORMAT: SdCommand.ValueType # 3
"""wipe the card and put a fresh FAT on it, then mount it"""
global___SdCommand = SdCommand
@typing.final
class FileTransfer(google.protobuf.message.Message):
"""Message for file operations"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
OPERATION_FIELD_NUMBER: builtins.int
FILEPATH_FIELD_NUMBER: builtins.int
FILEDATA_FIELD_NUMBER: builtins.int
STATUS_FIELD_NUMBER: builtins.int
MESSAGE_FIELD_NUMBER: builtins.int
OFFSET_FIELD_NUMBER: builtins.int
LENGTH_FIELD_NUMBER: builtins.int
FILE_SIZE_FIELD_NUMBER: builtins.int
operation: global___FileOperation.ValueType
"""File operation (GET, POST, PUT, DELETE)"""
filepath: builtins.str
filedata: builtins.bytes
status: global___FileStatus.ValueType
"""Response: outcome of the operation"""
message: builtins.str
offset: builtins.int
"""Byte offset of this chunk within the file (ranged GET/PUT)"""
length: builtins.int
"""GET request: number of bytes to read, 0 = max chunk size. A response
carries at most the filedata max_size (see interdevice.options) per
chunk; larger requests are truncated, visible in the filedata length.
"""
file_size: builtins.int
"""GET response: total size of the file"""
def __init__(
self,
*,
operation: global___FileOperation.ValueType = ...,
filepath: builtins.str = ...,
filedata: builtins.bytes = ...,
status: global___FileStatus.ValueType = ...,
message: builtins.str = ...,
offset: builtins.int = ...,
length: builtins.int = ...,
file_size: builtins.int = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["file_size", b"file_size", "filedata", b"filedata", "filepath", b"filepath", "length", b"length", "message", b"message", "offset", b"offset", "operation", b"operation", "status", b"status"]) -> None: ...
global___FileTransfer = FileTransfer
@typing.final
class DirectoryListing(google.protobuf.message.Message):
"""Message for structured directory listing"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
DIRECTORY_FIELD_NUMBER: builtins.int
FILENAMES_FIELD_NUMBER: builtins.int
STATUS_FIELD_NUMBER: builtins.int
MESSAGE_FIELD_NUMBER: builtins.int
OFFSET_FIELD_NUMBER: builtins.int
TOTAL_COUNT_FIELD_NUMBER: builtins.int
directory: builtins.str
status: global___FileStatus.ValueType
"""Response: outcome of the operation"""
message: builtins.str
offset: builtins.int
"""Request: skip this many entries (paging)"""
total_count: builtins.int
"""Response: total number of entries in the directory"""
@property
def filenames(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]:
"""One page of entry names, full FAT LFN length. Subdirectories carry a
trailing slash. Note that a name whose directory prefix pushes the
combined path past the FileTransfer.filepath limit cannot round-trip.
Page size is the max_count in interdevice.options; page through with
offset and total_count.
"""
def __init__(
self,
*,
directory: builtins.str = ...,
filenames: collections.abc.Iterable[builtins.str] | None = ...,
status: global___FileStatus.ValueType = ...,
message: builtins.str = ...,
offset: builtins.int = ...,
total_count: builtins.int = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["directory", b"directory", "filenames", b"filenames", "message", b"message", "offset", b"offset", "status", b"status", "total_count", b"total_count"]) -> None: ...
global___DirectoryListing = DirectoryListing
@typing.final
class I2CTransaction(google.protobuf.message.Message):
"""A single I2C transaction: an optional write followed by an optional
read with repeated start, matching the TwoWire usage of sensor drivers
(beginTransmission/write.../endTransmission(false)/requestFrom)
"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
ADDRESS_FIELD_NUMBER: builtins.int
WRITE_DATA_FIELD_NUMBER: builtins.int
READ_LEN_FIELD_NUMBER: builtins.int
address: builtins.int
"""7-bit device address"""
write_data: builtins.bytes
read_len: builtins.int
"""Number of bytes to read after the write, 0 = write-only. Bounded by
the read_data max_size of I2CResult (see interdevice.options); larger
requests are truncated, visible in the returned byte count.
"""
def __init__(
self,
*,
address: builtins.int = ...,
write_data: builtins.bytes = ...,
read_len: builtins.int = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["address", b"address", "read_len", b"read_len", "write_data", b"write_data"]) -> None: ...
global___I2CTransaction = I2CTransaction
@typing.final
class SdCardInfo(google.protobuf.message.Message):
"""SD card statistics"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
class _CardType:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
class _CardTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[SdCardInfo._CardType.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
NONE: SdCardInfo._CardType.ValueType # 0
MMC: SdCardInfo._CardType.ValueType # 1
SD: SdCardInfo._CardType.ValueType # 2
SDHC: SdCardInfo._CardType.ValueType # 3
SDXC: SdCardInfo._CardType.ValueType # 4
UNKNOWN_CARD: SdCardInfo._CardType.ValueType # 5
class CardType(_CardType, metaclass=_CardTypeEnumTypeWrapper): ...
NONE: SdCardInfo.CardType.ValueType # 0
MMC: SdCardInfo.CardType.ValueType # 1
SD: SdCardInfo.CardType.ValueType # 2
SDHC: SdCardInfo.CardType.ValueType # 3
SDXC: SdCardInfo.CardType.ValueType # 4
UNKNOWN_CARD: SdCardInfo.CardType.ValueType # 5
class _FatType:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
class _FatTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[SdCardInfo._FatType.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
UNKNOWN_FAT: SdCardInfo._FatType.ValueType # 0
FAT16: SdCardInfo._FatType.ValueType # 1
FAT32: SdCardInfo._FatType.ValueType # 2
EXFAT: SdCardInfo._FatType.ValueType # 3
class FatType(_FatType, metaclass=_FatTypeEnumTypeWrapper): ...
UNKNOWN_FAT: SdCardInfo.FatType.ValueType # 0
FAT16: SdCardInfo.FatType.ValueType # 1
FAT32: SdCardInfo.FatType.ValueType # 2
EXFAT: SdCardInfo.FatType.ValueType # 3
PRESENT_FIELD_NUMBER: builtins.int
CARD_TYPE_FIELD_NUMBER: builtins.int
FAT_TYPE_FIELD_NUMBER: builtins.int
CARD_SIZE_FIELD_NUMBER: builtins.int
USED_BYTES_FIELD_NUMBER: builtins.int
FREE_BYTES_FIELD_NUMBER: builtins.int
STATS_VALID_FIELD_NUMBER: builtins.int
BUSY_FIELD_NUMBER: builtins.int
UNFORMATTED_FIELD_NUMBER: builtins.int
present: builtins.bool
"""Card initialized and usable. False while `busy` is set does not mean
there is no card: the co-processor does not know yet.
"""
card_type: global___SdCardInfo.CardType.ValueType
fat_type: global___SdCardInfo.FatType.ValueType
card_size: builtins.int
"""Filesystem size in bytes"""
used_bytes: builtins.int
"""Used bytes (may be expensive to compute on FAT32)"""
free_bytes: builtins.int
"""Free bytes"""
stats_valid: builtins.bool
"""used_bytes/free_bytes are only meaningful when true: the scan behind
them runs in the background after mount and can take a while, and a
full card is otherwise indistinguishable from a scan in progress
"""
busy: builtins.bool
"""The co-processor is mounting a card right now, so whether one is
present is not decided yet. Ask again rather than concluding the slot
is empty.
"""
unformatted: builtins.bool
"""A card answers in the slot but carries no filesystem that could be
mounted (present is false then). Formatting it makes it usable.
"""
def __init__(
self,
*,
present: builtins.bool = ...,
card_type: global___SdCardInfo.CardType.ValueType = ...,
fat_type: global___SdCardInfo.FatType.ValueType = ...,
card_size: builtins.int = ...,
used_bytes: builtins.int = ...,
free_bytes: builtins.int = ...,
stats_valid: builtins.bool = ...,
busy: builtins.bool = ...,
unformatted: builtins.bool = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["busy", b"busy", "card_size", b"card_size", "card_type", b"card_type", "fat_type", b"fat_type", "free_bytes", b"free_bytes", "present", b"present", "stats_valid", b"stats_valid", "unformatted", b"unformatted", "used_bytes", b"used_bytes"]) -> None: ...
global___SdCardInfo = SdCardInfo
@typing.final
class I2CResult(google.protobuf.message.Message):
"""Result of an I2CTransaction"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
class _Status:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
class _StatusEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[I2CResult._Status.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
UNSPECIFIED: I2CResult._Status.ValueType # 0
"""Never sent: an all-defaults (e.g. accidentally empty) message must
not decode as a successful transaction
"""
OK: I2CResult._Status.ValueType # 1
NACK_ADDRESS: I2CResult._Status.ValueType # 2
NACK_DATA: I2CResult._Status.ValueType # 3
ERROR: I2CResult._Status.ValueType # 4
class Status(_Status, metaclass=_StatusEnumTypeWrapper): ...
UNSPECIFIED: I2CResult.Status.ValueType # 0
"""Never sent: an all-defaults (e.g. accidentally empty) message must
not decode as a successful transaction
"""
OK: I2CResult.Status.ValueType # 1
NACK_ADDRESS: I2CResult.Status.ValueType # 2
NACK_DATA: I2CResult.Status.ValueType # 3
ERROR: I2CResult.Status.ValueType # 4
STATUS_FIELD_NUMBER: builtins.int
READ_DATA_FIELD_NUMBER: builtins.int
status: global___I2CResult.Status.ValueType
read_data: builtins.bytes
def __init__(
self,
*,
status: global___I2CResult.Status.ValueType = ...,
read_data: builtins.bytes = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["read_data", b"read_data", "status", b"status"]) -> None: ...
global___I2CResult = I2CResult
@typing.final
class InterdeviceMessage(google.protobuf.message.Message):
"""Main message for interdevice communication"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
ID_FIELD_NUMBER: builtins.int
NMEA_FIELD_NUMBER: builtins.int
BEEP_FIELD_NUMBER: builtins.int
I2C_TRANSACTION_FIELD_NUMBER: builtins.int
I2C_RESULT_FIELD_NUMBER: builtins.int
I2C_SCAN_FIELD_NUMBER: builtins.int
I2C_SCAN_RESULT_FIELD_NUMBER: builtins.int
FILE_TRANSFER_FIELD_NUMBER: builtins.int
DIRECTORY_LISTING_FIELD_NUMBER: builtins.int
GET_SD_INFO_FIELD_NUMBER: builtins.int
SD_INFO_FIELD_NUMBER: builtins.int
PING_FIELD_NUMBER: builtins.int
PONG_FIELD_NUMBER: builtins.int
NACK_FIELD_NUMBER: builtins.int
SD_COMMAND_FIELD_NUMBER: builtins.int
id: builtins.int
"""Correlates a response with its request: responses echo the id of the
request they answer. 0 for unsolicited messages (e.g. the nmea stream).
"""
nmea: builtins.str
beep: builtins.int
i2c_scan: builtins.bool
"""Request: scan the secondary I2C bus"""
i2c_scan_result: builtins.bytes
get_sd_info: builtins.bool
"""Request: SD card statistics"""
ping: global___InterdeviceVersion.ValueType
"""Link liveness probe and version handshake. The receiver answers ping
with pong, echoing the id. Touches no peripherals, so it works with
nothing attached. Both carry the version the sender speaks; a peer
that answers with a different one speaks another protocol and must
not be used.
"""
pong: global___InterdeviceVersion.ValueType
nack: builtins.bool
"""Response: the request could not be decoded or is of an unhandled
type, so the requester fails fast instead of burning its timeout.
Echoes the id when known, 0 when the frame was undecodable. Never
sent in reaction to a nack.
"""
sd_command: global___SdCommand.ValueType
"""Request: mount the card, or release it so it can be pulled safely. The
co-processor answers with sd_info. Without an eject the card is mounted
on its own and kept mounted; after one it stays released until a mount
is asked for.
"""
@property
def i2c_transaction(self) -> global___I2CTransaction: ...
@property
def i2c_result(self) -> global___I2CResult: ...
@property
def file_transfer(self) -> global___FileTransfer: ...
@property
def directory_listing(self) -> global___DirectoryListing: ...
@property
def sd_info(self) -> global___SdCardInfo:
"""Response"""
def __init__(
self,
*,
id: builtins.int = ...,
nmea: builtins.str = ...,
beep: builtins.int = ...,
i2c_transaction: global___I2CTransaction | None = ...,
i2c_result: global___I2CResult | None = ...,
i2c_scan: builtins.bool = ...,
i2c_scan_result: builtins.bytes = ...,
file_transfer: global___FileTransfer | None = ...,
directory_listing: global___DirectoryListing | None = ...,
get_sd_info: builtins.bool = ...,
sd_info: global___SdCardInfo | None = ...,
ping: global___InterdeviceVersion.ValueType = ...,
pong: global___InterdeviceVersion.ValueType = ...,
nack: builtins.bool = ...,
sd_command: global___SdCommand.ValueType = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["beep", b"beep", "data", b"data", "directory_listing", b"directory_listing", "file_transfer", b"file_transfer", "get_sd_info", b"get_sd_info", "i2c_result", b"i2c_result", "i2c_scan", b"i2c_scan", "i2c_scan_result", b"i2c_scan_result", "i2c_transaction", b"i2c_transaction", "nack", b"nack", "nmea", b"nmea", "ping", b"ping", "pong", b"pong", "sd_command", b"sd_command", "sd_info", b"sd_info"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["beep", b"beep", "data", b"data", "directory_listing", b"directory_listing", "file_transfer", b"file_transfer", "get_sd_info", b"get_sd_info", "i2c_result", b"i2c_result", "i2c_scan", b"i2c_scan", "i2c_scan_result", b"i2c_scan_result", "i2c_transaction", b"i2c_transaction", "id", b"id", "nack", b"nack", "nmea", b"nmea", "ping", b"ping", "pong", b"pong", "sd_command", b"sd_command", "sd_info", b"sd_info"]) -> None: ...
def WhichOneof(self, oneof_group: typing.Literal["data", b"data"]) -> typing.Literal["nmea", "beep", "i2c_transaction", "i2c_result", "i2c_scan", "i2c_scan_result", "file_transfer", "directory_listing", "get_sd_info", "sd_info", "ping", "pong", "nack", "sd_command"] | None: ...
global___InterdeviceMessage = InterdeviceMessage
+3 -3
View File
@@ -15,16 +15,16 @@ from meshtastic.protobuf import config_pb2 as meshtastic_dot_protobuf_dot_config
from meshtastic.protobuf import module_config_pb2 as meshtastic_dot_protobuf_dot_module__config__pb2
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n#meshtastic/protobuf/localonly.proto\x12\x13meshtastic.protobuf\x1a meshtastic/protobuf/config.proto\x1a\'meshtastic/protobuf/module_config.proto\"\xfa\x03\n\x0bLocalConfig\x12\x38\n\x06\x64\x65vice\x18\x01 \x01(\x0b\x32(.meshtastic.protobuf.Config.DeviceConfig\x12<\n\x08position\x18\x02 \x01(\x0b\x32*.meshtastic.protobuf.Config.PositionConfig\x12\x36\n\x05power\x18\x03 \x01(\x0b\x32\'.meshtastic.protobuf.Config.PowerConfig\x12:\n\x07network\x18\x04 \x01(\x0b\x32).meshtastic.protobuf.Config.NetworkConfig\x12:\n\x07\x64isplay\x18\x05 \x01(\x0b\x32).meshtastic.protobuf.Config.DisplayConfig\x12\x34\n\x04lora\x18\x06 \x01(\x0b\x32&.meshtastic.protobuf.Config.LoRaConfig\x12>\n\tbluetooth\x18\x07 \x01(\x0b\x32+.meshtastic.protobuf.Config.BluetoothConfig\x12\x0f\n\x07version\x18\x08 \x01(\r\x12<\n\x08security\x18\t \x01(\x0b\x32*.meshtastic.protobuf.Config.SecurityConfig\"\xf0\x07\n\x11LocalModuleConfig\x12:\n\x04mqtt\x18\x01 \x01(\x0b\x32,.meshtastic.protobuf.ModuleConfig.MQTTConfig\x12>\n\x06serial\x18\x02 \x01(\x0b\x32..meshtastic.protobuf.ModuleConfig.SerialConfig\x12[\n\x15\x65xternal_notification\x18\x03 \x01(\x0b\x32<.meshtastic.protobuf.ModuleConfig.ExternalNotificationConfig\x12K\n\rstore_forward\x18\x04 \x01(\x0b\x32\x34.meshtastic.protobuf.ModuleConfig.StoreForwardConfig\x12\x45\n\nrange_test\x18\x05 \x01(\x0b\x32\x31.meshtastic.protobuf.ModuleConfig.RangeTestConfig\x12\x44\n\ttelemetry\x18\x06 \x01(\x0b\x32\x31.meshtastic.protobuf.ModuleConfig.TelemetryConfig\x12M\n\x0e\x63\x61nned_message\x18\x07 \x01(\x0b\x32\x35.meshtastic.protobuf.ModuleConfig.CannedMessageConfig\x12<\n\x05\x61udio\x18\t \x01(\x0b\x32-.meshtastic.protobuf.ModuleConfig.AudioConfig\x12O\n\x0fremote_hardware\x18\n \x01(\x0b\x32\x36.meshtastic.protobuf.ModuleConfig.RemoteHardwareConfig\x12K\n\rneighbor_info\x18\x0b \x01(\x0b\x32\x34.meshtastic.protobuf.ModuleConfig.NeighborInfoConfig\x12Q\n\x10\x61mbient_lighting\x18\x0c \x01(\x0b\x32\x37.meshtastic.protobuf.ModuleConfig.AmbientLightingConfig\x12Q\n\x10\x64\x65tection_sensor\x18\r \x01(\x0b\x32\x37.meshtastic.protobuf.ModuleConfig.DetectionSensorConfig\x12\x46\n\npaxcounter\x18\x0e \x01(\x0b\x32\x32.meshtastic.protobuf.ModuleConfig.PaxcounterConfig\x12\x0f\n\x07version\x18\x08 \x01(\rBd\n\x13\x63om.geeksville.meshB\x0fLocalOnlyProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n#meshtastic/protobuf/localonly.proto\x12\x13meshtastic.protobuf\x1a meshtastic/protobuf/config.proto\x1a\'meshtastic/protobuf/module_config.proto\"\xfa\x03\n\x0bLocalConfig\x12\x38\n\x06\x64\x65vice\x18\x01 \x01(\x0b\x32(.meshtastic.protobuf.Config.DeviceConfig\x12<\n\x08position\x18\x02 \x01(\x0b\x32*.meshtastic.protobuf.Config.PositionConfig\x12\x36\n\x05power\x18\x03 \x01(\x0b\x32\'.meshtastic.protobuf.Config.PowerConfig\x12:\n\x07network\x18\x04 \x01(\x0b\x32).meshtastic.protobuf.Config.NetworkConfig\x12:\n\x07\x64isplay\x18\x05 \x01(\x0b\x32).meshtastic.protobuf.Config.DisplayConfig\x12\x34\n\x04lora\x18\x06 \x01(\x0b\x32&.meshtastic.protobuf.Config.LoRaConfig\x12>\n\tbluetooth\x18\x07 \x01(\x0b\x32+.meshtastic.protobuf.Config.BluetoothConfig\x12\x0f\n\x07version\x18\x08 \x01(\r\x12<\n\x08security\x18\t \x01(\x0b\x32*.meshtastic.protobuf.Config.SecurityConfig\"\x98\n\n\x11LocalModuleConfig\x12:\n\x04mqtt\x18\x01 \x01(\x0b\x32,.meshtastic.protobuf.ModuleConfig.MQTTConfig\x12>\n\x06serial\x18\x02 \x01(\x0b\x32..meshtastic.protobuf.ModuleConfig.SerialConfig\x12[\n\x15\x65xternal_notification\x18\x03 \x01(\x0b\x32<.meshtastic.protobuf.ModuleConfig.ExternalNotificationConfig\x12K\n\rstore_forward\x18\x04 \x01(\x0b\x32\x34.meshtastic.protobuf.ModuleConfig.StoreForwardConfig\x12\x45\n\nrange_test\x18\x05 \x01(\x0b\x32\x31.meshtastic.protobuf.ModuleConfig.RangeTestConfig\x12\x44\n\ttelemetry\x18\x06 \x01(\x0b\x32\x31.meshtastic.protobuf.ModuleConfig.TelemetryConfig\x12M\n\x0e\x63\x61nned_message\x18\x07 \x01(\x0b\x32\x35.meshtastic.protobuf.ModuleConfig.CannedMessageConfig\x12<\n\x05\x61udio\x18\t \x01(\x0b\x32-.meshtastic.protobuf.ModuleConfig.AudioConfig\x12O\n\x0fremote_hardware\x18\n \x01(\x0b\x32\x36.meshtastic.protobuf.ModuleConfig.RemoteHardwareConfig\x12K\n\rneighbor_info\x18\x0b \x01(\x0b\x32\x34.meshtastic.protobuf.ModuleConfig.NeighborInfoConfig\x12Q\n\x10\x61mbient_lighting\x18\x0c \x01(\x0b\x32\x37.meshtastic.protobuf.ModuleConfig.AmbientLightingConfig\x12Q\n\x10\x64\x65tection_sensor\x18\r \x01(\x0b\x32\x37.meshtastic.protobuf.ModuleConfig.DetectionSensorConfig\x12\x46\n\npaxcounter\x18\x0e \x01(\x0b\x32\x32.meshtastic.protobuf.ModuleConfig.PaxcounterConfig\x12L\n\rstatusmessage\x18\x0f \x01(\x0b\x32\x35.meshtastic.protobuf.ModuleConfig.StatusMessageConfig\x12U\n\x12traffic_management\x18\x10 \x01(\x0b\x32\x39.meshtastic.protobuf.ModuleConfig.TrafficManagementConfig\x12\x38\n\x03tak\x18\x11 \x01(\x0b\x32+.meshtastic.protobuf.ModuleConfig.TAKConfig\x12G\n\x0bmesh_beacon\x18\x12 \x01(\x0b\x32\x32.meshtastic.protobuf.ModuleConfig.MeshBeaconConfig\x12\x0f\n\x07version\x18\x08 \x01(\rBe\n\x14org.meshtastic.protoB\x0fLocalOnlyProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.localonly_pb2', _globals)
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\017LocalOnlyProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\017LocalOnlyProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
_globals['_LOCALCONFIG']._serialized_start=136
_globals['_LOCALCONFIG']._serialized_end=642
_globals['_LOCALMODULECONFIG']._serialized_start=645
_globals['_LOCALMODULECONFIG']._serialized_end=1653
_globals['_LOCALMODULECONFIG']._serialized_end=1949
# @@protoc_insertion_point(module_scope)
+34 -2
View File
@@ -119,6 +119,10 @@ class LocalModuleConfig(google.protobuf.message.Message):
AMBIENT_LIGHTING_FIELD_NUMBER: builtins.int
DETECTION_SENSOR_FIELD_NUMBER: builtins.int
PAXCOUNTER_FIELD_NUMBER: builtins.int
STATUSMESSAGE_FIELD_NUMBER: builtins.int
TRAFFIC_MANAGEMENT_FIELD_NUMBER: builtins.int
TAK_FIELD_NUMBER: builtins.int
MESH_BEACON_FIELD_NUMBER: builtins.int
VERSION_FIELD_NUMBER: builtins.int
version: builtins.int
"""
@@ -204,6 +208,30 @@ class LocalModuleConfig(google.protobuf.message.Message):
Paxcounter Config
"""
@property
def statusmessage(self) -> meshtastic.protobuf.module_config_pb2.ModuleConfig.StatusMessageConfig:
"""
StatusMessage Config
"""
@property
def traffic_management(self) -> meshtastic.protobuf.module_config_pb2.ModuleConfig.TrafficManagementConfig:
"""
The part of the config that is specific to the Traffic Management module
"""
@property
def tak(self) -> meshtastic.protobuf.module_config_pb2.ModuleConfig.TAKConfig:
"""
TAK Config
"""
@property
def mesh_beacon(self) -> meshtastic.protobuf.module_config_pb2.ModuleConfig.MeshBeaconConfig:
"""
MeshBeacon Config
"""
def __init__(
self,
*,
@@ -220,9 +248,13 @@ class LocalModuleConfig(google.protobuf.message.Message):
ambient_lighting: meshtastic.protobuf.module_config_pb2.ModuleConfig.AmbientLightingConfig | None = ...,
detection_sensor: meshtastic.protobuf.module_config_pb2.ModuleConfig.DetectionSensorConfig | None = ...,
paxcounter: meshtastic.protobuf.module_config_pb2.ModuleConfig.PaxcounterConfig | None = ...,
statusmessage: meshtastic.protobuf.module_config_pb2.ModuleConfig.StatusMessageConfig | None = ...,
traffic_management: meshtastic.protobuf.module_config_pb2.ModuleConfig.TrafficManagementConfig | None = ...,
tak: meshtastic.protobuf.module_config_pb2.ModuleConfig.TAKConfig | None = ...,
mesh_beacon: meshtastic.protobuf.module_config_pb2.ModuleConfig.MeshBeaconConfig | None = ...,
version: builtins.int = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["ambient_lighting", b"ambient_lighting", "audio", b"audio", "canned_message", b"canned_message", "detection_sensor", b"detection_sensor", "external_notification", b"external_notification", "mqtt", b"mqtt", "neighbor_info", b"neighbor_info", "paxcounter", b"paxcounter", "range_test", b"range_test", "remote_hardware", b"remote_hardware", "serial", b"serial", "store_forward", b"store_forward", "telemetry", b"telemetry"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["ambient_lighting", b"ambient_lighting", "audio", b"audio", "canned_message", b"canned_message", "detection_sensor", b"detection_sensor", "external_notification", b"external_notification", "mqtt", b"mqtt", "neighbor_info", b"neighbor_info", "paxcounter", b"paxcounter", "range_test", b"range_test", "remote_hardware", b"remote_hardware", "serial", b"serial", "store_forward", b"store_forward", "telemetry", b"telemetry", "version", b"version"]) -> None: ...
def HasField(self, field_name: typing.Literal["ambient_lighting", b"ambient_lighting", "audio", b"audio", "canned_message", b"canned_message", "detection_sensor", b"detection_sensor", "external_notification", b"external_notification", "mesh_beacon", b"mesh_beacon", "mqtt", b"mqtt", "neighbor_info", b"neighbor_info", "paxcounter", b"paxcounter", "range_test", b"range_test", "remote_hardware", b"remote_hardware", "serial", b"serial", "statusmessage", b"statusmessage", "store_forward", b"store_forward", "tak", b"tak", "telemetry", b"telemetry", "traffic_management", b"traffic_management"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["ambient_lighting", b"ambient_lighting", "audio", b"audio", "canned_message", b"canned_message", "detection_sensor", b"detection_sensor", "external_notification", b"external_notification", "mesh_beacon", b"mesh_beacon", "mqtt", b"mqtt", "neighbor_info", b"neighbor_info", "paxcounter", b"paxcounter", "range_test", b"range_test", "remote_hardware", b"remote_hardware", "serial", b"serial", "statusmessage", b"statusmessage", "store_forward", b"store_forward", "tak", b"tak", "telemetry", b"telemetry", "traffic_management", b"traffic_management", "version", b"version"]) -> None: ...
global___LocalModuleConfig = LocalModuleConfig
+31
View File
@@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: meshtastic/protobuf/mesh_beacon.proto
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
from meshtastic.protobuf import channel_pb2 as meshtastic_dot_protobuf_dot_channel__pb2
from meshtastic.protobuf import config_pb2 as meshtastic_dot_protobuf_dot_config__pb2
from meshtastic.protobuf import nanopb_pb2 as meshtastic_dot_protobuf_dot_nanopb__pb2
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n%meshtastic/protobuf/mesh_beacon.proto\x12\x13meshtastic.protobuf\x1a!meshtastic/protobuf/channel.proto\x1a meshtastic/protobuf/config.proto\x1a meshtastic/protobuf/nanopb.proto\"\x8a\x02\n\nMeshBeacon\x12\x16\n\x07message\x18\x01 \x01(\tB\x05\x92?\x02\x08\x65\x12;\n\roffer_channel\x18\x02 \x01(\x0b\x32$.meshtastic.protobuf.ChannelSettings\x12G\n\x0coffer_region\x18\x03 \x01(\x0e\x32\x31.meshtastic.protobuf.Config.LoRaConfig.RegionCode\x12M\n\x0coffer_preset\x18\x04 \x01(\x0e\x32\x32.meshtastic.protobuf.Config.LoRaConfig.ModemPresetH\x00\x88\x01\x01\x42\x0f\n\r_offer_presetBf\n\x14org.meshtastic.protoB\x10MeshBeaconProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.mesh_beacon_pb2', _globals)
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\020MeshBeaconProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
_MESHBEACON.fields_by_name['message']._options = None
_MESHBEACON.fields_by_name['message']._serialized_options = b'\222?\002\010e'
_globals['_MESHBEACON']._serialized_start=166
_globals['_MESHBEACON']._serialized_end=432
# @@protoc_insertion_point(module_scope)
+62
View File
@@ -0,0 +1,62 @@
"""
@generated by mypy-protobuf. Do not edit manually!
isort:skip_file
"""
import builtins
import google.protobuf.descriptor
import google.protobuf.message
import meshtastic.protobuf.channel_pb2
import meshtastic.protobuf.config_pb2
import typing
DESCRIPTOR: google.protobuf.descriptor.FileDescriptor
@typing.final
class MeshBeacon(google.protobuf.message.Message):
"""
Payload for MESH_BEACON_APP packets.
Periodically broadcast by nodes in beacon mode.
Listeners deliver the text message to the local inbox and cache any offered
channel/preset for the client app to act on the firmware never auto-applies them.
"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
MESSAGE_FIELD_NUMBER: builtins.int
OFFER_CHANNEL_FIELD_NUMBER: builtins.int
OFFER_REGION_FIELD_NUMBER: builtins.int
OFFER_PRESET_FIELD_NUMBER: builtins.int
message: builtins.str
"""
Human-readable beacon message. Max 100 bytes enforced by firmware on send.
"""
offer_region: meshtastic.protobuf.config_pb2.Config.LoRaConfig.RegionCode.ValueType
"""
Optional region being advertised alongside offer_preset.
"""
offer_preset: meshtastic.protobuf.config_pb2.Config.LoRaConfig.ModemPreset.ValueType
"""
Optional modem preset being advertised.
Combined with offer_region, tells a client "there is a mesh on this preset/region".
"""
@property
def offer_channel(self) -> meshtastic.protobuf.channel_pb2.ChannelSettings:
"""
Optional channel (name + PSK) being advertised to listening clients.
A client app may offer to switch the user to this channel; firmware never applies it automatically.
"""
def __init__(
self,
*,
message: builtins.str = ...,
offer_channel: meshtastic.protobuf.channel_pb2.ChannelSettings | None = ...,
offer_region: meshtastic.protobuf.config_pb2.Config.LoRaConfig.RegionCode.ValueType = ...,
offer_preset: meshtastic.protobuf.config_pb2.Config.LoRaConfig.ModemPreset.ValueType | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["_offer_preset", b"_offer_preset", "offer_channel", b"offer_channel", "offer_preset", b"offer_preset"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["_offer_preset", b"_offer_preset", "message", b"message", "offer_channel", b"offer_channel", "offer_preset", b"offer_preset", "offer_region", b"offer_region"]) -> None: ...
def WhichOneof(self, oneof_group: typing.Literal["_offer_preset", b"_offer_preset"]) -> typing.Literal["offer_preset"] | None: ...
global___MeshBeacon = MeshBeacon
+237 -74
View File
File diff suppressed because one or more lines are too long.
+1603 -32
View File
File diff suppressed because it is too large. Load diff
+118 -46
View File
File diff suppressed because one or more lines are too long.
+364 -8
View File
@@ -9,6 +9,9 @@ import google.protobuf.descriptor
import google.protobuf.internal.containers
import google.protobuf.internal.enum_type_wrapper
import google.protobuf.message
import meshtastic.protobuf.atak_pb2
import meshtastic.protobuf.channel_pb2
import meshtastic.protobuf.config_pb2
import sys
import typing
@@ -112,7 +115,7 @@ class ModuleConfig(google.protobuf.message.Message):
"""
json_enabled: builtins.bool
"""
Whether to send / consume json packets on MQTT
Deprecated: JSON packet support on MQTT was removed, and this field is ignored.
"""
tls_enabled: builtins.bool
"""
@@ -165,6 +168,7 @@ class ModuleConfig(google.protobuf.message.Message):
PUBLISH_INTERVAL_SECS_FIELD_NUMBER: builtins.int
POSITION_PRECISION_FIELD_NUMBER: builtins.int
SHOULD_REPORT_LOCATION_FIELD_NUMBER: builtins.int
publish_interval_secs: builtins.int
"""
How often we should report our info to the map (in seconds)
@@ -173,13 +177,18 @@ class ModuleConfig(google.protobuf.message.Message):
"""
Bits of precision for the location sent (default of 32 is full precision).
"""
should_report_location: builtins.bool
"""
Whether we have opted-in to report our location to the map
"""
def __init__(
self,
*,
publish_interval_secs: builtins.int = ...,
position_precision: builtins.int = ...,
should_report_location: builtins.bool = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["position_precision", b"position_precision", "publish_interval_secs", b"publish_interval_secs"]) -> None: ...
def ClearField(self, field_name: typing.Literal["position_precision", b"position_precision", "publish_interval_secs", b"publish_interval_secs", "should_report_location", b"should_report_location"]) -> None: ...
@typing.final
class RemoteHardwareConfig(google.protobuf.message.Message):
@@ -486,6 +495,58 @@ class ModuleConfig(google.protobuf.message.Message):
) -> None: ...
def ClearField(self, field_name: typing.Literal["ble_threshold", b"ble_threshold", "enabled", b"enabled", "paxcounter_update_interval", b"paxcounter_update_interval", "wifi_threshold", b"wifi_threshold"]) -> None: ...
@typing.final
class TrafficManagementConfig(google.protobuf.message.Message):
"""
Config for the Traffic Management module.
Provides packet inspection and traffic shaping to help reduce channel utilization
"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
POSITION_MIN_INTERVAL_SECS_FIELD_NUMBER: builtins.int
NODEINFO_DIRECT_RESPONSE_MAX_HOPS_FIELD_NUMBER: builtins.int
RATE_LIMIT_WINDOW_SECS_FIELD_NUMBER: builtins.int
RATE_LIMIT_MAX_PACKETS_FIELD_NUMBER: builtins.int
UNKNOWN_PACKET_THRESHOLD_FIELD_NUMBER: builtins.int
position_min_interval_secs: builtins.int
"""
Minimum interval in seconds between position updates from the same node.
A non-zero value implicitly enables the suppression window; 0 disables it.
"""
nodeinfo_direct_response_max_hops: builtins.int
"""
Maximum hop distance from the requestor at which direct NodeInfo responses
are served from the local cache. A non-zero value implicitly enables direct
response; 0 disables it.
"""
rate_limit_window_secs: builtins.int
"""
Time window in seconds for per-node rate limiting.
A non-zero value implicitly enables rate limiting; 0 disables it.
"""
rate_limit_max_packets: builtins.int
"""
Maximum packets allowed per node within the rate limit window.
A non-zero value implicitly enables rate limiting; 0 disables it.
"""
unknown_packet_threshold: builtins.int
"""
Maximum unknown/undecryptable packets per rate window before the source
is dropped. A non-zero value implicitly enables unknown-packet filtering;
0 disables it.
"""
def __init__(
self,
*,
position_min_interval_secs: builtins.int = ...,
nodeinfo_direct_response_max_hops: builtins.int = ...,
rate_limit_window_secs: builtins.int = ...,
rate_limit_max_packets: builtins.int = ...,
unknown_packet_threshold: builtins.int = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["nodeinfo_direct_response_max_hops", b"nodeinfo_direct_response_max_hops", "position_min_interval_secs", b"position_min_interval_secs", "rate_limit_max_packets", b"rate_limit_max_packets", "rate_limit_window_secs", b"rate_limit_window_secs", "unknown_packet_threshold", b"unknown_packet_threshold"]) -> None: ...
@typing.final
class SerialConfig(google.protobuf.message.Message):
"""
@@ -554,6 +615,20 @@ class ModuleConfig(google.protobuf.message.Message):
"""NMEA messages specifically tailored for CalTopo"""
WS85: ModuleConfig.SerialConfig._Serial_Mode.ValueType # 6
"""Ecowitt WS85 weather station"""
VE_DIRECT: ModuleConfig.SerialConfig._Serial_Mode.ValueType # 7
"""VE.Direct is a serial protocol used by Victron Energy products
https://beta.ivc.no/wiki/index.php/Victron_VE_Direct_DIY_Cable
"""
MS_CONFIG: ModuleConfig.SerialConfig._Serial_Mode.ValueType # 8
"""Used to configure and view some parameters of MeshSolar.
https://heltec.org/project/meshsolar/
"""
LOG: ModuleConfig.SerialConfig._Serial_Mode.ValueType # 9
"""Logs mesh traffic to the serial pins, ideal for logging via openLog or similar.
includes other packets
"""
LOGTEXT: ModuleConfig.SerialConfig._Serial_Mode.ValueType # 10
"""only text (channel & DM)"""
class Serial_Mode(_Serial_Mode, metaclass=_Serial_ModeEnumTypeWrapper):
"""
@@ -569,6 +644,20 @@ class ModuleConfig(google.protobuf.message.Message):
"""NMEA messages specifically tailored for CalTopo"""
WS85: ModuleConfig.SerialConfig.Serial_Mode.ValueType # 6
"""Ecowitt WS85 weather station"""
VE_DIRECT: ModuleConfig.SerialConfig.Serial_Mode.ValueType # 7
"""VE.Direct is a serial protocol used by Victron Energy products
https://beta.ivc.no/wiki/index.php/Victron_VE_Direct_DIY_Cable
"""
MS_CONFIG: ModuleConfig.SerialConfig.Serial_Mode.ValueType # 8
"""Used to configure and view some parameters of MeshSolar.
https://heltec.org/project/meshsolar/
"""
LOG: ModuleConfig.SerialConfig.Serial_Mode.ValueType # 9
"""Logs mesh traffic to the serial pins, ideal for logging via openLog or similar.
includes other packets
"""
LOGTEXT: ModuleConfig.SerialConfig.Serial_Mode.ValueType # 10
"""only text (channel & DM)"""
ENABLED_FIELD_NUMBER: builtins.int
ECHO_FIELD_NUMBER: builtins.int
@@ -802,6 +891,7 @@ class ModuleConfig(google.protobuf.message.Message):
ENABLED_FIELD_NUMBER: builtins.int
SENDER_FIELD_NUMBER: builtins.int
SAVE_FIELD_NUMBER: builtins.int
CLEAR_ON_REBOOT_FIELD_NUMBER: builtins.int
enabled: builtins.bool
"""
Enable the Range Test Module
@@ -815,14 +905,20 @@ class ModuleConfig(google.protobuf.message.Message):
Bool value indicating that this node should save a RangeTest.csv file.
ESP32 Only
"""
clear_on_reboot: builtins.bool
"""
Bool indicating that the node should cleanup / destroy it's RangeTest.csv file.
ESP32 Only
"""
def __init__(
self,
*,
enabled: builtins.bool = ...,
sender: builtins.int = ...,
save: builtins.bool = ...,
clear_on_reboot: builtins.bool = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["enabled", b"enabled", "save", b"save", "sender", b"sender"]) -> None: ...
def ClearField(self, field_name: typing.Literal["clear_on_reboot", b"clear_on_reboot", "enabled", b"enabled", "save", b"save", "sender", b"sender"]) -> None: ...
@typing.final
class TelemetryConfig(google.protobuf.message.Message):
@@ -845,6 +941,8 @@ class ModuleConfig(google.protobuf.message.Message):
HEALTH_MEASUREMENT_ENABLED_FIELD_NUMBER: builtins.int
HEALTH_UPDATE_INTERVAL_FIELD_NUMBER: builtins.int
HEALTH_SCREEN_ENABLED_FIELD_NUMBER: builtins.int
DEVICE_TELEMETRY_ENABLED_FIELD_NUMBER: builtins.int
AIR_QUALITY_SCREEN_ENABLED_FIELD_NUMBER: builtins.int
device_update_interval: builtins.int
"""
Interval in seconds of how often we should try to send our
@@ -905,6 +1003,15 @@ class ModuleConfig(google.protobuf.message.Message):
"""
Enable/Disable the health telemetry module on-device display
"""
device_telemetry_enabled: builtins.bool
"""
Enable/Disable the device telemetry module to send metrics to the mesh
Note: We will still send telemtry to the connected phone / client every minute over the API
"""
air_quality_screen_enabled: builtins.bool
"""
Enable/Disable the air quality telemetry measurement module on-device display
"""
def __init__(
self,
*,
@@ -921,13 +1028,15 @@ class ModuleConfig(google.protobuf.message.Message):
health_measurement_enabled: builtins.bool = ...,
health_update_interval: builtins.int = ...,
health_screen_enabled: builtins.bool = ...,
device_telemetry_enabled: builtins.bool = ...,
air_quality_screen_enabled: builtins.bool = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["air_quality_enabled", b"air_quality_enabled", "air_quality_interval", b"air_quality_interval", "device_update_interval", b"device_update_interval", "environment_display_fahrenheit", b"environment_display_fahrenheit", "environment_measurement_enabled", b"environment_measurement_enabled", "environment_screen_enabled", b"environment_screen_enabled", "environment_update_interval", b"environment_update_interval", "health_measurement_enabled", b"health_measurement_enabled", "health_screen_enabled", b"health_screen_enabled", "health_update_interval", b"health_update_interval", "power_measurement_enabled", b"power_measurement_enabled", "power_screen_enabled", b"power_screen_enabled", "power_update_interval", b"power_update_interval"]) -> None: ...
def ClearField(self, field_name: typing.Literal["air_quality_enabled", b"air_quality_enabled", "air_quality_interval", b"air_quality_interval", "air_quality_screen_enabled", b"air_quality_screen_enabled", "device_telemetry_enabled", b"device_telemetry_enabled", "device_update_interval", b"device_update_interval", "environment_display_fahrenheit", b"environment_display_fahrenheit", "environment_measurement_enabled", b"environment_measurement_enabled", "environment_screen_enabled", b"environment_screen_enabled", "environment_update_interval", b"environment_update_interval", "health_measurement_enabled", b"health_measurement_enabled", "health_screen_enabled", b"health_screen_enabled", "health_update_interval", b"health_update_interval", "power_measurement_enabled", b"power_measurement_enabled", "power_screen_enabled", b"power_screen_enabled", "power_update_interval", b"power_update_interval"]) -> None: ...
@typing.final
class CannedMessageConfig(google.protobuf.message.Message):
"""
TODO: REPLACE
Canned Messages Module Config
"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
@@ -1128,6 +1237,221 @@ class ModuleConfig(google.protobuf.message.Message):
) -> None: ...
def ClearField(self, field_name: typing.Literal["blue", b"blue", "current", b"current", "green", b"green", "led_state", b"led_state", "red", b"red"]) -> None: ...
@typing.final
class StatusMessageConfig(google.protobuf.message.Message):
"""
StatusMessage config - Allows setting a status message for a node to periodically rebroadcast
"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
NODE_STATUS_FIELD_NUMBER: builtins.int
node_status: builtins.str
"""
The actual status string
"""
def __init__(
self,
*,
node_status: builtins.str = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["node_status", b"node_status"]) -> None: ...
@typing.final
class MeshBeaconConfig(google.protobuf.message.Message):
"""
MeshBeacon module config
"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
class _Flags:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
class _FlagsEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[ModuleConfig.MeshBeaconConfig._Flags.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
FLAG_NONE: ModuleConfig.MeshBeaconConfig._Flags.ValueType # 0
"""
No options enabled.
"""
FLAG_LISTEN_ENABLED: ModuleConfig.MeshBeaconConfig._Flags.ValueType # 1
"""
Enable receiving MESH_BEACON_APP packets from other nodes.
The text portion is delivered to the local message inbox.
Offered channel/preset are stored for the client app to act on.
"""
FLAG_BROADCAST_ENABLED: ModuleConfig.MeshBeaconConfig._Flags.ValueType # 2
"""
Enable periodically broadcasting MESH_BEACON_APP packets from this node.
"""
FLAG_LEGACY_SPLIT: ModuleConfig.MeshBeaconConfig._Flags.ValueType # 4
"""
When both text and offer content are present, split the beacon into a separate
MESH_BEACON_APP (offer only) and TEXT_MESSAGE_APP (text only) packet, so firmware
that only decodes TEXT_MESSAGE_APP still receives the human-readable text.
"""
class Flags(_Flags, metaclass=_FlagsEnumTypeWrapper):
"""
Boolean options for the beacon module, packed into the `flags` bitfield below.
OR the FLAG_* values together; a flag is on when its bit is set.
"""
FLAG_NONE: ModuleConfig.MeshBeaconConfig.Flags.ValueType # 0
"""
No options enabled.
"""
FLAG_LISTEN_ENABLED: ModuleConfig.MeshBeaconConfig.Flags.ValueType # 1
"""
Enable receiving MESH_BEACON_APP packets from other nodes.
The text portion is delivered to the local message inbox.
Offered channel/preset are stored for the client app to act on.
"""
FLAG_BROADCAST_ENABLED: ModuleConfig.MeshBeaconConfig.Flags.ValueType # 2
"""
Enable periodically broadcasting MESH_BEACON_APP packets from this node.
"""
FLAG_LEGACY_SPLIT: ModuleConfig.MeshBeaconConfig.Flags.ValueType # 4
"""
When both text and offer content are present, split the beacon into a separate
MESH_BEACON_APP (offer only) and TEXT_MESSAGE_APP (text only) packet, so firmware
that only decodes TEXT_MESSAGE_APP still receives the human-readable text.
"""
@typing.final
class BroadcastTarget(google.protobuf.message.Message):
"""
One entry in the broadcast destination list.
Each entry names one set of radio settings to send a beacon copy on.
"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
PRESET_FIELD_NUMBER: builtins.int
REGION_FIELD_NUMBER: builtins.int
CHANNEL_INDEX_FIELD_NUMBER: builtins.int
preset: meshtastic.protobuf.config_pb2.Config.LoRaConfig.ModemPreset.ValueType
"""
Modem preset to use for this target.
Falls back to the running config preset if unset.
"""
region: meshtastic.protobuf.config_pb2.Config.LoRaConfig.RegionCode.ValueType
"""
Region to use for this target. UNSET means use the running config region.
"""
channel_index: builtins.int
"""Tag 3 was an embedded ChannelSettings; replaced by channel_index (tag 4) to keep
ModuleConfig within the BLE FromRadio size budget. Branch unreleased, so tag 3 is a gap.
Index into the device's channel table (0..MAX_NUM_CHANNELS-1) of the channel to
transmit this target's beacon on. The referenced channel must already be configured
on the node (its key is needed to encrypt). If unset, the default channel for the
preset is used.
"""
def __init__(
self,
*,
preset: meshtastic.protobuf.config_pb2.Config.LoRaConfig.ModemPreset.ValueType | None = ...,
region: meshtastic.protobuf.config_pb2.Config.LoRaConfig.RegionCode.ValueType = ...,
channel_index: builtins.int | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["_channel_index", b"_channel_index", "_preset", b"_preset", "channel_index", b"channel_index", "preset", b"preset"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["_channel_index", b"_channel_index", "_preset", b"_preset", "channel_index", b"channel_index", "preset", b"preset", "region", b"region"]) -> None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_channel_index", b"_channel_index"]) -> typing.Literal["channel_index"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_preset", b"_preset"]) -> typing.Literal["preset"] | None: ...
FLAGS_FIELD_NUMBER: builtins.int
BROADCAST_MESSAGE_FIELD_NUMBER: builtins.int
BROADCAST_OFFER_CHANNEL_FIELD_NUMBER: builtins.int
BROADCAST_OFFER_REGION_FIELD_NUMBER: builtins.int
BROADCAST_OFFER_PRESET_FIELD_NUMBER: builtins.int
BROADCAST_INTERVAL_SECS_FIELD_NUMBER: builtins.int
BROADCAST_TARGETS_FIELD_NUMBER: builtins.int
flags: builtins.int
"""
Bitwise-OR of Flags values (listen / broadcast / legacy-split toggles).
"""
broadcast_message: builtins.str
"""
Message to include in each beacon broadcast. Max 100 bytes enforced by firmware.
"""
broadcast_offer_region: meshtastic.protobuf.config_pb2.Config.LoRaConfig.RegionCode.ValueType
"""
Optional region to advertise in the MeshBeacon offer_region field.
"""
broadcast_offer_preset: meshtastic.protobuf.config_pb2.Config.LoRaConfig.ModemPreset.ValueType
"""
Optional modem preset to advertise in the MeshBeacon offer_preset field.
"""
broadcast_interval_secs: builtins.int
"""
How often to broadcast, in seconds. Min 3600 (1 h), default 3600.
"""
@property
def broadcast_offer_channel(self) -> meshtastic.protobuf.channel_pb2.ChannelSettings:
"""
Optional channel (name + PSK) to advertise in the MeshBeacon offer_channel field.
"""
@property
def broadcast_targets(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ModuleConfig.MeshBeaconConfig.BroadcastTarget]:
"""
Broadcast destination list.
The broadcaster sends one beacon copy per distinct destination, in sequence, temporarily
switching the radio to that entry's preset/region/channel for each.
When empty, a single beacon is sent on the node's running preset and region over the
primary channel.
Entries that resolve to the same effective preset, region and channel are deduplicated, so
a duplicate entry does not produce a second transmission.
"""
def __init__(
self,
*,
flags: builtins.int = ...,
broadcast_message: builtins.str = ...,
broadcast_offer_channel: meshtastic.protobuf.channel_pb2.ChannelSettings | None = ...,
broadcast_offer_region: meshtastic.protobuf.config_pb2.Config.LoRaConfig.RegionCode.ValueType = ...,
broadcast_offer_preset: meshtastic.protobuf.config_pb2.Config.LoRaConfig.ModemPreset.ValueType | None = ...,
broadcast_interval_secs: builtins.int = ...,
broadcast_targets: collections.abc.Iterable[global___ModuleConfig.MeshBeaconConfig.BroadcastTarget] | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["_broadcast_offer_preset", b"_broadcast_offer_preset", "broadcast_offer_channel", b"broadcast_offer_channel", "broadcast_offer_preset", b"broadcast_offer_preset"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["_broadcast_offer_preset", b"_broadcast_offer_preset", "broadcast_interval_secs", b"broadcast_interval_secs", "broadcast_message", b"broadcast_message", "broadcast_offer_channel", b"broadcast_offer_channel", "broadcast_offer_preset", b"broadcast_offer_preset", "broadcast_offer_region", b"broadcast_offer_region", "broadcast_targets", b"broadcast_targets", "flags", b"flags"]) -> None: ...
def WhichOneof(self, oneof_group: typing.Literal["_broadcast_offer_preset", b"_broadcast_offer_preset"]) -> typing.Literal["broadcast_offer_preset"] | None: ...
@typing.final
class TAKConfig(google.protobuf.message.Message):
"""
TAK team/role configuration
"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
TEAM_FIELD_NUMBER: builtins.int
ROLE_FIELD_NUMBER: builtins.int
team: meshtastic.protobuf.atak_pb2.Team.ValueType
"""
Team color.
Default Unspecifed_Color -> firmware uses Cyan
"""
role: meshtastic.protobuf.atak_pb2.MemberRole.ValueType
"""
Member role.
Default Unspecifed -> firmware uses TeamMember
"""
def __init__(
self,
*,
team: meshtastic.protobuf.atak_pb2.Team.ValueType = ...,
role: meshtastic.protobuf.atak_pb2.MemberRole.ValueType = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["role", b"role", "team", b"team"]) -> None: ...
MQTT_FIELD_NUMBER: builtins.int
SERIAL_FIELD_NUMBER: builtins.int
EXTERNAL_NOTIFICATION_FIELD_NUMBER: builtins.int
@@ -1141,6 +1465,10 @@ class ModuleConfig(google.protobuf.message.Message):
AMBIENT_LIGHTING_FIELD_NUMBER: builtins.int
DETECTION_SENSOR_FIELD_NUMBER: builtins.int
PAXCOUNTER_FIELD_NUMBER: builtins.int
STATUSMESSAGE_FIELD_NUMBER: builtins.int
TRAFFIC_MANAGEMENT_FIELD_NUMBER: builtins.int
TAK_FIELD_NUMBER: builtins.int
MESH_BEACON_FIELD_NUMBER: builtins.int
@property
def mqtt(self) -> global___ModuleConfig.MQTTConfig:
"""
@@ -1219,6 +1547,30 @@ class ModuleConfig(google.protobuf.message.Message):
TODO: REPLACE
"""
@property
def statusmessage(self) -> global___ModuleConfig.StatusMessageConfig:
"""
TODO: REPLACE
"""
@property
def traffic_management(self) -> global___ModuleConfig.TrafficManagementConfig:
"""
Traffic management module config for mesh network optimization
"""
@property
def tak(self) -> global___ModuleConfig.TAKConfig:
"""
TAK team/role configuration for TAK_TRACKER
"""
@property
def mesh_beacon(self) -> global___ModuleConfig.MeshBeaconConfig:
"""
MeshBeacon module config
"""
def __init__(
self,
*,
@@ -1235,10 +1587,14 @@ class ModuleConfig(google.protobuf.message.Message):
ambient_lighting: global___ModuleConfig.AmbientLightingConfig | None = ...,
detection_sensor: global___ModuleConfig.DetectionSensorConfig | None = ...,
paxcounter: global___ModuleConfig.PaxcounterConfig | None = ...,
statusmessage: global___ModuleConfig.StatusMessageConfig | None = ...,
traffic_management: global___ModuleConfig.TrafficManagementConfig | None = ...,
tak: global___ModuleConfig.TAKConfig | None = ...,
mesh_beacon: global___ModuleConfig.MeshBeaconConfig | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["ambient_lighting", b"ambient_lighting", "audio", b"audio", "canned_message", b"canned_message", "detection_sensor", b"detection_sensor", "external_notification", b"external_notification", "mqtt", b"mqtt", "neighbor_info", b"neighbor_info", "paxcounter", b"paxcounter", "payload_variant", b"payload_variant", "range_test", b"range_test", "remote_hardware", b"remote_hardware", "serial", b"serial", "store_forward", b"store_forward", "telemetry", b"telemetry"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["ambient_lighting", b"ambient_lighting", "audio", b"audio", "canned_message", b"canned_message", "detection_sensor", b"detection_sensor", "external_notification", b"external_notification", "mqtt", b"mqtt", "neighbor_info", b"neighbor_info", "paxcounter", b"paxcounter", "payload_variant", b"payload_variant", "range_test", b"range_test", "remote_hardware", b"remote_hardware", "serial", b"serial", "store_forward", b"store_forward", "telemetry", b"telemetry"]) -> None: ...
def WhichOneof(self, oneof_group: typing.Literal["payload_variant", b"payload_variant"]) -> typing.Literal["mqtt", "serial", "external_notification", "store_forward", "range_test", "telemetry", "canned_message", "audio", "remote_hardware", "neighbor_info", "ambient_lighting", "detection_sensor", "paxcounter"] | None: ...
def HasField(self, field_name: typing.Literal["ambient_lighting", b"ambient_lighting", "audio", b"audio", "canned_message", b"canned_message", "detection_sensor", b"detection_sensor", "external_notification", b"external_notification", "mesh_beacon", b"mesh_beacon", "mqtt", b"mqtt", "neighbor_info", b"neighbor_info", "paxcounter", b"paxcounter", "payload_variant", b"payload_variant", "range_test", b"range_test", "remote_hardware", b"remote_hardware", "serial", b"serial", "statusmessage", b"statusmessage", "store_forward", b"store_forward", "tak", b"tak", "telemetry", b"telemetry", "traffic_management", b"traffic_management"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["ambient_lighting", b"ambient_lighting", "audio", b"audio", "canned_message", b"canned_message", "detection_sensor", b"detection_sensor", "external_notification", b"external_notification", "mesh_beacon", b"mesh_beacon", "mqtt", b"mqtt", "neighbor_info", b"neighbor_info", "paxcounter", b"paxcounter", "payload_variant", b"payload_variant", "range_test", b"range_test", "remote_hardware", b"remote_hardware", "serial", b"serial", "statusmessage", b"statusmessage", "store_forward", b"store_forward", "tak", b"tak", "telemetry", b"telemetry", "traffic_management", b"traffic_management"]) -> None: ...
def WhichOneof(self, oneof_group: typing.Literal["payload_variant", b"payload_variant"]) -> typing.Literal["mqtt", "serial", "external_notification", "store_forward", "range_test", "telemetry", "canned_message", "audio", "remote_hardware", "neighbor_info", "ambient_lighting", "detection_sensor", "paxcounter", "statusmessage", "traffic_management", "tak", "mesh_beacon"] | None: ...
global___ModuleConfig = ModuleConfig
+21 -6
View File
@@ -13,18 +13,33 @@ _sym_db = _symbol_database.Default()
from meshtastic.protobuf import config_pb2 as meshtastic_dot_protobuf_dot_config__pb2
from meshtastic.protobuf import mesh_pb2 as meshtastic_dot_protobuf_dot_mesh__pb2
from meshtastic.protobuf import nanopb_pb2 as meshtastic_dot_protobuf_dot_nanopb__pb2
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1emeshtastic/protobuf/mqtt.proto\x12\x13meshtastic.protobuf\x1a meshtastic/protobuf/config.proto\x1a\x1emeshtastic/protobuf/mesh.proto\"j\n\x0fServiceEnvelope\x12/\n\x06packet\x18\x01 \x01(\x0b\x32\x1f.meshtastic.protobuf.MeshPacket\x12\x12\n\nchannel_id\x18\x02 \x01(\t\x12\x12\n\ngateway_id\x18\x03 \x01(\t\"\xe0\x03\n\tMapReport\x12\x11\n\tlong_name\x18\x01 \x01(\t\x12\x12\n\nshort_name\x18\x02 \x01(\t\x12;\n\x04role\x18\x03 \x01(\x0e\x32-.meshtastic.protobuf.Config.DeviceConfig.Role\x12\x34\n\x08hw_model\x18\x04 \x01(\x0e\x32\".meshtastic.protobuf.HardwareModel\x12\x18\n\x10\x66irmware_version\x18\x05 \x01(\t\x12\x41\n\x06region\x18\x06 \x01(\x0e\x32\x31.meshtastic.protobuf.Config.LoRaConfig.RegionCode\x12H\n\x0cmodem_preset\x18\x07 \x01(\x0e\x32\x32.meshtastic.protobuf.Config.LoRaConfig.ModemPreset\x12\x1b\n\x13has_default_channel\x18\x08 \x01(\x08\x12\x12\n\nlatitude_i\x18\t \x01(\x0f\x12\x13\n\x0blongitude_i\x18\n \x01(\x0f\x12\x10\n\x08\x61ltitude\x18\x0b \x01(\x05\x12\x1a\n\x12position_precision\x18\x0c \x01(\r\x12\x1e\n\x16num_online_local_nodes\x18\r \x01(\rB_\n\x13\x63om.geeksville.meshB\nMQTTProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1emeshtastic/protobuf/mqtt.proto\x12\x13meshtastic.protobuf\x1a meshtastic/protobuf/config.proto\x1a\x1emeshtastic/protobuf/mesh.proto\x1a meshtastic/protobuf/nanopb.proto\"\x7f\n\x0fServiceEnvelope\x12\x36\n\x06packet\x18\x01 \x01(\x0b\x32\x1f.meshtastic.protobuf.MeshPacketB\x05\x92?\x02\x18\x04\x12\x19\n\nchannel_id\x18\x02 \x01(\tB\x05\x92?\x02\x18\x04\x12\x19\n\ngateway_id\x18\x03 \x01(\tB\x05\x92?\x02\x18\x04\"\x9f\x04\n\tMapReport\x12\x18\n\tlong_name\x18\x01 \x01(\tB\x05\x92?\x02\x08\x19\x12\x19\n\nshort_name\x18\x02 \x01(\tB\x05\x92?\x02\x08\x05\x12;\n\x04role\x18\x03 \x01(\x0e\x32-.meshtastic.protobuf.Config.DeviceConfig.Role\x12\x34\n\x08hw_model\x18\x04 \x01(\x0e\x32\".meshtastic.protobuf.HardwareModel\x12\x1f\n\x10\x66irmware_version\x18\x05 \x01(\tB\x05\x92?\x02\x08\x12\x12\x41\n\x06region\x18\x06 \x01(\x0e\x32\x31.meshtastic.protobuf.Config.LoRaConfig.RegionCode\x12H\n\x0cmodem_preset\x18\x07 \x01(\x0e\x32\x32.meshtastic.protobuf.Config.LoRaConfig.ModemPreset\x12\x1b\n\x13has_default_channel\x18\x08 \x01(\x08\x12\x12\n\nlatitude_i\x18\t \x01(\x0f\x12\x13\n\x0blongitude_i\x18\n \x01(\x0f\x12\x10\n\x08\x61ltitude\x18\x0b \x01(\x05\x12\x1a\n\x12position_precision\x18\x0c \x01(\r\x12%\n\x16num_online_local_nodes\x18\r \x01(\rB\x05\x92?\x02\x38\x10\x12!\n\x19has_opted_report_location\x18\x0e \x01(\x08\x42`\n\x14org.meshtastic.protoB\nMQTTProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.mqtt_pb2', _globals)
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\nMQTTProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
_globals['_SERVICEENVELOPE']._serialized_start=121
_globals['_SERVICEENVELOPE']._serialized_end=227
_globals['_MAPREPORT']._serialized_start=230
_globals['_MAPREPORT']._serialized_end=710
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\nMQTTProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
_SERVICEENVELOPE.fields_by_name['packet']._options = None
_SERVICEENVELOPE.fields_by_name['packet']._serialized_options = b'\222?\002\030\004'
_SERVICEENVELOPE.fields_by_name['channel_id']._options = None
_SERVICEENVELOPE.fields_by_name['channel_id']._serialized_options = b'\222?\002\030\004'
_SERVICEENVELOPE.fields_by_name['gateway_id']._options = None
_SERVICEENVELOPE.fields_by_name['gateway_id']._serialized_options = b'\222?\002\030\004'
_MAPREPORT.fields_by_name['long_name']._options = None
_MAPREPORT.fields_by_name['long_name']._serialized_options = b'\222?\002\010\031'
_MAPREPORT.fields_by_name['short_name']._options = None
_MAPREPORT.fields_by_name['short_name']._serialized_options = b'\222?\002\010\005'
_MAPREPORT.fields_by_name['firmware_version']._options = None
_MAPREPORT.fields_by_name['firmware_version']._serialized_options = b'\222?\002\010\022'
_MAPREPORT.fields_by_name['num_online_local_nodes']._options = None
_MAPREPORT.fields_by_name['num_online_local_nodes']._serialized_options = b'\222?\0028\020'
_globals['_SERVICEENVELOPE']._serialized_start=155
_globals['_SERVICEENVELOPE']._serialized_end=282
_globals['_MAPREPORT']._serialized_start=285
_globals['_MAPREPORT']._serialized_end=828
# @@protoc_insertion_point(module_scope)
+8 -1
View File
@@ -72,6 +72,7 @@ class MapReport(google.protobuf.message.Message):
ALTITUDE_FIELD_NUMBER: builtins.int
POSITION_PRECISION_FIELD_NUMBER: builtins.int
NUM_ONLINE_LOCAL_NODES_FIELD_NUMBER: builtins.int
HAS_OPTED_REPORT_LOCATION_FIELD_NUMBER: builtins.int
long_name: builtins.str
"""
A full name for this user, i.e. "Kevin Hester"
@@ -126,6 +127,11 @@ class MapReport(google.protobuf.message.Message):
"""
Number of online nodes (heard in the last 2 hours) this node has in its list that were received locally (not via MQTT)
"""
has_opted_report_location: builtins.bool
"""
User has opted in to share their location (map report) with the mqtt server
Controlled by map_report.should_report_location
"""
def __init__(
self,
*,
@@ -142,7 +148,8 @@ class MapReport(google.protobuf.message.Message):
altitude: builtins.int = ...,
position_precision: builtins.int = ...,
num_online_local_nodes: builtins.int = ...,
has_opted_report_location: builtins.bool = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["altitude", b"altitude", "firmware_version", b"firmware_version", "has_default_channel", b"has_default_channel", "hw_model", b"hw_model", "latitude_i", b"latitude_i", "long_name", b"long_name", "longitude_i", b"longitude_i", "modem_preset", b"modem_preset", "num_online_local_nodes", b"num_online_local_nodes", "position_precision", b"position_precision", "region", b"region", "role", b"role", "short_name", b"short_name"]) -> None: ...
def ClearField(self, field_name: typing.Literal["altitude", b"altitude", "firmware_version", b"firmware_version", "has_default_channel", b"has_default_channel", "has_opted_report_location", b"has_opted_report_location", "hw_model", b"hw_model", "latitude_i", b"latitude_i", "long_name", b"long_name", "longitude_i", b"longitude_i", "modem_preset", b"modem_preset", "num_online_local_nodes", b"num_online_local_nodes", "position_precision", b"position_precision", "region", b"region", "role", b"role", "short_name", b"short_name"]) -> None: ...
global___MapReport = MapReport
+35
View File
@@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: meshtastic/protobuf/nanopb.proto
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
from google.protobuf import descriptor_pb2 as google_dot_protobuf_dot_descriptor__pb2
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n meshtastic/protobuf/nanopb.proto\x1a google/protobuf/descriptor.proto\"\xa4\x07\n\rNanoPBOptions\x12\x10\n\x08max_size\x18\x01 \x01(\x05\x12\x12\n\nmax_length\x18\x0e \x01(\x05\x12\x11\n\tmax_count\x18\x02 \x01(\x05\x12&\n\x08int_size\x18\x07 \x01(\x0e\x32\x08.IntSize:\nIS_DEFAULT\x12$\n\x04type\x18\x03 \x01(\x0e\x32\n.FieldType:\nFT_DEFAULT\x12\x18\n\nlong_names\x18\x04 \x01(\x08:\x04true\x12\x1c\n\rpacked_struct\x18\x05 \x01(\x08:\x05\x66\x61lse\x12\x1a\n\x0bpacked_enum\x18\n \x01(\x08:\x05\x66\x61lse\x12\x1b\n\x0cskip_message\x18\x06 \x01(\x08:\x05\x66\x61lse\x12\x18\n\tno_unions\x18\x08 \x01(\x08:\x05\x66\x61lse\x12\r\n\x05msgid\x18\t \x01(\r\x12\x1e\n\x0f\x61nonymous_oneof\x18\x0b \x01(\x08:\x05\x66\x61lse\x12\x15\n\x06proto3\x18\x0c \x01(\x08:\x05\x66\x61lse\x12#\n\x14proto3_singular_msgs\x18\x15 \x01(\x08:\x05\x66\x61lse\x12\x1d\n\x0e\x65num_to_string\x18\r \x01(\x08:\x05\x66\x61lse\x12\x1b\n\x0c\x66ixed_length\x18\x0f \x01(\x08:\x05\x66\x61lse\x12\x1a\n\x0b\x66ixed_count\x18\x10 \x01(\x08:\x05\x66\x61lse\x12\x1e\n\x0fsubmsg_callback\x18\x16 \x01(\x08:\x05\x66\x61lse\x12/\n\x0cmangle_names\x18\x11 \x01(\x0e\x32\x11.TypenameMangling:\x06M_NONE\x12(\n\x11\x63\x61llback_datatype\x18\x12 \x01(\t:\rpb_callback_t\x12\x34\n\x11\x63\x61llback_function\x18\x13 \x01(\t:\x19pb_default_field_callback\x12\x30\n\x0e\x64\x65scriptorsize\x18\x14 \x01(\x0e\x32\x0f.DescriptorSize:\x07\x44S_AUTO\x12\x1a\n\x0b\x64\x65\x66\x61ult_has\x18\x17 \x01(\x08:\x05\x66\x61lse\x12\x0f\n\x07include\x18\x18 \x03(\t\x12\x0f\n\x07\x65xclude\x18\x1a \x03(\t\x12\x0f\n\x07package\x18\x19 \x01(\t\x12\x41\n\rtype_override\x18\x1b \x01(\x0e\x32*.google.protobuf.FieldDescriptorProto.Type\x12\x19\n\x0bsort_by_tag\x18\x1c \x01(\x08:\x04true\x12.\n\rfallback_type\x18\x1d \x01(\x0e\x32\n.FieldType:\x0b\x46T_CALLBACK*i\n\tFieldType\x12\x0e\n\nFT_DEFAULT\x10\x00\x12\x0f\n\x0b\x46T_CALLBACK\x10\x01\x12\x0e\n\nFT_POINTER\x10\x04\x12\r\n\tFT_STATIC\x10\x02\x12\r\n\tFT_IGNORE\x10\x03\x12\r\n\tFT_INLINE\x10\x05*D\n\x07IntSize\x12\x0e\n\nIS_DEFAULT\x10\x00\x12\x08\n\x04IS_8\x10\x08\x12\t\n\x05IS_16\x10\x10\x12\t\n\x05IS_32\x10 \x12\t\n\x05IS_64\x10@*Z\n\x10TypenameMangling\x12\n\n\x06M_NONE\x10\x00\x12\x13\n\x0fM_STRIP_PACKAGE\x10\x01\x12\r\n\tM_FLATTEN\x10\x02\x12\x16\n\x12M_PACKAGE_INITIALS\x10\x03*E\n\x0e\x44\x65scriptorSize\x12\x0b\n\x07\x44S_AUTO\x10\x00\x12\x08\n\x04\x44S_1\x10\x01\x12\x08\n\x04\x44S_2\x10\x02\x12\x08\n\x04\x44S_4\x10\x04\x12\x08\n\x04\x44S_8\x10\x08:E\n\x0enanopb_fileopt\x12\x1c.google.protobuf.FileOptions\x18\xf2\x07 \x01(\x0b\x32\x0e.NanoPBOptions:G\n\rnanopb_msgopt\x12\x1f.google.protobuf.MessageOptions\x18\xf2\x07 \x01(\x0b\x32\x0e.NanoPBOptions:E\n\x0enanopb_enumopt\x12\x1c.google.protobuf.EnumOptions\x18\xf2\x07 \x01(\x0b\x32\x0e.NanoPBOptions:>\n\x06nanopb\x12\x1d.google.protobuf.FieldOptions\x18\xf2\x07 \x01(\x0b\x32\x0e.NanoPBOptionsB>\n\x18\x66i.kapsi.koti.jpa.nanopbZ\"github.com/meshtastic/go/generated')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.nanopb_pb2', _globals)
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
DESCRIPTOR._serialized_options = b'\n\030fi.kapsi.koti.jpa.nanopbZ\"github.com/meshtastic/go/generated'
_globals['_FIELDTYPE']._serialized_start=1005
_globals['_FIELDTYPE']._serialized_end=1110
_globals['_INTSIZE']._serialized_start=1112
_globals['_INTSIZE']._serialized_end=1180
_globals['_TYPENAMEMANGLING']._serialized_start=1182
_globals['_TYPENAMEMANGLING']._serialized_end=1272
_globals['_DESCRIPTORSIZE']._serialized_start=1274
_globals['_DESCRIPTORSIZE']._serialized_end=1343
_globals['_NANOPBOPTIONS']._serialized_start=71
_globals['_NANOPBOPTIONS']._serialized_end=1003
# @@protoc_insertion_point(module_scope)
+324
View File
@@ -0,0 +1,324 @@
"""
@generated by mypy-protobuf. Do not edit manually!
isort:skip_file
Custom options for defining:
- Maximum size of string/bytes
- Maximum number of elements in array
These are used by nanopb to generate statically allocable structures
for memory-limited environments.
"""
import builtins
import collections.abc
import google.protobuf.descriptor
import google.protobuf.descriptor_pb2
import google.protobuf.internal.containers
import google.protobuf.internal.enum_type_wrapper
import google.protobuf.internal.extension_dict
import google.protobuf.message
import sys
import typing
if sys.version_info >= (3, 10):
import typing as typing_extensions
else:
import typing_extensions
DESCRIPTOR: google.protobuf.descriptor.FileDescriptor
class _FieldType:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
class _FieldTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_FieldType.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
FT_DEFAULT: _FieldType.ValueType # 0
"""Automatically decide field type, generate static field if possible."""
FT_CALLBACK: _FieldType.ValueType # 1
"""Always generate a callback field."""
FT_POINTER: _FieldType.ValueType # 4
"""Always generate a dynamically allocated field."""
FT_STATIC: _FieldType.ValueType # 2
"""Generate a static field or raise an exception if not possible."""
FT_IGNORE: _FieldType.ValueType # 3
"""Ignore the field completely."""
FT_INLINE: _FieldType.ValueType # 5
"""Legacy option, use the separate 'fixed_length' option instead"""
class FieldType(_FieldType, metaclass=_FieldTypeEnumTypeWrapper): ...
FT_DEFAULT: FieldType.ValueType # 0
"""Automatically decide field type, generate static field if possible."""
FT_CALLBACK: FieldType.ValueType # 1
"""Always generate a callback field."""
FT_POINTER: FieldType.ValueType # 4
"""Always generate a dynamically allocated field."""
FT_STATIC: FieldType.ValueType # 2
"""Generate a static field or raise an exception if not possible."""
FT_IGNORE: FieldType.ValueType # 3
"""Ignore the field completely."""
FT_INLINE: FieldType.ValueType # 5
"""Legacy option, use the separate 'fixed_length' option instead"""
global___FieldType = FieldType
class _IntSize:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
class _IntSizeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_IntSize.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
IS_DEFAULT: _IntSize.ValueType # 0
"""Default, 32/64bit based on type in .proto"""
IS_8: _IntSize.ValueType # 8
IS_16: _IntSize.ValueType # 16
IS_32: _IntSize.ValueType # 32
IS_64: _IntSize.ValueType # 64
class IntSize(_IntSize, metaclass=_IntSizeEnumTypeWrapper): ...
IS_DEFAULT: IntSize.ValueType # 0
"""Default, 32/64bit based on type in .proto"""
IS_8: IntSize.ValueType # 8
IS_16: IntSize.ValueType # 16
IS_32: IntSize.ValueType # 32
IS_64: IntSize.ValueType # 64
global___IntSize = IntSize
class _TypenameMangling:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
class _TypenameManglingEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_TypenameMangling.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
M_NONE: _TypenameMangling.ValueType # 0
"""Default, no typename mangling"""
M_STRIP_PACKAGE: _TypenameMangling.ValueType # 1
"""Strip current package name"""
M_FLATTEN: _TypenameMangling.ValueType # 2
"""Only use last path component"""
M_PACKAGE_INITIALS: _TypenameMangling.ValueType # 3
"""Replace the package name by the initials"""
class TypenameMangling(_TypenameMangling, metaclass=_TypenameManglingEnumTypeWrapper): ...
M_NONE: TypenameMangling.ValueType # 0
"""Default, no typename mangling"""
M_STRIP_PACKAGE: TypenameMangling.ValueType # 1
"""Strip current package name"""
M_FLATTEN: TypenameMangling.ValueType # 2
"""Only use last path component"""
M_PACKAGE_INITIALS: TypenameMangling.ValueType # 3
"""Replace the package name by the initials"""
global___TypenameMangling = TypenameMangling
class _DescriptorSize:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
class _DescriptorSizeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_DescriptorSize.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
DS_AUTO: _DescriptorSize.ValueType # 0
"""Select minimal size based on field type"""
DS_1: _DescriptorSize.ValueType # 1
"""1 word; up to 15 byte fields, no arrays"""
DS_2: _DescriptorSize.ValueType # 2
"""2 words; up to 4095 byte fields, 4095 entry arrays"""
DS_4: _DescriptorSize.ValueType # 4
"""4 words; up to 2^32-1 byte fields, 2^16-1 entry arrays"""
DS_8: _DescriptorSize.ValueType # 8
"""8 words; up to 2^32-1 entry arrays"""
class DescriptorSize(_DescriptorSize, metaclass=_DescriptorSizeEnumTypeWrapper): ...
DS_AUTO: DescriptorSize.ValueType # 0
"""Select minimal size based on field type"""
DS_1: DescriptorSize.ValueType # 1
"""1 word; up to 15 byte fields, no arrays"""
DS_2: DescriptorSize.ValueType # 2
"""2 words; up to 4095 byte fields, 4095 entry arrays"""
DS_4: DescriptorSize.ValueType # 4
"""4 words; up to 2^32-1 byte fields, 2^16-1 entry arrays"""
DS_8: DescriptorSize.ValueType # 8
"""8 words; up to 2^32-1 entry arrays"""
global___DescriptorSize = DescriptorSize
@typing.final
class NanoPBOptions(google.protobuf.message.Message):
"""This is the inner options message, which basically defines options for
a field. When it is used in message or file scope, it applies to all
fields.
"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
MAX_SIZE_FIELD_NUMBER: builtins.int
MAX_LENGTH_FIELD_NUMBER: builtins.int
MAX_COUNT_FIELD_NUMBER: builtins.int
INT_SIZE_FIELD_NUMBER: builtins.int
TYPE_FIELD_NUMBER: builtins.int
LONG_NAMES_FIELD_NUMBER: builtins.int
PACKED_STRUCT_FIELD_NUMBER: builtins.int
PACKED_ENUM_FIELD_NUMBER: builtins.int
SKIP_MESSAGE_FIELD_NUMBER: builtins.int
NO_UNIONS_FIELD_NUMBER: builtins.int
MSGID_FIELD_NUMBER: builtins.int
ANONYMOUS_ONEOF_FIELD_NUMBER: builtins.int
PROTO3_FIELD_NUMBER: builtins.int
PROTO3_SINGULAR_MSGS_FIELD_NUMBER: builtins.int
ENUM_TO_STRING_FIELD_NUMBER: builtins.int
FIXED_LENGTH_FIELD_NUMBER: builtins.int
FIXED_COUNT_FIELD_NUMBER: builtins.int
SUBMSG_CALLBACK_FIELD_NUMBER: builtins.int
MANGLE_NAMES_FIELD_NUMBER: builtins.int
CALLBACK_DATATYPE_FIELD_NUMBER: builtins.int
CALLBACK_FUNCTION_FIELD_NUMBER: builtins.int
DESCRIPTORSIZE_FIELD_NUMBER: builtins.int
DEFAULT_HAS_FIELD_NUMBER: builtins.int
INCLUDE_FIELD_NUMBER: builtins.int
EXCLUDE_FIELD_NUMBER: builtins.int
PACKAGE_FIELD_NUMBER: builtins.int
TYPE_OVERRIDE_FIELD_NUMBER: builtins.int
SORT_BY_TAG_FIELD_NUMBER: builtins.int
FALLBACK_TYPE_FIELD_NUMBER: builtins.int
max_size: builtins.int
"""Allocated size for 'bytes' and 'string' fields.
For string fields, this should include the space for null terminator.
"""
max_length: builtins.int
"""Maximum length for 'string' fields. Setting this is equivalent
to setting max_size to a value of length+1.
"""
max_count: builtins.int
"""Allocated number of entries in arrays ('repeated' fields)"""
int_size: global___IntSize.ValueType
"""Size of integer fields. Can save some memory if you don't need
full 32 bits for the value.
"""
type: global___FieldType.ValueType
"""Force type of field (callback or static allocation)"""
long_names: builtins.bool
"""Use long names for enums, i.e. EnumName_EnumValue."""
packed_struct: builtins.bool
"""Add 'packed' attribute to generated structs.
Note: this cannot be used on CPUs that break on unaligned
accesses to variables.
"""
packed_enum: builtins.bool
"""Add 'packed' attribute to generated enums."""
skip_message: builtins.bool
"""Skip this message"""
no_unions: builtins.bool
"""Generate oneof fields as normal optional fields instead of union."""
msgid: builtins.int
"""integer type tag for a message"""
anonymous_oneof: builtins.bool
"""decode oneof as anonymous union"""
proto3: builtins.bool
"""Proto3 singular field does not generate a "has_" flag"""
proto3_singular_msgs: builtins.bool
"""Force proto3 messages to have no "has_" flag.
This was default behavior until nanopb-0.4.0.
"""
enum_to_string: builtins.bool
"""Generate an enum->string mapping function (can take up lots of space)."""
fixed_length: builtins.bool
"""Generate bytes arrays with fixed length"""
fixed_count: builtins.bool
"""Generate repeated field with fixed count"""
submsg_callback: builtins.bool
"""Generate message-level callback that is called before decoding submessages.
This can be used to set callback fields for submsgs inside oneofs.
"""
mangle_names: global___TypenameMangling.ValueType
"""Shorten or remove package names from type names.
This option applies only on the file level.
"""
callback_datatype: builtins.str
"""Data type for storage associated with callback fields."""
callback_function: builtins.str
"""Callback function used for encoding and decoding.
Prior to nanopb-0.4.0, the callback was specified in per-field pb_callback_t
structure. This is still supported, but does not work inside e.g. oneof or pointer
fields. Instead, a new method allows specifying a per-message callback that
will be called for all callback fields in a message type.
"""
descriptorsize: global___DescriptorSize.ValueType
"""Select the size of field descriptors. This option has to be defined
for the whole message, not per-field. Usually automatic selection is
ok, but if it results in compilation errors you can increase the field
size here.
"""
default_has: builtins.bool
"""Set default value for has_ fields."""
package: builtins.str
"""Package name that applies only for nanopb."""
type_override: google.protobuf.descriptor_pb2.FieldDescriptorProto.Type.ValueType
"""Override type of the field in generated C code. Only to be used with related field types"""
sort_by_tag: builtins.bool
"""Due to historical reasons, nanopb orders fields in structs by their tag number
instead of the order in .proto. Set this to false to keep the .proto order.
The default value will probably change to false in nanopb-0.5.0.
"""
fallback_type: global___FieldType.ValueType
"""Set the FT_DEFAULT field conversion strategy.
A field that can become a static member of a c struct (e.g. int, bool, etc)
will be a a static field.
Fields with dynamic length are converted to either a pointer or a callback.
"""
@property
def include(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]:
"""Extra files to include in generated `.pb.h`"""
@property
def exclude(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]:
"""Automatic includes to exclude from generated `.pb.h`
Same as nanopb_generator.py command line flag -x.
"""
def __init__(
self,
*,
max_size: builtins.int | None = ...,
max_length: builtins.int | None = ...,
max_count: builtins.int | None = ...,
int_size: global___IntSize.ValueType | None = ...,
type: global___FieldType.ValueType | None = ...,
long_names: builtins.bool | None = ...,
packed_struct: builtins.bool | None = ...,
packed_enum: builtins.bool | None = ...,
skip_message: builtins.bool | None = ...,
no_unions: builtins.bool | None = ...,
msgid: builtins.int | None = ...,
anonymous_oneof: builtins.bool | None = ...,
proto3: builtins.bool | None = ...,
proto3_singular_msgs: builtins.bool | None = ...,
enum_to_string: builtins.bool | None = ...,
fixed_length: builtins.bool | None = ...,
fixed_count: builtins.bool | None = ...,
submsg_callback: builtins.bool | None = ...,
mangle_names: global___TypenameMangling.ValueType | None = ...,
callback_datatype: builtins.str | None = ...,
callback_function: builtins.str | None = ...,
descriptorsize: global___DescriptorSize.ValueType | None = ...,
default_has: builtins.bool | None = ...,
include: collections.abc.Iterable[builtins.str] | None = ...,
exclude: collections.abc.Iterable[builtins.str] | None = ...,
package: builtins.str | None = ...,
type_override: google.protobuf.descriptor_pb2.FieldDescriptorProto.Type.ValueType | None = ...,
sort_by_tag: builtins.bool | None = ...,
fallback_type: global___FieldType.ValueType | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["anonymous_oneof", b"anonymous_oneof", "callback_datatype", b"callback_datatype", "callback_function", b"callback_function", "default_has", b"default_has", "descriptorsize", b"descriptorsize", "enum_to_string", b"enum_to_string", "fallback_type", b"fallback_type", "fixed_count", b"fixed_count", "fixed_length", b"fixed_length", "int_size", b"int_size", "long_names", b"long_names", "mangle_names", b"mangle_names", "max_count", b"max_count", "max_length", b"max_length", "max_size", b"max_size", "msgid", b"msgid", "no_unions", b"no_unions", "package", b"package", "packed_enum", b"packed_enum", "packed_struct", b"packed_struct", "proto3", b"proto3", "proto3_singular_msgs", b"proto3_singular_msgs", "skip_message", b"skip_message", "sort_by_tag", b"sort_by_tag", "submsg_callback", b"submsg_callback", "type", b"type", "type_override", b"type_override"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["anonymous_oneof", b"anonymous_oneof", "callback_datatype", b"callback_datatype", "callback_function", b"callback_function", "default_has", b"default_has", "descriptorsize", b"descriptorsize", "enum_to_string", b"enum_to_string", "exclude", b"exclude", "fallback_type", b"fallback_type", "fixed_count", b"fixed_count", "fixed_length", b"fixed_length", "include", b"include", "int_size", b"int_size", "long_names", b"long_names", "mangle_names", b"mangle_names", "max_count", b"max_count", "max_length", b"max_length", "max_size", b"max_size", "msgid", b"msgid", "no_unions", b"no_unions", "package", b"package", "packed_enum", b"packed_enum", "packed_struct", b"packed_struct", "proto3", b"proto3", "proto3_singular_msgs", b"proto3_singular_msgs", "skip_message", b"skip_message", "sort_by_tag", b"sort_by_tag", "submsg_callback", b"submsg_callback", "type", b"type", "type_override", b"type_override"]) -> None: ...
global___NanoPBOptions = NanoPBOptions
NANOPB_FILEOPT_FIELD_NUMBER: builtins.int
NANOPB_MSGOPT_FIELD_NUMBER: builtins.int
NANOPB_ENUMOPT_FIELD_NUMBER: builtins.int
NANOPB_FIELD_NUMBER: builtins.int
nanopb_fileopt: google.protobuf.internal.extension_dict._ExtensionFieldDescriptor[google.protobuf.descriptor_pb2.FileOptions, global___NanoPBOptions]
nanopb_msgopt: google.protobuf.internal.extension_dict._ExtensionFieldDescriptor[google.protobuf.descriptor_pb2.MessageOptions, global___NanoPBOptions]
nanopb_enumopt: google.protobuf.internal.extension_dict._ExtensionFieldDescriptor[google.protobuf.descriptor_pb2.EnumOptions, global___NanoPBOptions]
nanopb: google.protobuf.internal.extension_dict._ExtensionFieldDescriptor[google.protobuf.descriptor_pb2.FieldOptions, global___NanoPBOptions]
+2 -2
View File
@@ -13,14 +13,14 @@ _sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"meshtastic/protobuf/paxcount.proto\x12\x13meshtastic.protobuf\"5\n\x08Paxcount\x12\x0c\n\x04wifi\x18\x01 \x01(\r\x12\x0b\n\x03\x62le\x18\x02 \x01(\r\x12\x0e\n\x06uptime\x18\x03 \x01(\rBc\n\x13\x63om.geeksville.meshB\x0ePaxcountProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"meshtastic/protobuf/paxcount.proto\x12\x13meshtastic.protobuf\"5\n\x08Paxcount\x12\x0c\n\x04wifi\x18\x01 \x01(\r\x12\x0b\n\x03\x62le\x18\x02 \x01(\r\x12\x0e\n\x06uptime\x18\x03 \x01(\rBd\n\x14org.meshtastic.protoB\x0ePaxcountProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.paxcount_pb2', _globals)
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\016PaxcountProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\016PaxcountProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
_globals['_PAXCOUNT']._serialized_start=59
_globals['_PAXCOUNT']._serialized_end=112
# @@protoc_insertion_point(module_scope)
+3 -3
View File
@@ -13,14 +13,14 @@ _sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"meshtastic/protobuf/portnums.proto\x12\x13meshtastic.protobuf*\xb1\x04\n\x07PortNum\x12\x0f\n\x0bUNKNOWN_APP\x10\x00\x12\x14\n\x10TEXT_MESSAGE_APP\x10\x01\x12\x17\n\x13REMOTE_HARDWARE_APP\x10\x02\x12\x10\n\x0cPOSITION_APP\x10\x03\x12\x10\n\x0cNODEINFO_APP\x10\x04\x12\x0f\n\x0bROUTING_APP\x10\x05\x12\r\n\tADMIN_APP\x10\x06\x12\x1f\n\x1bTEXT_MESSAGE_COMPRESSED_APP\x10\x07\x12\x10\n\x0cWAYPOINT_APP\x10\x08\x12\r\n\tAUDIO_APP\x10\t\x12\x18\n\x14\x44\x45TECTION_SENSOR_APP\x10\n\x12\r\n\tALERT_APP\x10\x0b\x12\r\n\tREPLY_APP\x10 \x12\x11\n\rIP_TUNNEL_APP\x10!\x12\x12\n\x0ePAXCOUNTER_APP\x10\"\x12\x0e\n\nSERIAL_APP\x10@\x12\x15\n\x11STORE_FORWARD_APP\x10\x41\x12\x12\n\x0eRANGE_TEST_APP\x10\x42\x12\x11\n\rTELEMETRY_APP\x10\x43\x12\x0b\n\x07ZPS_APP\x10\x44\x12\x11\n\rSIMULATOR_APP\x10\x45\x12\x12\n\x0eTRACEROUTE_APP\x10\x46\x12\x14\n\x10NEIGHBORINFO_APP\x10G\x12\x0f\n\x0b\x41TAK_PLUGIN\x10H\x12\x12\n\x0eMAP_REPORT_APP\x10I\x12\x13\n\x0fPOWERSTRESS_APP\x10J\x12\x10\n\x0bPRIVATE_APP\x10\x80\x02\x12\x13\n\x0e\x41TAK_FORWARDER\x10\x81\x02\x12\x08\n\x03MAX\x10\xff\x03\x42]\n\x13\x63om.geeksville.meshB\x08PortnumsZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"meshtastic/protobuf/portnums.proto\x12\x13meshtastic.protobuf*\xa4\x06\n\x07PortNum\x12\x0f\n\x0bUNKNOWN_APP\x10\x00\x12\x14\n\x10TEXT_MESSAGE_APP\x10\x01\x12\x17\n\x13REMOTE_HARDWARE_APP\x10\x02\x12\x10\n\x0cPOSITION_APP\x10\x03\x12\x10\n\x0cNODEINFO_APP\x10\x04\x12\x0f\n\x0bROUTING_APP\x10\x05\x12\r\n\tADMIN_APP\x10\x06\x12\x1f\n\x1bTEXT_MESSAGE_COMPRESSED_APP\x10\x07\x12\x10\n\x0cWAYPOINT_APP\x10\x08\x12\r\n\tAUDIO_APP\x10\t\x12\x18\n\x14\x44\x45TECTION_SENSOR_APP\x10\n\x12\r\n\tALERT_APP\x10\x0b\x12\x18\n\x14KEY_VERIFICATION_APP\x10\x0c\x12\x14\n\x10REMOTE_SHELL_APP\x10\r\x12\r\n\tREPLY_APP\x10 \x12\x11\n\rIP_TUNNEL_APP\x10!\x12\x12\n\x0ePAXCOUNTER_APP\x10\"\x12\x1e\n\x1aSTORE_FORWARD_PLUSPLUS_APP\x10#\x12\x13\n\x0fNODE_STATUS_APP\x10$\x12\x13\n\x0fMESH_BEACON_APP\x10%\x12\x0e\n\nSERIAL_APP\x10@\x12\x15\n\x11STORE_FORWARD_APP\x10\x41\x12\x12\n\x0eRANGE_TEST_APP\x10\x42\x12\x11\n\rTELEMETRY_APP\x10\x43\x12\x0b\n\x07ZPS_APP\x10\x44\x12\x11\n\rSIMULATOR_APP\x10\x45\x12\x12\n\x0eTRACEROUTE_APP\x10\x46\x12\x14\n\x10NEIGHBORINFO_APP\x10G\x12\x0f\n\x0b\x41TAK_PLUGIN\x10H\x12\x12\n\x0eMAP_REPORT_APP\x10I\x12\x13\n\x0fPOWERSTRESS_APP\x10J\x12\x12\n\x0eLORAWAN_BRIDGE\x10K\x12\x18\n\x14RETICULUM_TUNNEL_APP\x10L\x12\x0f\n\x0b\x43\x41YENNE_APP\x10M\x12\x12\n\x0e\x41TAK_PLUGIN_V2\x10N\x12\x10\n\x0cLORA_OTA_APP\x10O\x12\x12\n\x0eGROUPALARM_APP\x10p\x12\x10\n\x0bPRIVATE_APP\x10\x80\x02\x12\x13\n\x0e\x41TAK_FORWARDER\x10\x81\x02\x12\x08\n\x03MAX\x10\xff\x03\x42^\n\x14org.meshtastic.protoB\x08PortnumsZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.portnums_pb2', _globals)
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\010PortnumsZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\010PortnumsZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
_globals['_PORTNUM']._serialized_start=60
_globals['_PORTNUM']._serialized_end=621
_globals['_PORTNUM']._serialized_end=864
# @@protoc_insertion_point(module_scope)
+124
View File
@@ -97,6 +97,14 @@ class _PortNumEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTy
"""
Same as Text Message but used for critical alerts.
"""
KEY_VERIFICATION_APP: _PortNum.ValueType # 12
"""
Module/port for handling key verification requests.
"""
REMOTE_SHELL_APP: _PortNum.ValueType # 13
"""
Module/port for handling primitive remote shell access.
"""
REPLY_APP: _PortNum.ValueType # 32
"""
Provides a 'ping' service that replies to any packet it receives.
@@ -113,6 +121,27 @@ class _PortNumEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTy
Paxcounter lib included in the firmware
ENCODING: protobuf
"""
STORE_FORWARD_PLUSPLUS_APP: _PortNum.ValueType # 35
"""
Store and Forward++ module included in the firmware
ENCODING: protobuf
This module is specifically for Native Linux nodes, and provides a Git-style
chain of messages.
"""
NODE_STATUS_APP: _PortNum.ValueType # 36
"""
Node Status module
ENCODING: protobuf
This module allows setting an extra string of status for a node.
Broadcasts on change and on a timer, possibly once a day.
"""
MESH_BEACON_APP: _PortNum.ValueType # 37
"""
Beacon module broadcast packets.
ENCODING: protobuf
Periodically broadcast by nodes in beacon mode; received by nodes with MeshBeaconConfig.FLAG_LISTEN_ENABLED.
Carries a text message plus optional channel/preset offers for client apps.
"""
SERIAL_APP: _PortNum.ValueType # 64
"""
Provides a hardware serial interface to send and receive from the Meshtastic network.
@@ -179,6 +208,39 @@ class _PortNumEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTy
"""
PowerStress based monitoring support (for automated power consumption testing)
"""
LORAWAN_BRIDGE: _PortNum.ValueType # 75
"""
LoraWAN Payload Transport
ENCODING: compact binary LoRaWAN uplink (10-byte RF metadata + PHY payload) - see LoRaWANBridgeModule
"""
RETICULUM_TUNNEL_APP: _PortNum.ValueType # 76
"""
Reticulum Network Stack Tunnel App
ENCODING: Fragmented RNS Packet. Handled by Meshtastic RNS interface
"""
CAYENNE_APP: _PortNum.ValueType # 77
"""
App for transporting Cayenne Low Power Payload, popular for LoRaWAN sensor nodes. Offers ability to send
arbitrary telemetry over meshtastic that is not covered by telemetry.proto
ENCODING: CayenneLLP
"""
ATAK_PLUGIN_V2: _PortNum.ValueType # 78
"""
ATAK Plugin V2
Portnum for payloads from the official Meshtastic ATAK plugin using
TAKPacketV2 with zstd dictionary compression.
"""
LORA_OTA_APP: _PortNum.ValueType # 79
"""signed firmware updates over lora.
ENCODING: binary (ota-common transport frames)
"""
GROUPALARM_APP: _PortNum.ValueType # 112
"""
GroupAlarm integration
Used for transporting GroupAlarm-related messages between Meshtastic nodes
and companion applications/services.
"""
PRIVATE_APP: _PortNum.ValueType # 256
"""
Private applications should use portnums >= 256.
@@ -286,6 +348,14 @@ ALERT_APP: PortNum.ValueType # 11
"""
Same as Text Message but used for critical alerts.
"""
KEY_VERIFICATION_APP: PortNum.ValueType # 12
"""
Module/port for handling key verification requests.
"""
REMOTE_SHELL_APP: PortNum.ValueType # 13
"""
Module/port for handling primitive remote shell access.
"""
REPLY_APP: PortNum.ValueType # 32
"""
Provides a 'ping' service that replies to any packet it receives.
@@ -302,6 +372,27 @@ PAXCOUNTER_APP: PortNum.ValueType # 34
Paxcounter lib included in the firmware
ENCODING: protobuf
"""
STORE_FORWARD_PLUSPLUS_APP: PortNum.ValueType # 35
"""
Store and Forward++ module included in the firmware
ENCODING: protobuf
This module is specifically for Native Linux nodes, and provides a Git-style
chain of messages.
"""
NODE_STATUS_APP: PortNum.ValueType # 36
"""
Node Status module
ENCODING: protobuf
This module allows setting an extra string of status for a node.
Broadcasts on change and on a timer, possibly once a day.
"""
MESH_BEACON_APP: PortNum.ValueType # 37
"""
Beacon module broadcast packets.
ENCODING: protobuf
Periodically broadcast by nodes in beacon mode; received by nodes with MeshBeaconConfig.FLAG_LISTEN_ENABLED.
Carries a text message plus optional channel/preset offers for client apps.
"""
SERIAL_APP: PortNum.ValueType # 64
"""
Provides a hardware serial interface to send and receive from the Meshtastic network.
@@ -368,6 +459,39 @@ POWERSTRESS_APP: PortNum.ValueType # 74
"""
PowerStress based monitoring support (for automated power consumption testing)
"""
LORAWAN_BRIDGE: PortNum.ValueType # 75
"""
LoraWAN Payload Transport
ENCODING: compact binary LoRaWAN uplink (10-byte RF metadata + PHY payload) - see LoRaWANBridgeModule
"""
RETICULUM_TUNNEL_APP: PortNum.ValueType # 76
"""
Reticulum Network Stack Tunnel App
ENCODING: Fragmented RNS Packet. Handled by Meshtastic RNS interface
"""
CAYENNE_APP: PortNum.ValueType # 77
"""
App for transporting Cayenne Low Power Payload, popular for LoRaWAN sensor nodes. Offers ability to send
arbitrary telemetry over meshtastic that is not covered by telemetry.proto
ENCODING: CayenneLLP
"""
ATAK_PLUGIN_V2: PortNum.ValueType # 78
"""
ATAK Plugin V2
Portnum for payloads from the official Meshtastic ATAK plugin using
TAKPacketV2 with zstd dictionary compression.
"""
LORA_OTA_APP: PortNum.ValueType # 79
"""signed firmware updates over lora.
ENCODING: binary (ota-common transport frames)
"""
GROUPALARM_APP: PortNum.ValueType # 112
"""
GroupAlarm integration
Used for transporting GroupAlarm-related messages between Meshtastic nodes
and companion applications/services.
"""
PRIVATE_APP: PortNum.ValueType # 256
"""
Private applications should use portnums >= 256.
+2 -2
View File
@@ -13,14 +13,14 @@ _sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"meshtastic/protobuf/powermon.proto\x12\x13meshtastic.protobuf\"\xe0\x01\n\x08PowerMon\"\xd3\x01\n\x05State\x12\x08\n\x04None\x10\x00\x12\x11\n\rCPU_DeepSleep\x10\x01\x12\x12\n\x0e\x43PU_LightSleep\x10\x02\x12\x0c\n\x08Vext1_On\x10\x04\x12\r\n\tLora_RXOn\x10\x08\x12\r\n\tLora_TXOn\x10\x10\x12\x11\n\rLora_RXActive\x10 \x12\t\n\x05\x42T_On\x10@\x12\x0b\n\x06LED_On\x10\x80\x01\x12\x0e\n\tScreen_On\x10\x80\x02\x12\x13\n\x0eScreen_Drawing\x10\x80\x04\x12\x0c\n\x07Wifi_On\x10\x80\x08\x12\x0f\n\nGPS_Active\x10\x80\x10\"\x88\x03\n\x12PowerStressMessage\x12;\n\x03\x63md\x18\x01 \x01(\x0e\x32..meshtastic.protobuf.PowerStressMessage.Opcode\x12\x13\n\x0bnum_seconds\x18\x02 \x01(\x02\"\x9f\x02\n\x06Opcode\x12\t\n\x05UNSET\x10\x00\x12\x0e\n\nPRINT_INFO\x10\x01\x12\x0f\n\x0b\x46ORCE_QUIET\x10\x02\x12\r\n\tEND_QUIET\x10\x03\x12\r\n\tSCREEN_ON\x10\x10\x12\x0e\n\nSCREEN_OFF\x10\x11\x12\x0c\n\x08\x43PU_IDLE\x10 \x12\x11\n\rCPU_DEEPSLEEP\x10!\x12\x0e\n\nCPU_FULLON\x10\"\x12\n\n\x06LED_ON\x10\x30\x12\x0b\n\x07LED_OFF\x10\x31\x12\x0c\n\x08LORA_OFF\x10@\x12\x0b\n\x07LORA_TX\x10\x41\x12\x0b\n\x07LORA_RX\x10\x42\x12\n\n\x06\x42T_OFF\x10P\x12\t\n\x05\x42T_ON\x10Q\x12\x0c\n\x08WIFI_OFF\x10`\x12\x0b\n\x07WIFI_ON\x10\x61\x12\x0b\n\x07GPS_OFF\x10p\x12\n\n\x06GPS_ON\x10qBc\n\x13\x63om.geeksville.meshB\x0ePowerMonProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"meshtastic/protobuf/powermon.proto\x12\x13meshtastic.protobuf\"\xe0\x01\n\x08PowerMon\"\xd3\x01\n\x05State\x12\x08\n\x04None\x10\x00\x12\x11\n\rCPU_DeepSleep\x10\x01\x12\x12\n\x0e\x43PU_LightSleep\x10\x02\x12\x0c\n\x08Vext1_On\x10\x04\x12\r\n\tLora_RXOn\x10\x08\x12\r\n\tLora_TXOn\x10\x10\x12\x11\n\rLora_RXActive\x10 \x12\t\n\x05\x42T_On\x10@\x12\x0b\n\x06LED_On\x10\x80\x01\x12\x0e\n\tScreen_On\x10\x80\x02\x12\x13\n\x0eScreen_Drawing\x10\x80\x04\x12\x0c\n\x07Wifi_On\x10\x80\x08\x12\x0f\n\nGPS_Active\x10\x80\x10\"\x88\x03\n\x12PowerStressMessage\x12;\n\x03\x63md\x18\x01 \x01(\x0e\x32..meshtastic.protobuf.PowerStressMessage.Opcode\x12\x13\n\x0bnum_seconds\x18\x02 \x01(\x02\"\x9f\x02\n\x06Opcode\x12\t\n\x05UNSET\x10\x00\x12\x0e\n\nPRINT_INFO\x10\x01\x12\x0f\n\x0b\x46ORCE_QUIET\x10\x02\x12\r\n\tEND_QUIET\x10\x03\x12\r\n\tSCREEN_ON\x10\x10\x12\x0e\n\nSCREEN_OFF\x10\x11\x12\x0c\n\x08\x43PU_IDLE\x10 \x12\x11\n\rCPU_DEEPSLEEP\x10!\x12\x0e\n\nCPU_FULLON\x10\"\x12\n\n\x06LED_ON\x10\x30\x12\x0b\n\x07LED_OFF\x10\x31\x12\x0c\n\x08LORA_OFF\x10@\x12\x0b\n\x07LORA_TX\x10\x41\x12\x0b\n\x07LORA_RX\x10\x42\x12\n\n\x06\x42T_OFF\x10P\x12\t\n\x05\x42T_ON\x10Q\x12\x0c\n\x08WIFI_OFF\x10`\x12\x0b\n\x07WIFI_ON\x10\x61\x12\x0b\n\x07GPS_OFF\x10p\x12\n\n\x06GPS_ON\x10qBd\n\x14org.meshtastic.protoB\x0ePowerMonProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.powermon_pb2', _globals)
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\016PowerMonProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\016PowerMonProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
_globals['_POWERMON']._serialized_start=60
_globals['_POWERMON']._serialized_end=284
_globals['_POWERMON_STATE']._serialized_start=73
+2 -2
View File
@@ -13,14 +13,14 @@ _sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n)meshtastic/protobuf/remote_hardware.proto\x12\x13meshtastic.protobuf\"\xdf\x01\n\x0fHardwareMessage\x12\x37\n\x04type\x18\x01 \x01(\x0e\x32).meshtastic.protobuf.HardwareMessage.Type\x12\x11\n\tgpio_mask\x18\x02 \x01(\x04\x12\x12\n\ngpio_value\x18\x03 \x01(\x04\"l\n\x04Type\x12\t\n\x05UNSET\x10\x00\x12\x0f\n\x0bWRITE_GPIOS\x10\x01\x12\x0f\n\x0bWATCH_GPIOS\x10\x02\x12\x11\n\rGPIOS_CHANGED\x10\x03\x12\x0e\n\nREAD_GPIOS\x10\x04\x12\x14\n\x10READ_GPIOS_REPLY\x10\x05\x42\x63\n\x13\x63om.geeksville.meshB\x0eRemoteHardwareZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n)meshtastic/protobuf/remote_hardware.proto\x12\x13meshtastic.protobuf\"\xdf\x01\n\x0fHardwareMessage\x12\x37\n\x04type\x18\x01 \x01(\x0e\x32).meshtastic.protobuf.HardwareMessage.Type\x12\x11\n\tgpio_mask\x18\x02 \x01(\x04\x12\x12\n\ngpio_value\x18\x03 \x01(\x04\"l\n\x04Type\x12\t\n\x05UNSET\x10\x00\x12\x0f\n\x0bWRITE_GPIOS\x10\x01\x12\x0f\n\x0bWATCH_GPIOS\x10\x02\x12\x11\n\rGPIOS_CHANGED\x10\x03\x12\x0e\n\nREAD_GPIOS\x10\x04\x12\x14\n\x10READ_GPIOS_REPLY\x10\x05\x42\x64\n\x14org.meshtastic.protoB\x0eRemoteHardwareZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.remote_hardware_pb2', _globals)
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\016RemoteHardwareZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\016RemoteHardwareZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
_globals['_HARDWAREMESSAGE']._serialized_start=67
_globals['_HARDWAREMESSAGE']._serialized_end=290
_globals['_HARDWAREMESSAGE_TYPE']._serialized_start=182
+7 -4
View File
@@ -11,16 +11,19 @@ from google.protobuf.internal import builder as _builder
_sym_db = _symbol_database.Default()
from meshtastic.protobuf import nanopb_pb2 as meshtastic_dot_protobuf_dot_nanopb__pb2
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1fmeshtastic/protobuf/rtttl.proto\x12\x13meshtastic.protobuf\"\x1f\n\x0bRTTTLConfig\x12\x10\n\x08ringtone\x18\x01 \x01(\tBf\n\x13\x63om.geeksville.meshB\x11RTTTLConfigProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1fmeshtastic/protobuf/rtttl.proto\x12\x13meshtastic.protobuf\x1a meshtastic/protobuf/nanopb.proto\"\'\n\x0bRTTTLConfig\x12\x18\n\x08ringtone\x18\x01 \x01(\tB\x06\x92?\x03\x08\xe7\x01\x42g\n\x14org.meshtastic.protoB\x11RTTTLConfigProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.rtttl_pb2', _globals)
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\021RTTTLConfigProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
_globals['_RTTTLCONFIG']._serialized_start=56
_globals['_RTTTLCONFIG']._serialized_end=87
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\021RTTTLConfigProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
_RTTTLCONFIG.fields_by_name['ringtone']._options = None
_RTTTLCONFIG.fields_by_name['ringtone']._serialized_options = b'\222?\003\010\347\001'
_globals['_RTTTLCONFIG']._serialized_start=90
_globals['_RTTTLCONFIG']._serialized_end=129
# @@protoc_insertion_point(module_scope)
+39
View File
@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: meshtastic/protobuf/serial_hal.proto
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
from meshtastic.protobuf import nanopb_pb2 as meshtastic_dot_protobuf_dot_nanopb__pb2
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n$meshtastic/protobuf/serial_hal.proto\x12\x13meshtastic.protobuf\x1a meshtastic/protobuf/nanopb.proto\"\xb3\x02\n\x10SerialHalCommand\x12\x16\n\x0etransaction_id\x18\x01 \x01(\r\x12\x38\n\x04type\x18\x02 \x01(\x0e\x32*.meshtastic.protobuf.SerialHalCommand.Type\x12\x0b\n\x03pin\x18\x03 \x01(\r\x12\r\n\x05value\x18\x04 \x01(\r\x12\x0c\n\x04mode\x18\x05 \x01(\r\x12\x14\n\x04\x64\x61ta\x18\x06 \x01(\x0c\x42\x06\x92?\x03\x08\x80\x04\"\x8c\x01\n\x04Type\x12\t\n\x05UNSET\x10\x00\x12\x0c\n\x08PIN_MODE\x10\x01\x12\x11\n\rDIGITAL_WRITE\x10\x02\x12\x10\n\x0c\x44IGITAL_READ\x10\x03\x12\x14\n\x10\x41TTACH_INTERRUPT\x10\x04\x12\x14\n\x10\x44\x45TACH_INTERRUPT\x10\x05\x12\x10\n\x0cSPI_TRANSFER\x10\x06\x12\x08\n\x04NOOP\x10\x07\"\xe4\x01\n\x11SerialHalResponse\x12\x16\n\x0etransaction_id\x18\x01 \x01(\r\x12=\n\x06result\x18\x02 \x01(\x0e\x32-.meshtastic.protobuf.SerialHalResponse.Result\x12\r\n\x05value\x18\x03 \x01(\r\x12\x14\n\x04\x64\x61ta\x18\x04 \x01(\x0c\x42\x06\x92?\x03\x08\x80\x04\x12\x14\n\x05\x65rror\x18\x05 \x01(\tB\x05\x92?\x02\x08P\"=\n\x06Result\x12\x06\n\x02OK\x10\x00\x12\t\n\x05\x45RROR\x10\x01\x12\x0f\n\x0b\x42\x41\x44_REQUEST\x10\x02\x12\x0f\n\x0bUNSUPPORTED\x10\x03\x42_\n\x14org.meshtastic.protoB\tSerialHalZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.serial_hal_pb2', _globals)
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\tSerialHalZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
_SERIALHALCOMMAND.fields_by_name['data']._options = None
_SERIALHALCOMMAND.fields_by_name['data']._serialized_options = b'\222?\003\010\200\004'
_SERIALHALRESPONSE.fields_by_name['data']._options = None
_SERIALHALRESPONSE.fields_by_name['data']._serialized_options = b'\222?\003\010\200\004'
_SERIALHALRESPONSE.fields_by_name['error']._options = None
_SERIALHALRESPONSE.fields_by_name['error']._serialized_options = b'\222?\002\010P'
_globals['_SERIALHALCOMMAND']._serialized_start=96
_globals['_SERIALHALCOMMAND']._serialized_end=403
_globals['_SERIALHALCOMMAND_TYPE']._serialized_start=263
_globals['_SERIALHALCOMMAND_TYPE']._serialized_end=403
_globals['_SERIALHALRESPONSE']._serialized_start=406
_globals['_SERIALHALRESPONSE']._serialized_end=634
_globals['_SERIALHALRESPONSE_RESULT']._serialized_start=573
_globals['_SERIALHALRESPONSE_RESULT']._serialized_end=634
# @@protoc_insertion_point(module_scope)
+140
View File
@@ -0,0 +1,140 @@
"""
@generated by mypy-protobuf. Do not edit manually!
isort:skip_file
"""
import builtins
import google.protobuf.descriptor
import google.protobuf.internal.enum_type_wrapper
import google.protobuf.message
import sys
import typing
if sys.version_info >= (3, 10):
import typing as typing_extensions
else:
import typing_extensions
DESCRIPTOR: google.protobuf.descriptor.FileDescriptor
@typing.final
class SerialHalCommand(google.protobuf.message.Message):
"""SerialHalCommand messages are sent from host to device over the SerialHal
framing stream. Responses normally come back as SerialHalResponse with the
same transaction_id.
Interrupt notifications are the one asynchronous exception: the device emits
an unsolicited SerialHalResponse with transaction_id == 0 and value == pin.
Host implementations should treat those frames as interrupt events rather
than replies to an outstanding request.
"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
class _Type:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
class _TypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[SerialHalCommand._Type.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
UNSET: SerialHalCommand._Type.ValueType # 0
PIN_MODE: SerialHalCommand._Type.ValueType # 1
DIGITAL_WRITE: SerialHalCommand._Type.ValueType # 2
DIGITAL_READ: SerialHalCommand._Type.ValueType # 3
ATTACH_INTERRUPT: SerialHalCommand._Type.ValueType # 4
DETACH_INTERRUPT: SerialHalCommand._Type.ValueType # 5
SPI_TRANSFER: SerialHalCommand._Type.ValueType # 6
NOOP: SerialHalCommand._Type.ValueType # 7
class Type(_Type, metaclass=_TypeEnumTypeWrapper): ...
UNSET: SerialHalCommand.Type.ValueType # 0
PIN_MODE: SerialHalCommand.Type.ValueType # 1
DIGITAL_WRITE: SerialHalCommand.Type.ValueType # 2
DIGITAL_READ: SerialHalCommand.Type.ValueType # 3
ATTACH_INTERRUPT: SerialHalCommand.Type.ValueType # 4
DETACH_INTERRUPT: SerialHalCommand.Type.ValueType # 5
SPI_TRANSFER: SerialHalCommand.Type.ValueType # 6
NOOP: SerialHalCommand.Type.ValueType # 7
TRANSACTION_ID_FIELD_NUMBER: builtins.int
TYPE_FIELD_NUMBER: builtins.int
PIN_FIELD_NUMBER: builtins.int
VALUE_FIELD_NUMBER: builtins.int
MODE_FIELD_NUMBER: builtins.int
DATA_FIELD_NUMBER: builtins.int
transaction_id: builtins.int
"""Host-assigned request id. Replies echo this id back in
SerialHalResponse.transaction_id.
"""
type: global___SerialHalCommand.Type.ValueType
pin: builtins.int
value: builtins.int
mode: builtins.int
data: builtins.bytes
def __init__(
self,
*,
transaction_id: builtins.int = ...,
type: global___SerialHalCommand.Type.ValueType = ...,
pin: builtins.int = ...,
value: builtins.int = ...,
mode: builtins.int = ...,
data: builtins.bytes = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["data", b"data", "mode", b"mode", "pin", b"pin", "transaction_id", b"transaction_id", "type", b"type", "value", b"value"]) -> None: ...
global___SerialHalCommand = SerialHalCommand
@typing.final
class SerialHalResponse(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
class _Result:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
class _ResultEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[SerialHalResponse._Result.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
OK: SerialHalResponse._Result.ValueType # 0
ERROR: SerialHalResponse._Result.ValueType # 1
BAD_REQUEST: SerialHalResponse._Result.ValueType # 2
UNSUPPORTED: SerialHalResponse._Result.ValueType # 3
class Result(_Result, metaclass=_ResultEnumTypeWrapper): ...
OK: SerialHalResponse.Result.ValueType # 0
ERROR: SerialHalResponse.Result.ValueType # 1
BAD_REQUEST: SerialHalResponse.Result.ValueType # 2
UNSUPPORTED: SerialHalResponse.Result.ValueType # 3
TRANSACTION_ID_FIELD_NUMBER: builtins.int
RESULT_FIELD_NUMBER: builtins.int
VALUE_FIELD_NUMBER: builtins.int
DATA_FIELD_NUMBER: builtins.int
ERROR_FIELD_NUMBER: builtins.int
transaction_id: builtins.int
"""Matches the originating SerialHalCommand.transaction_id for normal
request/response traffic.
A value of 0 indicates an unsolicited interrupt notification generated by
the device. In that case, the host should interpret value as the GPIO pin
that triggered.
"""
result: global___SerialHalResponse.Result.ValueType
value: builtins.int
"""Used by DIGITAL_READ replies and interrupt notifications. For interrupt
notifications (transaction_id == 0), this carries the pin number.
"""
data: builtins.bytes
error: builtins.str
def __init__(
self,
*,
transaction_id: builtins.int = ...,
result: global___SerialHalResponse.Result.ValueType = ...,
value: builtins.int = ...,
data: builtins.bytes = ...,
error: builtins.str = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["data", b"data", "error", b"error", "result", b"result", "transaction_id", b"transaction_id", "value", b"value"]) -> None: ...
global___SerialHalResponse = SerialHalResponse
+15 -12
View File
@@ -11,24 +11,27 @@ from google.protobuf.internal import builder as _builder
_sym_db = _symbol_database.Default()
from meshtastic.protobuf import nanopb_pb2 as meshtastic_dot_protobuf_dot_nanopb__pb2
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n&meshtastic/protobuf/storeforward.proto\x12\x13meshtastic.protobuf\"\xc0\x07\n\x0fStoreAndForward\x12@\n\x02rr\x18\x01 \x01(\x0e\x32\x34.meshtastic.protobuf.StoreAndForward.RequestResponse\x12@\n\x05stats\x18\x02 \x01(\x0b\x32/.meshtastic.protobuf.StoreAndForward.StatisticsH\x00\x12?\n\x07history\x18\x03 \x01(\x0b\x32,.meshtastic.protobuf.StoreAndForward.HistoryH\x00\x12\x43\n\theartbeat\x18\x04 \x01(\x0b\x32..meshtastic.protobuf.StoreAndForward.HeartbeatH\x00\x12\x0e\n\x04text\x18\x05 \x01(\x0cH\x00\x1a\xcd\x01\n\nStatistics\x12\x16\n\x0emessages_total\x18\x01 \x01(\r\x12\x16\n\x0emessages_saved\x18\x02 \x01(\r\x12\x14\n\x0cmessages_max\x18\x03 \x01(\r\x12\x0f\n\x07up_time\x18\x04 \x01(\r\x12\x10\n\x08requests\x18\x05 \x01(\r\x12\x18\n\x10requests_history\x18\x06 \x01(\r\x12\x11\n\theartbeat\x18\x07 \x01(\x08\x12\x12\n\nreturn_max\x18\x08 \x01(\r\x12\x15\n\rreturn_window\x18\t \x01(\r\x1aI\n\x07History\x12\x18\n\x10history_messages\x18\x01 \x01(\r\x12\x0e\n\x06window\x18\x02 \x01(\r\x12\x14\n\x0clast_request\x18\x03 \x01(\r\x1a.\n\tHeartbeat\x12\x0e\n\x06period\x18\x01 \x01(\r\x12\x11\n\tsecondary\x18\x02 \x01(\r\"\xbc\x02\n\x0fRequestResponse\x12\t\n\x05UNSET\x10\x00\x12\x10\n\x0cROUTER_ERROR\x10\x01\x12\x14\n\x10ROUTER_HEARTBEAT\x10\x02\x12\x0f\n\x0bROUTER_PING\x10\x03\x12\x0f\n\x0bROUTER_PONG\x10\x04\x12\x0f\n\x0bROUTER_BUSY\x10\x05\x12\x12\n\x0eROUTER_HISTORY\x10\x06\x12\x10\n\x0cROUTER_STATS\x10\x07\x12\x16\n\x12ROUTER_TEXT_DIRECT\x10\x08\x12\x19\n\x15ROUTER_TEXT_BROADCAST\x10\t\x12\x10\n\x0c\x43LIENT_ERROR\x10@\x12\x12\n\x0e\x43LIENT_HISTORY\x10\x41\x12\x10\n\x0c\x43LIENT_STATS\x10\x42\x12\x0f\n\x0b\x43LIENT_PING\x10\x43\x12\x0f\n\x0b\x43LIENT_PONG\x10\x44\x12\x10\n\x0c\x43LIENT_ABORT\x10jB\t\n\x07variantBj\n\x13\x63om.geeksville.meshB\x15StoreAndForwardProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n&meshtastic/protobuf/storeforward.proto\x12\x13meshtastic.protobuf\x1a meshtastic/protobuf/nanopb.proto\"\xc8\x07\n\x0fStoreAndForward\x12@\n\x02rr\x18\x01 \x01(\x0e\x32\x34.meshtastic.protobuf.StoreAndForward.RequestResponse\x12@\n\x05stats\x18\x02 \x01(\x0b\x32/.meshtastic.protobuf.StoreAndForward.StatisticsH\x00\x12?\n\x07history\x18\x03 \x01(\x0b\x32,.meshtastic.protobuf.StoreAndForward.HistoryH\x00\x12\x43\n\theartbeat\x18\x04 \x01(\x0b\x32..meshtastic.protobuf.StoreAndForward.HeartbeatH\x00\x12\x16\n\x04text\x18\x05 \x01(\x0c\x42\x06\x92?\x03\x08\xe9\x01H\x00\x1a\xcd\x01\n\nStatistics\x12\x16\n\x0emessages_total\x18\x01 \x01(\r\x12\x16\n\x0emessages_saved\x18\x02 \x01(\r\x12\x14\n\x0cmessages_max\x18\x03 \x01(\r\x12\x0f\n\x07up_time\x18\x04 \x01(\r\x12\x10\n\x08requests\x18\x05 \x01(\r\x12\x18\n\x10requests_history\x18\x06 \x01(\r\x12\x11\n\theartbeat\x18\x07 \x01(\x08\x12\x12\n\nreturn_max\x18\x08 \x01(\r\x12\x15\n\rreturn_window\x18\t \x01(\r\x1aI\n\x07History\x12\x18\n\x10history_messages\x18\x01 \x01(\r\x12\x0e\n\x06window\x18\x02 \x01(\r\x12\x14\n\x0clast_request\x18\x03 \x01(\r\x1a.\n\tHeartbeat\x12\x0e\n\x06period\x18\x01 \x01(\r\x12\x11\n\tsecondary\x18\x02 \x01(\r\"\xbc\x02\n\x0fRequestResponse\x12\t\n\x05UNSET\x10\x00\x12\x10\n\x0cROUTER_ERROR\x10\x01\x12\x14\n\x10ROUTER_HEARTBEAT\x10\x02\x12\x0f\n\x0bROUTER_PING\x10\x03\x12\x0f\n\x0bROUTER_PONG\x10\x04\x12\x0f\n\x0bROUTER_BUSY\x10\x05\x12\x12\n\x0eROUTER_HISTORY\x10\x06\x12\x10\n\x0cROUTER_STATS\x10\x07\x12\x16\n\x12ROUTER_TEXT_DIRECT\x10\x08\x12\x19\n\x15ROUTER_TEXT_BROADCAST\x10\t\x12\x10\n\x0c\x43LIENT_ERROR\x10@\x12\x12\n\x0e\x43LIENT_HISTORY\x10\x41\x12\x10\n\x0c\x43LIENT_STATS\x10\x42\x12\x0f\n\x0b\x43LIENT_PING\x10\x43\x12\x0f\n\x0b\x43LIENT_PONG\x10\x44\x12\x10\n\x0c\x43LIENT_ABORT\x10jB\t\n\x07variantBk\n\x14org.meshtastic.protoB\x15StoreAndForwardProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.storeforward_pb2', _globals)
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\025StoreAndForwardProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
_globals['_STOREANDFORWARD']._serialized_start=64
_globals['_STOREANDFORWARD']._serialized_end=1024
_globals['_STOREANDFORWARD_STATISTICS']._serialized_start=366
_globals['_STOREANDFORWARD_STATISTICS']._serialized_end=571
_globals['_STOREANDFORWARD_HISTORY']._serialized_start=573
_globals['_STOREANDFORWARD_HISTORY']._serialized_end=646
_globals['_STOREANDFORWARD_HEARTBEAT']._serialized_start=648
_globals['_STOREANDFORWARD_HEARTBEAT']._serialized_end=694
_globals['_STOREANDFORWARD_REQUESTRESPONSE']._serialized_start=697
_globals['_STOREANDFORWARD_REQUESTRESPONSE']._serialized_end=1013
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\025StoreAndForwardProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
_STOREANDFORWARD.fields_by_name['text']._options = None
_STOREANDFORWARD.fields_by_name['text']._serialized_options = b'\222?\003\010\351\001'
_globals['_STOREANDFORWARD']._serialized_start=98
_globals['_STOREANDFORWARD']._serialized_end=1066
_globals['_STOREANDFORWARD_STATISTICS']._serialized_start=408
_globals['_STOREANDFORWARD_STATISTICS']._serialized_end=613
_globals['_STOREANDFORWARD_HISTORY']._serialized_start=615
_globals['_STOREANDFORWARD_HISTORY']._serialized_end=688
_globals['_STOREANDFORWARD_HEARTBEAT']._serialized_start=690
_globals['_STOREANDFORWARD_HEARTBEAT']._serialized_end=736
_globals['_STOREANDFORWARD_REQUESTRESPONSE']._serialized_start=739
_globals['_STOREANDFORWARD_REQUESTRESPONSE']._serialized_end=1055
# @@protoc_insertion_point(module_scope)
+85 -20
View File
File diff suppressed because one or more lines are too long.
+903 -29
View File
File diff suppressed because it is too large. Load diff
+13 -6
View File
@@ -11,18 +11,25 @@ from google.protobuf.internal import builder as _builder
_sym_db = _symbol_database.Default()
from meshtastic.protobuf import nanopb_pb2 as meshtastic_dot_protobuf_dot_nanopb__pb2
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n meshtastic/protobuf/xmodem.proto\x12\x13meshtastic.protobuf\"\xbf\x01\n\x06XModem\x12\x34\n\x07\x63ontrol\x18\x01 \x01(\x0e\x32#.meshtastic.protobuf.XModem.Control\x12\x0b\n\x03seq\x18\x02 \x01(\r\x12\r\n\x05\x63rc16\x18\x03 \x01(\r\x12\x0e\n\x06\x62uffer\x18\x04 \x01(\x0c\"S\n\x07\x43ontrol\x12\x07\n\x03NUL\x10\x00\x12\x07\n\x03SOH\x10\x01\x12\x07\n\x03STX\x10\x02\x12\x07\n\x03\x45OT\x10\x04\x12\x07\n\x03\x41\x43K\x10\x06\x12\x07\n\x03NAK\x10\x15\x12\x07\n\x03\x43\x41N\x10\x18\x12\t\n\x05\x43TRLZ\x10\x1a\x42\x61\n\x13\x63om.geeksville.meshB\x0cXmodemProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n meshtastic/protobuf/xmodem.proto\x12\x13meshtastic.protobuf\x1a meshtastic/protobuf/nanopb.proto\"\xd5\x01\n\x06XModem\x12\x34\n\x07\x63ontrol\x18\x01 \x01(\x0e\x32#.meshtastic.protobuf.XModem.Control\x12\x12\n\x03seq\x18\x02 \x01(\rB\x05\x92?\x02\x38\x10\x12\x14\n\x05\x63rc16\x18\x03 \x01(\rB\x05\x92?\x02\x38\x10\x12\x16\n\x06\x62uffer\x18\x04 \x01(\x0c\x42\x06\x92?\x03\x08\x80\x01\"S\n\x07\x43ontrol\x12\x07\n\x03NUL\x10\x00\x12\x07\n\x03SOH\x10\x01\x12\x07\n\x03STX\x10\x02\x12\x07\n\x03\x45OT\x10\x04\x12\x07\n\x03\x41\x43K\x10\x06\x12\x07\n\x03NAK\x10\x15\x12\x07\n\x03\x43\x41N\x10\x18\x12\t\n\x05\x43TRLZ\x10\x1a\x42\x62\n\x14org.meshtastic.protoB\x0cXmodemProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.xmodem_pb2', _globals)
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\014XmodemProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
_globals['_XMODEM']._serialized_start=58
_globals['_XMODEM']._serialized_end=249
_globals['_XMODEM_CONTROL']._serialized_start=166
_globals['_XMODEM_CONTROL']._serialized_end=249
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\014XmodemProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
_XMODEM.fields_by_name['seq']._options = None
_XMODEM.fields_by_name['seq']._serialized_options = b'\222?\0028\020'
_XMODEM.fields_by_name['crc16']._options = None
_XMODEM.fields_by_name['crc16']._serialized_options = b'\222?\0028\020'
_XMODEM.fields_by_name['buffer']._options = None
_XMODEM.fields_by_name['buffer']._serialized_options = b'\222?\003\010\200\001'
_globals['_XMODEM']._serialized_start=92
_globals['_XMODEM']._serialized_end=305
_globals['_XMODEM_CONTROL']._serialized_start=222
_globals['_XMODEM_CONTROL']._serialized_end=305
# @@protoc_insertion_point(module_scope)
+5 -4
View File
@@ -7,10 +7,11 @@ from pubsub import pub # type: ignore[import-untyped]
from meshtastic.protobuf import portnums_pb2, remote_hardware_pb2
from meshtastic.util import our_exit
logger = logging.getLogger(__name__)
def onGPIOreceive(packet, interface) -> None:
"""Callback for received GPIO responses"""
logging.debug(f"packet:{packet} interface:{interface}")
logger.debug(f"packet:{packet} interface:{interface}")
gpioValue = 0
hw = packet["decoded"]["remotehw"]
if "gpioValue" in hw:
@@ -76,7 +77,7 @@ class RemoteHardwareClient:
Write the specified vals bits to the device GPIOs. Only bits in mask that
are 1 will be changed
"""
logging.debug(f"writeGPIOs nodeid:{nodeid} mask:{mask} vals:{vals}")
logger.debug(f"writeGPIOs nodeid:{nodeid} mask:{mask} vals:{vals}")
r = remote_hardware_pb2.HardwareMessage()
r.type = remote_hardware_pb2.HardwareMessage.Type.WRITE_GPIOS
r.gpio_mask = mask
@@ -85,7 +86,7 @@ class RemoteHardwareClient:
def readGPIOs(self, nodeid, mask, onResponse=None):
"""Read the specified bits from GPIO inputs on the device"""
logging.debug(f"readGPIOs nodeid:{nodeid} mask:{mask}")
logger.debug(f"readGPIOs nodeid:{nodeid} mask:{mask}")
r = remote_hardware_pb2.HardwareMessage()
r.type = remote_hardware_pb2.HardwareMessage.Type.READ_GPIOS
r.gpio_mask = mask
@@ -93,7 +94,7 @@ class RemoteHardwareClient:
def watchGPIOs(self, nodeid, mask):
"""Watch the specified bits from GPIO inputs on the device for changes"""
logging.debug(f"watchGPIOs nodeid:{nodeid} mask:{mask}")
logger.debug(f"watchGPIOs nodeid:{nodeid} mask:{mask}")
r = remote_hardware_pb2.HardwareMessage()
r.type = remote_hardware_pb2.HardwareMessage.Type.WATCH_GPIOS
r.gpio_mask = mask
+52 -22
View File
@@ -2,8 +2,9 @@
"""
# pylint: disable=R0917
import logging
import platform
import sys
import time
from io import TextIOWrapper
from typing import List, Optional
@@ -12,28 +13,33 @@ import serial # type: ignore[import-untyped]
import meshtastic.util
from meshtastic.stream_interface import StreamInterface
if platform.system() != "Windows":
import termios
logger = logging.getLogger(__name__)
class SerialInterface(StreamInterface):
"""Interface class for meshtastic devices over a serial link"""
def __init__(self, devPath: Optional[str]=None, debugOut=None, noProto: bool=False, connectNow: bool=True, noNodes: bool=False) -> None:
def __init__(
self,
devPath: Optional[str] = None,
debugOut=None,
noProto: bool = False,
connectNow: bool = True,
noNodes: bool = False,
timeout: int = 300
) -> None:
"""Constructor, opens a connection to a specified serial port, or if unspecified try to
find one Meshtastic device by probing
Keyword Arguments:
devPath {string} -- A filepath to a device, i.e. /dev/ttyUSB0 (default: {None})
debugOut {stream} -- If a stream is provided, any debug serial output from the device will be emitted to that stream. (default: {None})
timeout -- How long to wait for replies (default: 300 seconds)
"""
self.noProto = noProto
self.devPath: Optional[str] = devPath
if self.devPath is None:
ports: List[str] = meshtastic.util.findPorts(True)
logging.debug(f"ports:{ports}")
logger.debug(f"ports:{ports}")
if len(ports) == 0:
print("No Serial Meshtastic device detected, attempting TCP connection on localhost.")
return
@@ -44,27 +50,51 @@ class SerialInterface(StreamInterface):
else:
self.devPath = ports[0]
logging.debug(f"Connecting to {self.devPath}")
StreamInterface.__init__(
self, debugOut=debugOut, noProto=noProto, connectNow=connectNow, noNodes=noNodes, timeout=timeout
)
# first we need to set the HUPCL so the device will not reboot based on RTS and/or DTR
# see https://github.com/pyserial/pyserial/issues/124
if platform.system() != "Windows":
with open(self.devPath, encoding="utf8") as f:
attrs = termios.tcgetattr(f)
attrs[2] = attrs[2] & ~termios.HUPCL
termios.tcsetattr(f, termios.TCSAFLUSH, attrs)
f.close()
def connect(self) -> None:
logger.debug(f"Connecting to {self.devPath}")
dev_path = self.devPath
if dev_path is None:
raise RuntimeError("Serial device path is not set")
if sys.platform != "win32":
with open(dev_path, encoding="utf8") as f:
self._set_hupcl_with_termios(f)
time.sleep(0.1)
self.stream = serial.Serial(
self.devPath, 115200, exclusive=True, timeout=0.5, write_timeout=0
dev_path, 115200, exclusive=True, timeout=0.5, write_timeout=0
)
self.stream.flush() # type: ignore[attr-defined]
time.sleep(0.1)
StreamInterface.__init__(
self, debugOut=debugOut, noProto=noProto, connectNow=connectNow, noNodes=noNodes
)
super().connect()
def _set_hupcl_with_termios(self, f: TextIOWrapper):
"""first we need to set the HUPCL so the device will not reboot based on RTS and/or DTR
see https://github.com/pyserial/pyserial/issues/124
"""
if sys.platform == "win32":
return
import termios # pylint: disable=C0415,E0401
attrs = termios.tcgetattr(f)
attrs[2] = attrs[2] & ~termios.HUPCL
termios.tcsetattr(f, termios.TCSAFLUSH, attrs)
def __repr__(self):
rep = f"SerialInterface(devPath={self.devPath!r}"
if hasattr(self, 'debugOut') and self.debugOut is not None:
rep += f", debugOut={self.debugOut!r}"
if self.noProto:
rep += ", noProto=True"
if hasattr(self, 'noNodes') and self.noNodes:
rep += ", noNodes=True"
rep += ")"
return rep
def close(self) -> None:
"""Close a connection to the device"""
@@ -73,5 +103,5 @@ class SerialInterface(StreamInterface):
time.sleep(0.1)
self.stream.flush()
time.sleep(0.1)
logging.debug("Closing Serial stream")
logger.debug("Closing Serial stream")
StreamInterface.close(self)
+23 -16
View File
@@ -10,6 +10,7 @@ import time
from dataclasses import dataclass
from datetime import datetime
from functools import reduce
from pathlib import Path
from typing import Optional, List, Tuple
import parse # type: ignore[import-untyped]
@@ -22,6 +23,7 @@ from meshtastic.powermon import PowerMeter
from .arrow import FeatherWriter
logger = logging.getLogger(__name__)
def root_dir() -> str:
"""Return the root directory for slog files."""
@@ -29,9 +31,9 @@ def root_dir() -> str:
app_name = "meshtastic"
app_author = "meshtastic"
app_dir = platformdirs.user_data_dir(app_name, app_author)
dir_name = f"{app_dir}/slogs"
os.makedirs(dir_name, exist_ok=True)
return dir_name
dir_name = Path(app_dir, "slogs")
dir_name.mkdir(exist_ok=True, parents=True)
return str(dir_name)
@dataclass(init=False)
@@ -144,7 +146,7 @@ class StructuredLogger:
self.power_logger = power_logger
# Setup the arrow writer (and its schema)
self.writer = FeatherWriter(f"{dir_path}/slog")
self.writer = FeatherWriter(os.path.join(dir_path, "slog"))
all_fields = reduce(
(lambda x, y: x + y), map(lambda x: x.fields, log_defs.values())
)
@@ -164,7 +166,7 @@ class StructuredLogger:
self.raw_file: Optional[
io.TextIOWrapper
] = open( # pylint: disable=consider-using-with
f"{dir_path}/raw.txt", "w", encoding="utf8"
os.path.join(dir_path, "raw.txt"), "w", encoding="utf8"
)
# We need a closure here because the subscription API is very strict about exact arg matching
@@ -197,7 +199,7 @@ class StructuredLogger:
if m:
src = m.group(1)
args = m.group(2)
logging.debug(f"SLog {src}, args: {args}")
logger.debug(f"SLog {src}, args: {args}")
d = log_defs.get(src)
if d:
@@ -219,9 +221,9 @@ class StructuredLogger:
# If the last field is an empty string, remove it
del di[last_field[0]]
else:
logging.warning(f"Failed to parse slog {line} with {d.format}")
logger.warning(f"Failed to parse slog {line} with {d.format}")
else:
logging.warning(f"Unknown Structured Log: {line}")
logger.warning(f"Unknown Structured Log: {line}")
# Store our structured log record
if di or self.include_raw:
@@ -256,23 +258,28 @@ class LogSet:
if not dir_name:
app_dir = root_dir()
dir_name = f"{app_dir}/{datetime.now().strftime('%Y%m%d-%H%M%S')}"
os.makedirs(dir_name, exist_ok=True)
app_time_dir = Path(app_dir, datetime.now().strftime('%Y%m%d-%H%M%S'))
app_time_dir.mkdir(exist_ok=True)
dir_name = str(app_time_dir)
# Also make a 'latest' directory that always points to the most recent logs
latest_dir = Path(app_dir, "latest")
latest_dir.unlink(missing_ok=True)
# symlink might fail on some platforms, if it does fail silently
if os.path.exists(f"{app_dir}/latest"):
os.unlink(f"{app_dir}/latest")
os.symlink(dir_name, f"{app_dir}/latest", target_is_directory=True)
try:
latest_dir.symlink_to(dir_name, target_is_directory=True)
except OSError:
pass
self.dir_name = dir_name
logging.info(f"Writing slogs to {dir_name}")
logger.info(f"Writing slogs to {dir_name}")
self.power_logger: Optional[PowerLogger] = (
None
if not power_meter
else PowerLogger(power_meter, f"{self.dir_name}/power")
else PowerLogger(power_meter, os.path.join(self.dir_name, "power"))
)
self.slog_logger: Optional[StructuredLogger] = StructuredLogger(
@@ -286,7 +293,7 @@ class LogSet:
"""Close the log set."""
if self.slog_logger:
logging.info(f"Closing slogs in {self.dir_name}")
logger.info(f"Closing slogs in {self.dir_name}")
atexit.unregister(
self.atexit_handler
) # docs say it will silently ignore if not found
+53 -27
View File
@@ -1,5 +1,6 @@
"""Stream Interface base class
"""
import contextlib
import io
import logging
import threading
@@ -17,28 +18,36 @@ START1 = 0x94
START2 = 0xC3
HEADER_LEN = 4
MAX_TO_FROM_RADIO_SIZE = 512
logger = logging.getLogger(__name__)
class StreamInterface(MeshInterface):
"""Interface class for meshtastic devices over a stream link (serial, TCP, etc)"""
def __init__(self, debugOut: Optional[io.TextIOWrapper]=None, noProto: bool=False, connectNow: bool=True, noNodes: bool=False) -> None:
def __init__( # pylint: disable=R0917
self,
debugOut: Optional[io.TextIOWrapper] = None,
noProto: bool = False,
connectNow: bool = True,
noNodes: bool = False,
timeout: int = 300
) -> None:
"""Constructor, opens a connection to self.stream
Keyword Arguments:
debugOut {stream} -- If a stream is provided, any debug serial output from the
device will be emitted to that stream. (default: {None})
timeout -- How long to wait for replies (default: 300 seconds)
Raises:
Exception: [description]
Exception: [description]
RuntimeError: Raised if StreamInterface is instantiated when noProto is false.
"""
if not hasattr(self, "stream") and not noProto:
raise Exception( # pylint: disable=W0719
if not noProto and type(self) == StreamInterface: # pylint: disable=C0123
raise RuntimeError(
"StreamInterface is now abstract (to update existing code create SerialInterface instead)"
)
self.stream: Optional[serial.Serial] # only serial uses this, TCPInterface overrides the relevant methods instead
self.stream: Optional[serial.Serial] = None # only serial uses this, TCPInterface overrides the relevant methods instead
self._rxBuf = bytes() # empty
self._wantExit = False
@@ -48,13 +57,21 @@ class StreamInterface(MeshInterface):
# FIXME, figure out why daemon=True causes reader thread to exit too early
self._rxThread = threading.Thread(target=self.__reader, args=(), daemon=True, name="stream reader")
MeshInterface.__init__(self, debugOut=debugOut, noProto=noProto, noNodes=noNodes)
MeshInterface.__init__(self, debugOut=debugOut, noProto=noProto, noNodes=noNodes, timeout=timeout)
# Start the reader thread after superclass constructor completes init
if connectNow:
self.connect()
if not noProto:
self.waitForConfig()
try:
self.connect()
if not noProto:
self.waitForConfig()
except Exception:
# If the handshake raises, the caller never receives a reference
# to this instance and cannot call close() themselves. Clean up
# the reader thread + stream here so retries don't leak.
with contextlib.suppress(Exception):
self.close()
raise
def connect(self) -> None:
"""Connect to our radio
@@ -82,7 +99,7 @@ class StreamInterface(MeshInterface):
"""We override the superclass implementation to close our port"""
MeshInterface._disconnected(self)
logging.debug("Closing our port")
logger.debug("Closing our port")
# pylint: disable=E0203
if not self.stream is None:
# pylint: disable=E0203
@@ -111,23 +128,32 @@ class StreamInterface(MeshInterface):
def _sendToRadioImpl(self, toRadio) -> None:
"""Send a ToRadio protobuf to the device"""
logging.debug(f"Sending: {stripnl(toRadio)}")
logger.debug(f"Sending: {stripnl(toRadio)}")
b: bytes = toRadio.SerializeToString()
bufLen: int = len(b)
# We convert into a string, because the TCP code doesn't work with byte arrays
header: bytes = bytes([START1, START2, (bufLen >> 8) & 0xFF, bufLen & 0xFF])
logging.debug(f"sending header:{header!r} b:{b!r}")
logger.debug(f"sending header:{header!r} b:{b!r}")
self._writeBytes(header + b)
def close(self) -> None:
"""Close a connection to the device"""
logging.debug("Closing stream")
logger.debug("Closing stream")
MeshInterface.close(self)
# pyserial cancel_read doesn't seem to work, therefore we ask the
# reader thread to close things for us
self._wantExit = True
if self._rxThread != threading.current_thread():
self._rxThread.join() # wait for it to exit
try:
self._rxThread.join() # wait for it to exit
except RuntimeError:
# Thread was never started — happens when close() is invoked
# from a failed __init__ before connect() could spawn it.
# In this case there is no reader thread to close the stream.
if self.stream is not None:
with contextlib.suppress(Exception):
self.stream.close()
self.stream = None
def _handleLogByte(self, b):
"""Handle a byte that is part of a log message from the device."""
@@ -148,18 +174,18 @@ class StreamInterface(MeshInterface):
def __reader(self) -> None:
"""The reader thread that reads bytes from our stream"""
logging.debug("in __reader()")
logger.debug("in __reader()")
empty = bytes()
try:
while not self._wantExit:
# logging.debug("reading character")
# logger.debug("reading character")
b: Optional[bytes] = self._readBytes(1)
# logging.debug("In reader loop")
# logging.debug(f"read returned {b}")
# logger.debug("In reader loop")
# logger.debug(f"read returned {b}")
if b is not None and len(cast(bytes, b)) > 0:
c: int = b[0]
# logging.debug(f'c:{c}')
# logger.debug(f'c:{c}')
ptr: int = len(self._rxBuf)
# Assume we want to append this byte, fixme use bytearray instead
@@ -176,7 +202,7 @@ class StreamInterface(MeshInterface):
if c != START2:
self._rxBuf = empty # failed to find start2
elif ptr >= HEADER_LEN - 1: # we've at least got a header
# logging.debug('at least we received a header')
# logger.debug('at least we received a header')
# big endian length follows header
packetlen = (self._rxBuf[2] << 8) + self._rxBuf[3]
@@ -192,32 +218,32 @@ class StreamInterface(MeshInterface):
try:
self._handleFromRadio(self._rxBuf[HEADER_LEN:])
except Exception as ex:
logging.error(
logger.error(
f"Error while handling message from radio {ex}"
)
traceback.print_exc()
self._rxBuf = empty
else:
# logging.debug(f"timeout")
# logger.debug(f"timeout")
pass
except serial.SerialException as ex:
if (
not self._wantExit
): # We might intentionally get an exception during shutdown
logging.warning(
logger.warning(
f"Meshtastic serial port disconnected, disconnecting... {ex}"
)
except OSError as ex:
if (
not self._wantExit
): # We might intentionally get an exception during shutdown
logging.error(
logger.error(
f"Unexpected OSError, terminating meshtastic reader... {ex}"
)
except Exception as ex:
logging.error(
logger.error(
f"Unexpected exception, terminating meshtastic reader... {ex}"
)
finally:
logging.debug("reader is exiting")
logger.debug("reader is exiting")
self._disconnected()
+26
View File
@@ -207,6 +207,30 @@ nano_g1 = SupportedDevice(
usb_product_id_in_hex="55d4",
)
seeed_xiao_s3 = SupportedDevice(
name = "Seeed Xiao ESP32-S3",
version = "",
for_firmware="seeed-xiao-esp32s3",
baseport_on_linux="ttyACM",
baseport_on_mac="cu.usbmodem",
usb_vendor_id_in_hex="2886",
usb_product_id_in_hex="0059",
)
tdeck = SupportedDevice(
name="T-Deck",
version="",
for_firmware="t-deck", # Confirmed firmware identifier
device_class="esp32",
baseport_on_linux="ttyACM",
baseport_on_mac="cu.usbmodem",
baseport_on_windows="COM",
usb_vendor_id_in_hex="303a", # Espressif Systems (VERIFIED)
usb_product_id_in_hex="1001", # VERIFIED from actual device
)
supported_devices = [
tbeam_v0_7,
tbeam_v1_1,
@@ -226,4 +250,6 @@ supported_devices = [
rak4631_19003,
rak11200,
nano_g1,
seeed_xiao_s3,
tdeck, # T-Deck support added
]
+135 -41
View File
@@ -1,14 +1,24 @@
"""TCPInterface class for interfacing with http endpoint
"""
# pylint: disable=R0917
import contextlib
import logging
import socket
from typing import Optional, cast
import threading
import time
from typing import Optional
from meshtastic.stream_interface import StreamInterface
DEFAULT_TCP_PORT = 4403
# How long close() gives the device to consume what we last wrote before forcing
# the connection down. See close() for why this exists.
GRACEFUL_CLOSE_TIMEOUT = 0.25
logger = logging.getLogger(__name__)
class TCPInterface(StreamInterface):
"""Interface class for meshtastic devices over a TCP link"""
@@ -16,71 +26,155 @@ class TCPInterface(StreamInterface):
self,
hostname: str,
debugOut=None,
noProto: bool=False,
connectNow: bool=True,
portNumber: int=DEFAULT_TCP_PORT,
noNodes:bool=False,
noProto: bool = False,
connectNow: bool = True,
portNumber: int = DEFAULT_TCP_PORT,
noNodes: bool = False,
timeout: int = 300,
):
"""Constructor, opens a connection to a specified IP address/hostname
Keyword Arguments:
hostname {string} -- Hostname/IP address of the device to connect to
timeout -- How long to wait for replies (default: 300 seconds)
"""
self.stream = None
self.hostname: str = hostname
self.portNumber: int = portNumber
self.socket: Optional[socket.socket] = None
self.reconnectLock = threading.Lock()
if connectNow:
logging.debug(f"Connecting to {hostname}") # type: ignore[str-bytes-safe]
server_address: tuple[str, int] = (hostname, portNumber)
sock: Optional[socket.socket] = socket.create_connection(server_address)
self.socket = sock
else:
self.socket = None
StreamInterface.__init__(
self, debugOut=debugOut, noProto=noProto, connectNow=connectNow, noNodes=noNodes
super().__init__(
debugOut=debugOut,
noProto=noProto,
connectNow=connectNow,
noNodes=noNodes,
timeout=timeout,
)
def __repr__(self):
rep = f"TCPInterface({self.hostname!r}"
if self.debugOut is not None:
rep += f", debugOut={self.debugOut!r}"
if self.noProto:
rep += ", noProto=True"
if self.socket is None:
rep += ", connectNow=False"
if self.portNumber != DEFAULT_TCP_PORT:
rep += f", portNumber={self.portNumber!r}"
if self.noNodes:
rep += ", noNodes=True"
rep += ")"
return rep
def _socket_shutdown(self) -> None:
"""Shutdown the socket.
Note: Broke out this line so the exception could be unit tested.
"""
if self.socket: #mian: please check that this should be "if self.socket:"
cast(socket.socket, self.socket).shutdown(socket.SHUT_RDWR)
if self.socket is not None:
self.socket.shutdown(socket.SHUT_RDWR)
def connect(self) -> None:
"""Connect the interface"""
self.myConnect()
super().connect()
def myConnect(self) -> None:
"""Connect to socket"""
server_address: tuple[str, int] = (self.hostname, self.portNumber)
sock: Optional[socket.socket] = socket.create_connection(server_address)
self.socket = sock
"""Connect to socket (without attempting to start the interface's receive thread)"""
logger.debug(f"Connecting to {self.hostname}") # type: ignore[str-bytes-safe]
server_address = (self.hostname, self.portNumber)
self.socket = socket.create_connection(server_address)
def _wait_for_reader_exit(self, timeout: float) -> None:
"""Wait briefly for the reader thread to drain and exit after a half-close.
Returns as soon as it exits, or after timeout: the device is not obliged
to close just because we did.
"""
rx = getattr(self, "_rxThread", None)
if rx is None or rx is threading.current_thread():
return
with contextlib.suppress(Exception):
rx.join(timeout)
def close(self) -> None:
"""Close a connection to the device"""
logging.debug("Closing TCP stream")
StreamInterface.close(self)
"""Close a connection to the device."""
logger.debug("Closing TCP stream")
# Sometimes the socket read might be blocked in the reader thread.
# Therefore we force the shutdown by closing the socket here
self._wantExit: bool = True
if not self.socket is None:
try:
# Therefore force a shutdown first to unblock reader thread reads.
self._wantExit = True
if self.socket is not None:
# Half-close first. shutdown(SHUT_WR) sends FIN, which tells the
# device we are done writing and lets it consume what we last wrote
# before the connection goes away -- typically the admin message a
# one-shot command such as `--set` just sent.
#
# Going straight to shutdown(SHUT_RDWR) + close() while either side
# still has unread data makes the stack send RST instead. Winsock
# then discards data the peer had already received but not yet read,
# so the write is lost; Linux delivers it before reporting
# ECONNRESET, which is why this only bites on Windows.
with contextlib.suppress(Exception):
self.socket.shutdown(socket.SHUT_WR)
self._wait_for_reader_exit(GRACEFUL_CLOSE_TIMEOUT)
with contextlib.suppress(
Exception
): # Ignore errors in shutdown, because we might have a race with the server
self._socket_shutdown()
except:
pass # Ignore errors in shutdown, because we might have a race with the server
self.socket.close()
with contextlib.suppress(Exception):
self.socket.close()
self.socket = None
super().close()
def _writeBytes(self, b: bytes) -> None:
"""Write an array of bytes to our stream and flush"""
if self.socket:
self.socket.send(b)
"""Write an array of bytes to our stream"""
if self.socket is not None:
try:
self.socket.sendall(b)
except OSError as e:
logger.error(f"Socket send error, reconnecting: {e}")
if not self._wantExit:
self._reconnect()
raise
def _readBytes(self, length) -> Optional[bytes]:
"""Read an array of bytes from our stream"""
if self.socket:
return self.socket.recv(length)
else:
return None
if self.socket is not None:
data = self.socket.recv(length)
# empty byte indicates a disconnected socket,
# we need to handle it to avoid an infinite loop reading from null socket
if data == b"":
logger.debug("Closed socket, re-connecting")
if not self._wantExit:
self._reconnect()
return data
# no socket, break reader thread
self._wantExit = True
return None
def _reconnect(self) -> None:
"""Reconnect to the socket"""
# Save the socket reference before attempting to acquire the lock.
sock = self.socket
start_config = False
with self.reconnectLock:
if self._wantExit:
return
# Don't reconnect: someone else already did it.
if sock is not self.socket:
return
with contextlib.suppress(Exception):
self._socket_shutdown()
if self.socket is not None:
self.socket.close()
self.socket = None
time.sleep(1)
self.myConnect()
start_config = True
if start_config and not self._wantExit and self.socket is not None:
self._startConfig()
+11 -10
View File
@@ -29,6 +29,7 @@ testNumber: int = 0
sendingInterface = None
logger = logging.getLogger(__name__)
def onReceive(packet, interface) -> None:
"""Callback invoked when a packet arrives"""
@@ -79,7 +80,7 @@ def testSend(
else:
toNode = toInterface.myInfo.my_node_num
logging.debug(f"Sending test wantAck={wantAck} packet from {fromNode} to {toNode}")
logger.debug(f"Sending test wantAck={wantAck} packet from {fromNode} to {toNode}")
# pylint: disable=W0603
global sendingInterface
sendingInterface = fromInterface
@@ -98,7 +99,7 @@ def testSend(
def runTests(numTests: int=50, wantAck: bool=False, maxFailures: int=0) -> bool:
"""Run the tests."""
logging.info(f"Running {numTests} tests with wantAck={wantAck}")
logger.info(f"Running {numTests} tests with wantAck={wantAck}")
numFail: int = 0
numSuccess: int = 0
for _ in range(numTests):
@@ -112,26 +113,26 @@ def runTests(numTests: int=50, wantAck: bool=False, maxFailures: int=0) -> bool:
)
if not success:
numFail = numFail + 1
logging.error(
logger.error(
f"Test {testNumber} failed, expected packet not received ({numFail} failures so far)"
)
else:
numSuccess = numSuccess + 1
logging.info(
logger.info(
f"Test {testNumber} succeeded {numSuccess} successes {numFail} failures so far"
)
time.sleep(1)
if numFail > maxFailures:
logging.error("Too many failures! Test failed!")
logger.error("Too many failures! Test failed!")
return False
return True
def testThread(numTests=50) -> bool:
"""Test thread"""
logging.info("Found devices, starting tests...")
logger.info("Found devices, starting tests...")
result: bool = runTests(numTests, wantAck=True)
if result:
# Run another test
@@ -148,7 +149,7 @@ def onConnection(topic=pub.AUTO_TOPIC) -> None:
def openDebugLog(portName) -> io.TextIOWrapper:
"""Open the debug log file"""
debugname = "log" + portName.replace("/", "_")
logging.info(f"Writing serial debugging to {debugname}")
logger.info(f"Writing serial debugging to {debugname}")
return open(debugname, "w+", buffering=1, encoding="utf8")
@@ -177,7 +178,7 @@ def testAll(numTests: int=5) -> bool:
)
)
logging.info("Ports opened, starting test")
logger.info("Ports opened, starting test")
result: bool = testThread(numTests)
for i in interfaces:
@@ -196,14 +197,14 @@ def testSimulator() -> None:
python3 -c 'from meshtastic.test import testSimulator; testSimulator()'
"""
logging.basicConfig(level=logging.DEBUG)
logging.info("Connecting to simulator on localhost!")
logger.info("Connecting to simulator on localhost!")
try:
iface: meshtastic.tcp_interface.TCPInterface = TCPInterface("localhost")
iface.showInfo()
iface.localNode.showInfo()
iface.localNode.exitSimulator()
iface.close()
logging.info("Integration test successful!")
logger.info("Integration test successful!")
except:
print("Error while testing simulator:", sys.exc_info()[0])
traceback.print_exc()
+81
View File
@@ -8,6 +8,87 @@ import pytest
from meshtastic import mt_config
from ..mesh_interface import MeshInterface
from .firmware_harness import (
CHAIN_TOPOLOGY,
DEFAULT_BASE_PORT,
SimMesh,
find_meshtasticd,
is_compatible_host,
)
from .fw_helpers import set_region
# Use a different base port for the single-node fixture so it doesn't
# conflict with the multi-node mesh fixture.
SINGLE_NODE_BASE_PORT = DEFAULT_BASE_PORT + 100
def _skip_firmware_if_unavailable() -> None:
"""Skip the test when meshtasticd can't run on this host."""
if not is_compatible_host():
pytest.skip("meshtasticd firmware tests require Linux")
if find_meshtasticd() is None:
pytest.skip(
"meshtasticd not found — set MESHTASTICD_BIN or install it on PATH"
)
@pytest.fixture(scope="function")
def firmware_node():
"""A single meshtasticd sim node for smokevirt tests.
Function-scoped so every test gets a freshly-erased node with no
state leaking from previous tests. This makes destructive commands
(``--reboot``, ``--set factory_reset true``) safe to run and lets
tests be order-independent.
Yields the SimNode instance. The node is booted with a fresh erased
config and listens on localhost at its TCP port. Region is set to US
so modem-preset tests work against firmware >= 2.8, which clamps
presets to the legal set for the current region.
"""
_skip_firmware_if_unavailable()
mesh = SimMesh(n_nodes=1, base_port=SINGLE_NODE_BASE_PORT)
mesh.start()
node = mesh.get_node(0)
set_region(node.port, "US")
# The region commit restarts the TCP listener, so reconnect the harness
# interface in case a test wants to use it directly.
if node.iface is not None:
try:
node.iface.close()
except Exception: # pylint: disable=broad-except
pass
node.connect()
yield node
mesh.stop()
@pytest.fixture(scope="function")
def firmware_mesh():
"""A 3-node chain (A-B-C) meshtasticd sim mesh for smokemesh tests.
Yields the SimMesh instance. Nodes are connected and the SIMULATOR_APP
packet bridge is running. Region is set to US for firmware >= 2.8
compatibility, interfaces are reconnected after the region change, and
node DB convergence is awaited.
"""
_skip_firmware_if_unavailable()
mesh = SimMesh(n_nodes=3, topology=CHAIN_TOPOLOGY)
mesh.start()
for node in mesh.nodes:
set_region(node.port, "US")
# The region commit restarts each node's TCP listener, so reconnect the
# harness interfaces before waiting for convergence.
for node in mesh.nodes:
if node.iface is not None:
try:
node.iface.close()
except Exception: # pylint: disable=broad-except
pass
node.connect()
mesh.wait_for_convergence(timeout=30)
yield mesh
mesh.stop()
@pytest.fixture
+357
View File
@@ -0,0 +1,357 @@
"""Test harness for running real meshtasticd firmware instances.
Launches one or more meshtasticd processes in simulator mode (-s) and bridges
their "over-the-air" packets via the SIMULATOR_APP protocol so that multiple
instances can communicate as if via LoRa, with a configurable topology.
The harness expects meshtasticd to be available on PATH or via the
MESHTASTICD_BIN environment variable. It does not download or build the
binary itself.
"""
import logging
import os
import platform
import shutil
import signal
import socket
import subprocess
import tempfile
import time
from typing import Dict, List, Optional, Set
from pubsub import pub # type: ignore[import-untyped]
from meshtastic import BROADCAST_NUM, mesh_pb2, portnums_pb2
from meshtastic.tcp_interface import TCPInterface
logger = logging.getLogger(__name__)
HW_ID_OFFSET = 16
DEFAULT_BASE_PORT = 4404
DEFAULT_RSSI = -50
DEFAULT_SNR = 10.0
BOOT_TIMEOUT = 30
CONNECT_TIMEOUT = 30
CHAIN_TOPOLOGY: Dict[int, Set[int]] = {
0: {1},
1: {0, 2},
2: {1},
}
def find_meshtasticd() -> Optional[str]:
"""Return the path to the meshtasticd binary, or None if not found."""
env_path = os.environ.get("MESHTASTICD_BIN")
if env_path and os.path.isfile(env_path) and os.access(env_path, os.X_OK):
return env_path
return shutil.which("meshtasticd")
def is_compatible_host() -> bool:
"""True when the host can run meshtasticd natively (Linux only)."""
return platform.system() == "Linux"
def _wait_for_port(port: int, timeout: int = BOOT_TIMEOUT) -> bool:
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
try:
s = socket.create_connection(("localhost", port), timeout=0.5)
s.close()
return True
except OSError:
time.sleep(0.5)
return False
class SimNode:
"""A single meshtasticd simulator instance."""
def __init__(self, node_id: int, base_port: int = DEFAULT_BASE_PORT):
self.node_id = node_id
self.hw_id = node_id + HW_ID_OFFSET
self.port = base_port + node_id
self.process: Optional[subprocess.Popen] = None
self.workdir: Optional[str] = None
self.iface: Optional[TCPInterface] = None
self._log_files: list = []
@property
def node_num(self) -> int:
"""The firmware-assigned node number (== hw_id)."""
if self.iface and self.iface.myInfo:
return self.iface.myInfo.my_node_num
return self.hw_id
def start(self, binary: str) -> None:
"""Launch the meshtasticd process in simulator mode."""
self.workdir = tempfile.mkdtemp(prefix=f"mtd_node{self.node_id}_")
vfs_dir = os.path.join(self.workdir, "vfs")
os.mkdir(vfs_dir)
# Files are closed in _kill(); keep them open for the process lifetime.
log_stdout = open( # pylint: disable=consider-using-with
os.path.join(self.workdir, "meshtasticd.log"), "wb", buffering=0
)
log_stderr = open( # pylint: disable=consider-using-with
os.path.join(self.workdir, "meshtasticd.err"), "wb", buffering=0
)
self._log_files = [log_stdout, log_stderr]
self.process = subprocess.Popen( # pylint: disable=consider-using-with
[
binary,
"-s",
"-h", str(self.hw_id),
"-p", str(self.port),
"-d", vfs_dir,
"-e",
],
stdout=log_stdout,
stderr=log_stderr,
start_new_session=True,
)
if not _wait_for_port(self.port):
self._kill()
raise RuntimeError(
f"meshtasticd node {self.node_id} did not start listening on port {self.port}"
)
def connect(self) -> None:
"""Open a TCPInterface connection to this node."""
self.iface = TCPInterface(
hostname="localhost",
portNumber=self.port,
connectNow=False,
)
self.iface.myConnect()
self.iface.connect()
def close(self) -> None:
"""Close the interface and kill the process."""
if self.iface is not None:
try:
self.iface.localNode.exitSimulator()
except Exception:
pass
try:
self.iface.close()
except Exception:
pass
self.iface = None
self._kill()
if self.workdir:
shutil.rmtree(self.workdir, ignore_errors=True)
self.workdir = None
def _kill(self) -> None:
for f in self._log_files:
try:
f.close()
except Exception:
pass
self._log_files = []
if self.process is not None:
try:
os.killpg(os.getpgid(self.process.pid), signal.SIGTERM)
except (ProcessLookupError, OSError):
pass
try:
self.process.wait(timeout=5)
except Exception:
try:
os.killpg(os.getpgid(self.process.pid), signal.SIGKILL)
except Exception:
pass
# Give OS time to release TCP port (avoid TIME_WAIT preventing
# next instance from binding the same port)
time.sleep(1.0)
self.process = None
class SimMesh:
"""Manages N meshtasticd sim instances and bridges their SIMULATOR_APP packets.
When *topology* is None every node hears every other node (full mesh).
Otherwise *topology* maps a transmitter's node index to the set of receiver
node indices that can hear it.
"""
def __init__(
self,
n_nodes: int = 1,
topology: Optional[Dict[int, Set[int]]] = None,
base_port: int = DEFAULT_BASE_PORT,
):
self.n_nodes = n_nodes
self.topology = topology
self.base_port = base_port
self.nodes: List[SimNode] = [
SimNode(i, base_port) for i in range(n_nodes)
]
self._port_to_idx: Dict[int, int] = {}
self._started = False
def start(self) -> None:
"""Launch all nodes, connect, and start the packet bridge."""
binary = find_meshtasticd()
if binary is None:
raise RuntimeError(
"meshtasticd not found. Set MESHTASTICD_BIN or install it on PATH."
)
for node in self.nodes:
node.start(binary)
for node in self.nodes:
node.connect()
self._port_to_idx[node.port] = node.node_id
pub.subscribe(self._on_sim_packet, "meshtastic.receive.simulator")
self._started = True
if self.n_nodes > 1:
self._trigger_convergence()
def _trigger_convergence(self) -> None:
"""Actively trigger NodeInfo exchange instead of waiting passively.
Sends a NODEINFO_APP packet with wantResponse from each node so the
firmware's NodeInfoModule responds with its own user info, populating
all node DBs deterministically.
"""
for node in self.nodes:
iface = node.iface
if iface is None:
continue
user = mesh_pb2.User()
user.id = f"!{node.node_num:08x}"
user.long_name = f"Node {node.node_id}"
user.short_name = f"{node.node_id:04d}"[:4]
user.hw_model = mesh_pb2.HardwareModel.PORTDUINO
try:
iface.sendData(
user,
destinationId=BROADCAST_NUM,
portNum=portnums_pb2.PortNum.NODEINFO_APP,
wantAck=False,
wantResponse=True,
)
except Exception as ex:
logger.debug("NodeInfo trigger for node %d failed: %s", node.node_id, ex)
time.sleep(5)
def stop(self) -> None:
"""Shut down all nodes and clean up."""
if not self._started:
return
try:
pub.unsubscribe(self._on_sim_packet, "meshtastic.receive.simulator")
except Exception:
pass
for node in self.nodes:
node.close()
self._started = False
def get_node(self, idx: int) -> SimNode:
"""Return the SimNode at the given index."""
return self.nodes[idx]
def get_iface(self, idx: int) -> TCPInterface:
"""Return the TCPInterface for the node at the given index."""
iface = self.nodes[idx].iface
assert iface is not None, f"node {idx} has no interface"
return iface
def wait_for_convergence(self, timeout: int = 30) -> bool:
"""Wait until every node sees all others in its node DB.
Returns True if converged, False if timed out (non-fatal the packet
bridge forwards regardless of node DB state).
"""
if self.n_nodes <= 1:
return True
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
if all(
node.iface is not None
and node.iface.nodes is not None
and len(node.iface.nodes) >= self.n_nodes
for node in self.nodes
):
return True
time.sleep(2)
logger.warning("Mesh did not fully converge within %ds", timeout)
return False
def _get_receivers(self, tx_idx: int) -> List[int]:
"""Return node indices that can hear a transmission from *tx_idx*."""
if self.topology is not None:
return sorted(self.topology.get(tx_idx, set()))
return [i for i in range(self.n_nodes) if i != tx_idx]
def _on_sim_packet(self, interface, packet) -> None:
"""Bridge callback: forward a SIMULATOR_APP packet to receiving nodes."""
tx_port = getattr(interface, "portNumber", None)
tx_idx = self._port_to_idx.get(tx_port) if tx_port else None
if tx_idx is None:
return
rx_indices = self._get_receivers(tx_idx)
if not rx_indices:
return
data = packet["decoded"]["payload"]
if hasattr(data, "SerializeToString"):
data = data.SerializeToString()
if len(data) > mesh_pb2.Constants.DATA_PAYLOAD_LEN:
logger.warning("Simulator payload too big (%d bytes), dropping", len(data))
return
mesh_packet = _build_mesh_packet(packet, data)
for rx_idx in rx_indices:
rx_iface = self.nodes[rx_idx].iface
if rx_iface is None:
continue
mesh_packet.rx_rssi = DEFAULT_RSSI
mesh_packet.rx_snr = DEFAULT_SNR
to_radio = mesh_pb2.ToRadio()
to_radio.packet.CopyFrom(mesh_packet)
try:
rx_iface._sendToRadio(to_radio)
except Exception as ex:
logger.error("Error forwarding packet to node %d: %s", rx_idx, ex)
def __enter__(self):
self.start()
return self
def __exit__(self, *exc):
self.stop()
def _build_mesh_packet(packet: dict, data: bytes) -> mesh_pb2.MeshPacket:
"""Reconstruct a MeshPacket for SIMULATOR_APP injection."""
mp = mesh_pb2.MeshPacket()
mp.decoded.payload = data
mp.decoded.portnum = portnums_pb2.PortNum.SIMULATOR_APP
mp.to = packet.get("to", BROADCAST_NUM)
setattr(mp, "from", packet.get("from", 0))
mp.id = packet.get("id", 0)
mp.want_ack = packet.get("wantAck", False)
mp.hop_limit = packet.get("hopLimit", 0)
mp.hop_start = packet.get("hopStart", 0)
mp.via_mqtt = packet.get("viaMQTT", False)
mp.relay_node = packet.get("relayNode", 0)
mp.next_hop = packet.get("nextHop", 0)
mp.channel = int(packet.get("channel", 0))
decoded = packet.get("decoded", {})
if "requestId" in decoded:
mp.decoded.request_id = decoded["requestId"]
if "wantResponse" in decoded:
mp.decoded.want_response = decoded["wantResponse"]
if "bitfield" in decoded:
mp.decoded.bitfield = decoded["bitfield"]
return mp
+374
View File
@@ -0,0 +1,374 @@
"""Shared helpers for meshtasticd-backed smoke tests.
Both ``test_smokevirt`` (single node) and ``test_smokemesh`` (multi-node
chain) use these helpers to drive the ``meshtastic`` CLI against real
``meshtasticd`` simulator instances and then verify the resulting firmware
state through the Python library's ``TCPInterface``.
Verifying through the library (rather than regex on CLI stdout) is the
core design choice: it makes tests robust against CLI wording changes
while still exercising both the CLI argparse path and the firmware I/O
path of every feature.
"""
from __future__ import annotations
import logging
import shlex
import socket
import subprocess
import sys
import time
from typing import Callable, List, Optional, Tuple
from pubsub import pub # type: ignore[import-untyped]
from meshtastic.tcp_interface import TCPInterface
logger = logging.getLogger(__name__)
# Pause between a CLI command finishing and a verification interface
# opening, to let the firmware flush its TCP bookkeeping. Keeps the
# simulator happy when many short-lived connections are happening.
PAUSE_AFTER_CLI = 0.2
# Pause after changing the LoRa region. The firmware may restart the TCP
# listener while committing the new radio config, so give it time to settle
# before the next CLI call or harness reconnect.
PAUSE_AFTER_REGION_CHANGE = 2.0
# ---------------------------------------------------------------------------
# CLI invocation
# ---------------------------------------------------------------------------
def resolve_cli() -> str:
"""Return a shell-invokable ``meshtastic`` command.
Prefers the in-tree module run through the current interpreter so
tests exercise the source we are editing, regardless of any
separately-installed ``meshtastic`` entry point on PATH.
"""
# The PATH binary may live outside the nono sandbox's allowed paths;
# ``python -m meshtastic`` is more portable and always available.
return f"{sys.executable} -m meshtastic"
def run_cli(
port: int,
*args: str,
timeout: int = 60,
retries: int = 2,
retry_delay: float = 1.0,
) -> Tuple[int, str]:
"""Run the ``meshtastic`` CLI against the sim node on *port*.
Returns ``(return_code, merged_stdout_stderr)``. ``--host
localhost:PORT`` is prefixed automatically. stderr is merged into
stdout so callers can match warning text such as "Warning: Need to
specify ..." regardless of which stream it lands on.
If the CLI fails to connect on the first attempt (which happens
transiently when a freshly-booted sim node needs a moment to settle
after the harness interface connects), retry up to *retries*
additional times after *retry_delay* seconds.
"""
cli = resolve_cli()
argv = [*_shlex_split(cli)]
argv.extend(["--host", f"localhost:{port}"])
argv.extend(args)
logger.debug("run_cli: %s", argv)
last_out = ""
for attempt in range(retries + 1):
try:
proc = subprocess.run(
argv,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
timeout=timeout,
check=False,
)
except subprocess.TimeoutExpired as ex:
last_out = ex.stdout.decode("utf-8", errors="replace") if ex.stdout else ""
if attempt < retries:
time.sleep(retry_delay)
continue
return 124, last_out
out = proc.stdout.decode("utf-8", errors="replace")
rc = proc.returncode
# Retry on transient connection-refused / timed-out errors that
# are common right after a sim node spins up.
transient = (
"Error connecting" in out
or "Timed out waiting for connection" in out
or "Connection reset by peer" in out
)
if rc != 0 and transient and attempt < retries:
logger.debug("run_cli: transient failure, retry %d/%d", attempt + 1, retries)
time.sleep(retry_delay)
last_out = out
continue
return rc, out
return rc, last_out
def _shlex_split(cmd: str) -> List[str]:
"""Split a shell string into argv, honoring quotes."""
return shlex.split(cmd)
# ---------------------------------------------------------------------------
# Fresh-connection state verification
# ---------------------------------------------------------------------------
def _wait_for_port(port: int, timeout: float = 30.0) -> None:
"""Wait until *port* accepts a TCP connection (firmware sim comes up
or comes back after a config-commit reboot)."""
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
try:
s = socket.create_connection(("localhost", port), timeout=0.5)
s.close()
return
except OSError:
time.sleep(0.2)
raise TimeoutError(
f"port {port} did not accept connections within {timeout}s"
)
def connect_iface(
port: int,
no_nodes: bool = False,
retries: int = 4,
wait_timeout: float = 30.0,
) -> TCPInterface:
"""Open a fresh ``TCPInterface`` to *port* and block on the config exchange.
Firmware config writes (``writeChannel``, ``--seturl``, ``--factory_reset``)
can briefly restart the sim's TCP listener. We wait for the port to
come up first, then retry the connect+config-exchange a few times.
"""
last_exc: Optional[Exception] = None
for attempt in range(retries + 1):
try:
_wait_for_port(port, timeout=wait_timeout)
return TCPInterface(
hostname="localhost",
portNumber=port,
connectNow=True,
noNodes=no_nodes,
)
except Exception as ex: # pylint: disable=broad-except
last_exc = ex
if attempt < retries:
logger.debug(
"connect_iface attempt %d/%d failed: %s",
attempt + 1, retries, ex,
)
time.sleep(0.5)
continue
raise
assert last_exc is not None # type guard; unreachable
raise last_exc # pragma: no cover
def verify_state(
port: int,
verifier: Callable[[TCPInterface], None],
*,
no_nodes: bool = False,
) -> None:
"""Open a fresh interface and run *verifier(iface)*, then close.
Used after a CLI mutation to verify firmware state through the
library. Always closes the interface so the next test starts clean.
"""
iface = connect_iface(port, no_nodes=no_nodes)
try:
verifier(iface)
finally:
try:
iface.close()
except Exception: # pylint: disable=broad-except
pass
time.sleep(PAUSE_AFTER_CLI)
def cli_then_verify(
port: int,
cli_args: List[str],
verifier: Optional[Callable[[TCPInterface], None]],
*,
expect_rc: Optional[int] = 0,
no_nodes: bool = False,
cli_timeout: int = 60,
) -> str:
"""Run *cli_args* against *port*, optionally asserting *expect_rc*,
then (if *verifier* is not None) open a fresh interface and run
*verifier(iface)* against the just-mutated firmware state.
Returns the CLI stdout.
"""
rc, out = run_cli(port, *cli_args, timeout=cli_timeout)
if expect_rc is not None:
assert rc == expect_rc, f"CLI rc={rc} (expected {expect_rc}): {out}"
time.sleep(PAUSE_AFTER_CLI)
if verifier is not None:
verify_state(port, verifier, no_nodes=no_nodes)
return out
# ---------------------------------------------------------------------------
# Packet collectors (used by smokemesh receive-verification tests)
# ---------------------------------------------------------------------------
RECEIVE_TIMEOUT = 15.0
class PacketCollector:
"""Collect packets received on a specific interface via pubsub.
``Listener`` (pubsub 4.x) wraps handlers with a weak reference, so
we keep a strong reference to ``handler`` on the instance to prevent
garbage collection before the publishing thread gets to call it.
"""
def __init__(self):
self.packets: List[dict] = []
self._handler: Optional[Callable] = None
def on_receive(self, packet, interface): # pylint: disable=unused-argument
"""Append a received packet to the internal list."""
self.packets.append(packet)
def wait_for(self, count: int, timeout: float = RECEIVE_TIMEOUT) -> bool:
"""Wait until *count* packets have been collected or *timeout* expires."""
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
if len(self.packets) >= count:
return True
time.sleep(0.2)
return len(self.packets) >= count
@property
def texts(self) -> List[str]:
"""Return text payloads from TEXT_MESSAGE_APP packets."""
return [
p.get("decoded", {}).get("text", "")
for p in self.packets
if p.get("decoded", {}).get("portnum") == "TEXT_MESSAGE_APP"
]
@property
def traceroutes(self) -> List[dict]:
"""Return TRACEROUTE_APP packets."""
return [
p for p in self.packets
if p.get("decoded", {}).get("portnum") == "TRACEROUTE_APP"
]
@property
def telemetries(self) -> List[dict]:
"""Return TELEMETRY_APP packets."""
return [
p for p in self.packets
if p.get("decoded", {}).get("portnum") == "TELEMETRY_APP"
]
@property
def positions(self) -> List[dict]:
"""Return POSITION_APP packets."""
return [
p for p in self.packets
if p.get("decoded", {}).get("portnum") == "POSITION_APP"
]
def reset(self) -> None:
"""Clear all collected packets."""
self.packets.clear()
def _subscribe_topic(
iface: TCPInterface, topic: str
) -> PacketCollector:
"""Internal: subscribe a fully-qualified *topic* and return a collector.
Filters by *interface* so multi-node tests can subscribe several
collectors concurrently without cross-talk.
"""
collector = PacketCollector()
def handler(packet, interface):
if interface is iface:
collector.on_receive(packet, interface)
pub.subscribe(handler, topic)
collector._handler = handler # strong ref; see PacketCollector docstring
return collector
def subscribe_texts(iface: TCPInterface) -> PacketCollector:
"""Subscribe to ``meshtastic.receive.text`` filtered to *iface*."""
return _subscribe_topic(iface, "meshtastic.receive.text")
def subscribe_traceroutes(iface: TCPInterface) -> PacketCollector:
"""Subscribe to ``meshtastic.receive.traceroute`` filtered to *iface*."""
return _subscribe_topic(iface, "meshtastic.receive.traceroute")
def subscribe_telemetries(iface: TCPInterface) -> PacketCollector:
"""Subscribe to ``meshtastic.receive.telemetry`` filtered to *iface*."""
return _subscribe_topic(iface, "meshtastic.receive.telemetry")
def subscribe_positions(iface: TCPInterface) -> PacketCollector:
"""Subscribe to ``meshtastic.receive.position`` filtered to *iface*."""
return _subscribe_topic(iface, "meshtastic.receive.position")
def set_region(port: int, region: str = "US") -> None:
"""Set the LoRa region on a sim node via the CLI.
The node briefly restarts its TCP listener while committing the radio
config; ``run_cli()`` handles the reconnect retry, and we sleep long
enough for the new region to take effect before callers continue.
"""
rc, out = run_cli(port, "--set", "lora.region", region)
if rc != 0:
raise RuntimeError(f"Failed to set lora.region={region}: {out}")
time.sleep(PAUSE_AFTER_REGION_CHANGE)
def unsubscribe_all(topic: str) -> None:
"""Drop every handler currently registered on *topic*.
Tests use this in a ``finally`` block to keep pubsub clean across
the function-scoped mesh fixtures.
"""
try:
pub.unsubAll(topic)
except Exception: # pylint: disable=broad-except
pass
__all__ = [
"PAUSE_AFTER_CLI",
"PAUSE_AFTER_REGION_CHANGE",
"PacketCollector",
"RECEIVE_TIMEOUT",
"cli_then_verify",
"connect_iface",
"resolve_cli",
"run_cli",
"set_region",
"subscribe_positions",
"subscribe_telemetries",
"subscribe_texts",
"subscribe_traceroutes",
"unsubscribe_all",
"verify_state",
]
Loaded 100 of 122 files, more files were not shown because too many files have changed in this diff. Show more