Compare commits

...

365 Commits

Author SHA1 Message Date
dependabot[bot]
4f2ce3dfc2 Bump tornado from 6.5.6 to 6.5.7
Bumps [tornado](https://github.com/tornadoweb/tornado) from 6.5.6 to 6.5.7.
- [Changelog](https://github.com/tornadoweb/tornado/blob/master/docs/releases.rst)
- [Commits](https://github.com/tornadoweb/tornado/compare/v6.5.6...v6.5.7)

---
updated-dependencies:
- dependency-name: tornado
  dependency-version: 6.5.7
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-17 23:04:52 +00: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
Nox
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
Nox
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
Nox
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 Babeshkin
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 McEwen
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 McEwen
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
pdxlocations
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 Meadors
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 Sviridkin
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 Sviridkin
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 Sviridkin
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
github-actions
0487ce5e1a bump version to 2.5.10 2025-01-10 17:10:16 +00:00
Ian McEwen
aac19b2ecc protobufs: v2.5.19 2025-01-10 10:06:40 -07:00
digitaldisarray
0fb72b8ad1 Only allow PRIVATE_APP custom port 2024-12-31 15:15:18 -08:00
github-actions
872fbef5d6 bump version to 2.5.9 2024-12-29 03:22:52 +00:00
Ian McEwen
ec4fbe3a59 protobufs: v2.5.18 2024-12-28 20:18:13 -07:00
Ian McEwen
6bab385380 Tweak error messaging in the onReceive function for sendtext packets 2024-12-27 10:04:36 -07:00
Ian McEwen
b8178d513a bump alpha version 2024-12-27 09:54:15 -07:00
Ian McEwen
f4c085fc50 Merge pull request #714 from loic-fejoz/feature/waypoint
Waypoint creation/move/deletion
2024-12-27 09:46:45 -07:00
Ian McEwen
57f0598082 Fix some pylint complaints 2024-12-27 09:40:17 -07:00
github-actions
55d3188408 bump version to 2.5.8 2024-12-27 12:06:45 +00:00
Ben Meadors
7b64fbb71b Merge pull request #725 from meshtastic/dependabot/pip/jinja2-3.1.5
Bump jinja2 from 3.1.4 to 3.1.5
2024-12-25 08:42:58 -06:00
dependabot[bot]
7f85eb0285 Bump jinja2 from 3.1.4 to 3.1.5
Bumps [jinja2](https://github.com/pallets/jinja) from 3.1.4 to 3.1.5.
- [Release notes](https://github.com/pallets/jinja/releases)
- [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/jinja/compare/3.1.4...3.1.5)

---
updated-dependencies:
- dependency-name: jinja2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-24 00:32:14 +00:00
Ben Meadors
d05ef17ab3 Merge pull request #724 from meshtastic/send-alert
Add sendAlert method on mesh interface
2024-12-23 10:48:49 -06:00
Ben Meadors
d161291ca4 Add sendAlert method on mesh interface 2024-12-22 09:09:54 -06:00
Ian McEwen
4e267c75b0 bump to 2.5.8a0 2024-12-21 20:49:32 -07:00
github-actions
f56b9eefa6 bump version to 2.5.7 2024-12-22 03:46:10 +00:00
Ian McEwen
2de1f1921c Merge pull request #723 from ianmcorvidae/add-wcwidth-optional
Add 'wcwidth' as an optional cli dependency
2024-12-21 20:32:47 -07:00
Ian McEwen
227507780e Add 'wcwidth' as an optional cli dependency
Fixes #598 when installed as meshtastic[cli], as should now be
recommended by documentation.
2024-12-21 20:29:07 -07:00
Ian McEwen
9f286c9023 Merge pull request #721 from ianmcorvidae/shell-completion
Add very simple shell completion using argcomplete
2024-12-21 20:26:06 -07:00
Ian McEwen
0b1545393e Make mypy happy with the optional import of argcomplete 2024-12-21 20:23:35 -07:00
Ian McEwen
245a9e40b1 Add very simple shell completion using argcomplete 2024-12-21 20:12:26 -07:00
Ian McEwen
749c6a70bc Merge pull request #720 from ianmcorvidae/winserver-fix
Fix windows-11 detection for non-float platform.release() values
2024-12-21 14:22:55 -07:00
Ian McEwen
afd071c24e Fix windows-11 detection for non-float platform.release() values (fixes #639) 2024-12-21 14:17:41 -07:00
Ian McEwen
29f355bd61 Merge pull request #719 from ianmcorvidae/ignore-nodes
Support setting nodes ignored in nodedb (with 2.5.13+ firmware)
2024-12-21 13:50:44 -07:00
Ian McEwen
4b6d7a8587 Support setting nodes ignored in nodedb (with 2.5.13+ firmware) 2024-12-21 13:46:17 -07:00
Loïc Fejoz
7cc18e9df6 fix(waypoint): Missing methods
Add missing methods callbacks regarding waypoints.
2024-12-21 10:36:36 +01:00
Ian McEwen
a765bccf4d Merge pull request #718 from ianmcorvidae/show-unknown-more-nicely
Display unknown hop count as '?', and header as just 'Hops'
2024-12-20 23:08:19 -07:00
Ian McEwen
f950ecae2d Display unknown hop count as '?', and header as just 'Hops' 2024-12-20 23:04:36 -07:00
Ian McEwen
7c7170a5dd Merge pull request #706 from meshtastic/dependabot/pip/tornado-6.4.2
Bump tornado from 6.4.1 to 6.4.2
2024-12-20 22:50:01 -07:00
Ian McEwen
df191e149b Merge pull request #713 from esev/favorite
Support setting/removing nodes as favorites
2024-12-20 22:36:23 -07:00
Ian McEwen
843abe587f Back out release workflow changes from hotfix 2024-12-20 14:59:26 -07:00
Ian McEwen
ff7dcc3afb Merge branch '2.5.6-hotfix' 2024-12-20 14:47:52 -07:00
Ian McEwen
f6f8ccfcbc Fix message_to_json_shows_all test for added MyNodeInfo fields 2024-12-20 13:51:27 -07:00
Ian McEwen
cace959ca4 Update protobufs (to 2.5.17) and main python version (to 2.5.7) 2024-12-20 13:45:58 -07:00
Loïc Fejoz
9284a848f2 feat(waypoint): Waypoint creation/deletion
Add methods to send (create or move), delete waypoint.
Add an example script to create, move, delete waypoint.
2024-12-18 17:36:58 +01:00
Eric Severance
18ac0d6d5c Support setting/removing nodes as favorites 2024-12-17 17:38:51 -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
dependabot[bot]
7bb8e4e9dd Bump tornado from 6.4.1 to 6.4.2
Bumps [tornado](https://github.com/tornadoweb/tornado) from 6.4.1 to 6.4.2.
- [Changelog](https://github.com/tornadoweb/tornado/blob/v6.4.2/docs/releases.rst)
- [Commits](https://github.com/tornadoweb/tornado/compare/v6.4.1...v6.4.2)

---
updated-dependencies:
- dependency-name: tornado
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-22 22:17:33 +00:00
108 changed files with 19132 additions and 3012 deletions

11
.dockerignore Normal file
View File

@@ -0,0 +1,11 @@
*
!Containerfile*
!Dockerfile
!README.md
!LICENSE.md
!bin/container-entrypoint.sh
!examples/
!extra/
!meshtastic/
!poetry.lock
!pyproject.toml

22
.github/CONTRIBUTING.md vendored Normal file
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
.github/copilot-instructions.md vendored Normal file
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

BIN
.github/meshtastic_logo.png vendored Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

79
.github/workflows/container-build.yaml vendored Normal file
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

View File

@@ -24,9 +24,9 @@ jobs:
python -m pip install --upgrade pip
pip3 install poetry
#- name: Bump version
# run: >-
# poetry version patch
- name: Bump version
run: >-
poetry version patch
- name: Get version
id: get_version

View File

@@ -1,6 +1,9 @@
name: "Update protobufs"
on: workflow_dispatch
permissions:
contents: write
jobs:
update-protobufs:
runs-on: ubuntu-latest
@@ -9,23 +12,34 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: true
- name: Update Submodule
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install Poetry
run: |
git pull --recurse-submodules
python -m pip install --upgrade pip
python -m pip install poetry
- name: Update protobuf submodule
run: |
git submodule sync --recursive
git submodule update --init --recursive
git submodule update --remote --recursive
- 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: |
@@ -38,4 +52,9 @@ jobs:
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 "Update protobufs"
git push
else
echo "No changes to commit"
fi

3
.gitignore vendored
View File

@@ -17,4 +17,5 @@ examples/__pycache__
meshtastic.spec
.hypothesis/
coverage.xml
.ipynb_checkpoints
.ipynb_checkpoints
.cursor/

27
.vscode/launch.json vendored
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
Containerfile Symbolic link
View File

@@ -0,0 +1 @@
Containerfile.alpine

40
Containerfile.alpine Normal file
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
Containerfile.debian Normal file
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" ]

1
Dockerfile Symbolic link
View File

@@ -0,0 +1 @@
Containerfile

View File

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
bin/container-entrypoint.sh Executable file
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

View File

@@ -0,0 +1,332 @@
#!/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"}
# 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",
"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 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())

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

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
examples/CONTRIBUTING.md Normal file
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.

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

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

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

View File

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

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)

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
examples/replymessage.py Normal file
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()

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

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

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

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

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

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
examples/textchat.py Normal file
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()

60
examples/waypoint.py Normal file
View File

@@ -0,0 +1,60 @@
"""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
import datetime
import sys
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")
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", 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()
# By default will try to find a meshtastic device,
# otherwise provide a device path like /dev/ttyUSB0
if args.debug:
d = sys.stderr
else:
d = None
with meshtastic.serial_interface.SerialInterface(args.port, debugOut=d) as iface:
if args.cmd == "create":
p = iface.sendWaypoint(
waypoint_id=args.id,
name=args.name,
description=args.description,
icon=args.icon,
expire=int(datetime.datetime.fromisoformat(args.expire).timestamp()),
latitude=args.latitude,
longitude=args.longitude,
)
else:
p = iface.deleteWaypoint(args.id)
print(p)

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

View File

File diff suppressed because it is too large Load Diff

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

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

View File

@@ -1,11 +1,13 @@
"""Mesh Interface class
"""
# pylint: disable=R0917
# pylint: disable=R0917,C0302
import collections
import json
import logging
import math
import random
import secrets
import sys
import threading
import time
@@ -15,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:
@@ -44,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
@@ -86,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
@@ -95,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
@@ -115,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
@@ -147,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
@@ -220,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."""
@@ -244,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 Away": node.get("hopsAway", "0/unknown"),
"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):
@@ -330,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
@@ -342,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
@@ -358,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.
@@ -368,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.
@@ -382,13 +446,64 @@ 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,
)
def sendAlert(
self,
text: str,
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,
but carries a higher priority and is capable of generating special notifications
on certain clients.
Arguments:
text {string} -- The text of the alert to send
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.
"""
return self.sendData(
text.encode("utf-8"),
destinationId,
portNum=portnums_pb2.PortNum.ALERT_APP,
wantAck=False,
wantResponse=False,
onResponse=onResponse,
channelIndex=channelIndex,
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,
@@ -402,6 +517,8 @@ class MeshInterface: # pylint: disable=R0902
hopLimit: Optional[int]=None,
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
@@ -426,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:
@@ -453,9 +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
@@ -469,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)
@@ -482,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
@@ -505,6 +628,7 @@ class MeshInterface: # pylint: disable=R0902
wantResponse=wantResponse,
onResponse=onResponse,
channelIndex=channelIndex,
hopLimit=hopLimit,
)
if wantResponse:
self.waitForPosition()
@@ -557,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()
@@ -610,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()
@@ -657,6 +790,7 @@ class MeshInterface: # pylint: disable=R0902
wantResponse=wantResponse,
onResponse=onResponse,
channelIndex=channelIndex,
hopLimit=hopLimit,
)
if wantResponse:
self.waitForTelemetry()
@@ -700,6 +834,119 @@ class MeshInterface: # pylint: disable=R0902
"No response from node. At least firmware 2.1.22 is required on the destination node."
)
def onResponseWaypoint(self, p: dict):
"""on response for waypoint"""
if p["decoded"]["portnum"] == "WAYPOINT_APP":
self._acknowledgment.receivedWaypoint = True
w = mesh_pb2.Waypoint()
w.ParseFromString(p["decoded"]["payload"])
print(f"Waypoint received: {w}")
elif p["decoded"]["portnum"] == "ROUTING_APP":
if p["decoded"]["routing"]["errorReason"] == "NO_RESPONSE":
our_exit(
"No response from node. At least firmware 2.1.22 is required on the destination node."
)
def sendWaypoint(
self,
name,
description,
icon,
expire: int,
waypoint_id: Optional[int] = None,
latitude: float = 0.0,
longitude: float = 0.0,
destinationId: Union[int, str] = BROADCAST_ADDR,
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)
Returns the sent packet. The id field will be populated in this packet and
can be used to track future message acks/naks.
"""
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)
logger.debug(f"w.id:{w.id}")
else:
w.id = waypoint_id
if latitude != 0.0:
w.latitude_i = int(latitude * 1e7)
logger.debug(f"w.latitude_i:{w.latitude_i}")
if longitude != 0.0:
w.longitude_i = int(longitude * 1e7)
logger.debug(f"w.longitude_i:{w.longitude_i}")
if wantResponse:
onResponse = self.onResponseWaypoint
else:
onResponse = None
d = self.sendData(
w,
destinationId,
portNum=portnums_pb2.PortNum.WAYPOINT_APP,
wantAck=wantAck,
wantResponse=wantResponse,
onResponse=onResponse,
channelIndex=channelIndex,
hopLimit=hopLimit,
)
if wantResponse:
self.waitForWaypoint()
return d
def deleteWaypoint(
self,
waypoint_id: int,
destinationId: Union[int, str] = BROADCAST_ADDR,
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.
"""
p = mesh_pb2.Waypoint()
p.id = waypoint_id
p.expire = 0
if wantResponse:
onResponse = self.onResponseWaypoint
else:
onResponse = None
d = self.sendData(
p,
destinationId,
portNum=portnums_pb2.PortNum.WAYPOINT_APP,
wantAck=wantAck,
wantResponse=wantResponse,
onResponse=onResponse,
channelIndex=channelIndex,
hopLimit=hopLimit,
)
if wantResponse:
self.waitForWaypoint()
return d
def _addResponseHandler(
self,
requestId: int,
@@ -745,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)
@@ -755,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
@@ -779,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
@@ -824,11 +1073,17 @@ class MeshInterface: # pylint: disable=R0902
if not success:
raise MeshInterface.MeshInterfaceError("Timed out waiting for position")
def waitForWaypoint(self):
"""Wait for waypoint"""
success = self._timeout.waitForWaypoint(self._acknowledgment)
if not success:
raise MeshInterface.MeshInterfaceError("Timed out waiting for waypoint")
def getMyNodeInfo(self) -> Optional[Dict]:
"""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):
@@ -859,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"""
@@ -904,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()
@@ -962,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,
@@ -979,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:
"""
@@ -1026,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):
"""
@@ -1049,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"])
@@ -1080,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
@@ -1095,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)
@@ -1105,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(
@@ -1200,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
@@ -1236,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):
@@ -1292,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(
@@ -1306,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)
@@ -1330,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"]
@@ -1362,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
@@ -1379,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)
)

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
@@ -289,28 +323,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 +380,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 +449,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 +526,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 +546,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 +579,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 +589,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 +602,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 +625,7 @@ class Node:
while self.gotResponse is False:
time.sleep(0.1)
logging.debug(
logger.debug(
f"self.cannedPluginMessageMessages:{self.cannedPluginMessageMessages}"
)
@@ -494,12 +633,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 +660,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 +674,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 +683,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 +697,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 +711,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 +721,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 +734,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 +769,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 +782,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 +794,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 +810,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
@@ -668,12 +821,68 @@ class Node:
onResponse = self.onAckNak
return self._sendAdmin(p, onResponse=onResponse)
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()
nodeId = to_node_num(nodeId)
p = admin_pb2.AdminMessage()
p.set_favorite_node = nodeId
if self == self.iface.localNode:
onResponse = None
else:
onResponse = self.onAckNak
return self._sendAdmin(p, onResponse=onResponse)
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()
nodeId = to_node_num(nodeId)
p = admin_pb2.AdminMessage()
p.remove_favorite_node = nodeId
if self == self.iface.localNode:
onResponse = None
else:
onResponse = self.onAckNak
return self._sendAdmin(p, onResponse=onResponse)
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()
nodeId = to_node_num(nodeId)
p = admin_pb2.AdminMessage()
p.set_ignored_node = nodeId
if self == self.iface.localNode:
onResponse = None
else:
onResponse = self.onAckNak
return self._sendAdmin(p, onResponse=onResponse)
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()
nodeId = to_node_num(nodeId)
p = admin_pb2.AdminMessage()
p.remove_ignored_node = nodeId
if self == self.iface.localNode:
onResponse = None
else:
onResponse = self.onAckNak
return self._sendAdmin(p, onResponse=onResponse)
def resetNodeDb(self):
"""Tell the node to reset its list of nodes."""
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:
@@ -714,7 +923,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
@@ -729,7 +938,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
@@ -761,7 +970,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":
@@ -773,30 +982,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
@@ -804,18 +1025,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()
@@ -850,11 +1071,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
@@ -871,7 +1092,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:
@@ -879,18 +1100,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,
@@ -900,13 +1118,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
meshtastic/ota.py Normal file
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

View File

File diff suppressed because one or more lines are too long

View File

@@ -12,6 +12,7 @@ import google.protobuf.message
import meshtastic.protobuf.channel_pb2
import meshtastic.protobuf.config_pb2
import meshtastic.protobuf.connection_status_pb2
import meshtastic.protobuf.device_ui_pb2
import meshtastic.protobuf.mesh_pb2
import meshtastic.protobuf.module_config_pb2
import sys
@@ -24,6 +25,44 @@ else:
DESCRIPTOR: google.protobuf.descriptor.FileDescriptor
class _OTAMode:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
class _OTAModeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_OTAMode.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
NO_REBOOT_OTA: _OTAMode.ValueType # 0
"""
Do not reboot into OTA mode
"""
OTA_BLE: _OTAMode.ValueType # 1
"""
Reboot into OTA mode for BLE firmware update
"""
OTA_WIFI: _OTAMode.ValueType # 2
"""
Reboot into OTA mode for WiFi firmware update
"""
class OTAMode(_OTAMode, metaclass=_OTAModeEnumTypeWrapper):
"""
Firmware update mode for OTA updates
"""
NO_REBOOT_OTA: OTAMode.ValueType # 0
"""
Do not reboot into OTA mode
"""
OTA_BLE: OTAMode.ValueType # 1
"""
Reboot into OTA mode for BLE firmware update
"""
OTA_WIFI: OTAMode.ValueType # 2
"""
Reboot into OTA mode for WiFi firmware update
"""
global___OTAMode = OTAMode
@typing.final
class AdminMessage(google.protobuf.message.Message):
"""
@@ -73,7 +112,13 @@ class AdminMessage(google.protobuf.message.Message):
TODO: REPLACE
"""
SESSIONKEY_CONFIG: AdminMessage._ConfigType.ValueType # 8
""""""
"""
Session key config
"""
DEVICEUI_CONFIG: AdminMessage._ConfigType.ValueType # 9
"""
device-ui config
"""
class ConfigType(_ConfigType, metaclass=_ConfigTypeEnumTypeWrapper):
"""
@@ -113,7 +158,13 @@ class AdminMessage(google.protobuf.message.Message):
TODO: REPLACE
"""
SESSIONKEY_CONFIG: AdminMessage.ConfigType.ValueType # 8
""""""
"""
Session key config
"""
DEVICEUI_CONFIG: AdminMessage.ConfigType.ValueType # 9
"""
device-ui config
"""
class _ModuleConfigType:
ValueType = typing.NewType("ValueType", builtins.int)
@@ -173,6 +224,18 @@ class AdminMessage(google.protobuf.message.Message):
"""
TODO: REPLACE
"""
STATUSMESSAGE_CONFIG: AdminMessage._ModuleConfigType.ValueType # 13
"""
TODO: REPLACE
"""
TRAFFICMANAGEMENT_CONFIG: AdminMessage._ModuleConfigType.ValueType # 14
"""
Traffic management module config
"""
TAK_CONFIG: AdminMessage._ModuleConfigType.ValueType # 15
"""
TAK module config
"""
class ModuleConfigType(_ModuleConfigType, metaclass=_ModuleConfigTypeEnumTypeWrapper):
"""
@@ -231,6 +294,109 @@ class AdminMessage(google.protobuf.message.Message):
"""
TODO: REPLACE
"""
STATUSMESSAGE_CONFIG: AdminMessage.ModuleConfigType.ValueType # 13
"""
TODO: REPLACE
"""
TRAFFICMANAGEMENT_CONFIG: AdminMessage.ModuleConfigType.ValueType # 14
"""
Traffic management module config
"""
TAK_CONFIG: AdminMessage.ModuleConfigType.ValueType # 15
"""
TAK module config
"""
class _BackupLocation:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
class _BackupLocationEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[AdminMessage._BackupLocation.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
FLASH: AdminMessage._BackupLocation.ValueType # 0
"""
Backup to the internal flash
"""
SD: AdminMessage._BackupLocation.ValueType # 1
"""
Backup to the SD card
"""
class BackupLocation(_BackupLocation, metaclass=_BackupLocationEnumTypeWrapper): ...
FLASH: AdminMessage.BackupLocation.ValueType # 0
"""
Backup to the internal flash
"""
SD: AdminMessage.BackupLocation.ValueType # 1
"""
Backup to the SD card
"""
@typing.final
class InputEvent(google.protobuf.message.Message):
"""
Input event message to be sent to the node.
"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
EVENT_CODE_FIELD_NUMBER: builtins.int
KB_CHAR_FIELD_NUMBER: builtins.int
TOUCH_X_FIELD_NUMBER: builtins.int
TOUCH_Y_FIELD_NUMBER: builtins.int
event_code: builtins.int
"""
The input event code
"""
kb_char: builtins.int
"""
Keyboard character code
"""
touch_x: builtins.int
"""
The touch X coordinate
"""
touch_y: builtins.int
"""
The touch Y coordinate
"""
def __init__(
self,
*,
event_code: builtins.int = ...,
kb_char: builtins.int = ...,
touch_x: builtins.int = ...,
touch_y: builtins.int = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["event_code", b"event_code", "kb_char", b"kb_char", "touch_x", b"touch_x", "touch_y", b"touch_y"]) -> None: ...
@typing.final
class OTAEvent(google.protobuf.message.Message):
"""
User is requesting an over the air update.
Node will reboot into the OTA loader
"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
REBOOT_OTA_MODE_FIELD_NUMBER: builtins.int
OTA_HASH_FIELD_NUMBER: builtins.int
reboot_ota_mode: global___OTAMode.ValueType
"""
Tell the node to reboot into OTA mode for firmware update via BLE or WiFi (ESP32 only for now)
"""
ota_hash: builtins.bytes
"""
A 32 byte hash of the OTA firmware.
Used to verify the integrity of the firmware before applying an update.
"""
def __init__(
self,
*,
reboot_ota_mode: global___OTAMode.ValueType = ...,
ota_hash: builtins.bytes = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["ota_hash", b"ota_hash", "reboot_ota_mode", b"reboot_ota_mode"]) -> None: ...
SESSION_PASSKEY_FIELD_NUMBER: builtins.int
GET_CHANNEL_REQUEST_FIELD_NUMBER: builtins.int
@@ -255,6 +421,10 @@ class AdminMessage(google.protobuf.message.Message):
ENTER_DFU_MODE_REQUEST_FIELD_NUMBER: builtins.int
DELETE_FILE_REQUEST_FIELD_NUMBER: builtins.int
SET_SCALE_FIELD_NUMBER: builtins.int
BACKUP_PREFERENCES_FIELD_NUMBER: builtins.int
RESTORE_PREFERENCES_FIELD_NUMBER: builtins.int
REMOVE_BACKUP_PREFERENCES_FIELD_NUMBER: builtins.int
SEND_INPUT_EVENT_FIELD_NUMBER: builtins.int
SET_OWNER_FIELD_NUMBER: builtins.int
SET_CHANNEL_FIELD_NUMBER: builtins.int
SET_CONFIG_FIELD_NUMBER: builtins.int
@@ -267,8 +437,16 @@ class AdminMessage(google.protobuf.message.Message):
SET_FIXED_POSITION_FIELD_NUMBER: builtins.int
REMOVE_FIXED_POSITION_FIELD_NUMBER: builtins.int
SET_TIME_ONLY_FIELD_NUMBER: builtins.int
GET_UI_CONFIG_REQUEST_FIELD_NUMBER: builtins.int
GET_UI_CONFIG_RESPONSE_FIELD_NUMBER: builtins.int
STORE_UI_CONFIG_FIELD_NUMBER: builtins.int
SET_IGNORED_NODE_FIELD_NUMBER: builtins.int
REMOVE_IGNORED_NODE_FIELD_NUMBER: builtins.int
TOGGLE_MUTED_NODE_FIELD_NUMBER: builtins.int
BEGIN_EDIT_SETTINGS_FIELD_NUMBER: builtins.int
COMMIT_EDIT_SETTINGS_FIELD_NUMBER: builtins.int
ADD_CONTACT_FIELD_NUMBER: builtins.int
KEY_VERIFICATION_FIELD_NUMBER: builtins.int
FACTORY_RESET_DEVICE_FIELD_NUMBER: builtins.int
REBOOT_OTA_SECONDS_FIELD_NUMBER: builtins.int
EXIT_SIMULATOR_FIELD_NUMBER: builtins.int
@@ -276,6 +454,9 @@ class AdminMessage(google.protobuf.message.Message):
SHUTDOWN_SECONDS_FIELD_NUMBER: builtins.int
FACTORY_RESET_CONFIG_FIELD_NUMBER: builtins.int
NODEDB_RESET_FIELD_NUMBER: builtins.int
OTA_REQUEST_FIELD_NUMBER: builtins.int
SENSOR_CONFIG_FIELD_NUMBER: builtins.int
LOCKDOWN_AUTH_FIELD_NUMBER: builtins.int
session_passkey: builtins.bytes
"""
The node generates this key and sends it with any get_x_response packets.
@@ -340,6 +521,18 @@ class AdminMessage(google.protobuf.message.Message):
"""
Set zero and offset for scale chips
"""
backup_preferences: global___AdminMessage.BackupLocation.ValueType
"""
Backup the node's preferences
"""
restore_preferences: global___AdminMessage.BackupLocation.ValueType
"""
Restore the node's preferences
"""
remove_backup_preferences: global___AdminMessage.BackupLocation.ValueType
"""
Remove backups of the node's preferences
"""
set_canned_message_module_messages: builtins.str
"""
Set the Canned Message Module messages text.
@@ -369,6 +562,22 @@ class AdminMessage(google.protobuf.message.Message):
Set time only on the node
Convenience method to set the time on the node (as Net quality) without any other position data
"""
get_ui_config_request: builtins.bool
"""
Tell the node to send the stored ui data.
"""
set_ignored_node: builtins.int
"""
Set specified node-num to be ignored on the NodeDB on the device
"""
remove_ignored_node: builtins.int
"""
Set specified node-num to be un-ignored on the NodeDB on the device
"""
toggle_muted_node: builtins.int
"""
Set specified node-num to be muted
"""
begin_edit_settings: builtins.bool
"""
Begins an edit transaction for config, module config, owner, and channel settings changes
@@ -386,6 +595,7 @@ class AdminMessage(google.protobuf.message.Message):
"""
Tell the node to reboot into the OTA Firmware in this many seconds (or <0 to cancel reboot)
Only Implemented for ESP32 Devices. This needs to be issued to send a new main firmware via bluetooth.
Deprecated in favor of reboot_ota_mode in 2.7.17
"""
exit_simulator: builtins.bool
"""
@@ -404,9 +614,10 @@ class AdminMessage(google.protobuf.message.Message):
"""
Tell the node to factory reset config; all device state and configuration will be returned to factory defaults; BLE bonds will be preserved.
"""
nodedb_reset: builtins.int
nodedb_reset: builtins.bool
"""
Tell the node to reset the nodedb.
When true, favorites are preserved through reset.
"""
@property
def get_channel_response(self) -> meshtastic.protobuf.channel_pb2.Channel:
@@ -456,6 +667,13 @@ class AdminMessage(google.protobuf.message.Message):
Respond with the mesh's nodes with their available gpio pins for RemoteHardware module use
"""
@property
def send_input_event(self) -> global___AdminMessage.InputEvent:
"""
Send an input event to the node.
This is used to trigger physical input events like button presses, touch events, etc.
"""
@property
def set_owner(self) -> meshtastic.protobuf.mesh_pb2.User:
"""
@@ -490,6 +708,55 @@ class AdminMessage(google.protobuf.message.Message):
Set fixed position data on the node and then set the position.fixed_position = true
"""
@property
def get_ui_config_response(self) -> meshtastic.protobuf.device_ui_pb2.DeviceUIConfig:
"""
Reply stored device ui data.
"""
@property
def store_ui_config(self) -> meshtastic.protobuf.device_ui_pb2.DeviceUIConfig:
"""
Tell the node to store UI data persistently.
"""
@property
def add_contact(self) -> global___SharedContact:
"""
Add a contact (User) to the nodedb
"""
@property
def key_verification(self) -> global___KeyVerificationAdmin:
"""
Initiate or respond to a key verification request
"""
@property
def ota_request(self) -> global___AdminMessage.OTAEvent:
"""
Tell the node to reset into the OTA Loader
"""
@property
def sensor_config(self) -> global___SensorConfig:
"""
Parameters and sensor configuration
"""
@property
def lockdown_auth(self) -> global___LockdownAuth:
"""
Lockdown passphrase delivery / unlock / lock-now command for hardened
firmware builds (see MESHTASTIC_LOCKDOWN). Used to provision the
passphrase on first boot, unlock encrypted storage on subsequent
reboots, re-verify on already-unlocked devices to authorize a new
client connection, or immediately re-lock the device.
Replaces the earlier scheme that repurposed SecurityConfig.private_key
to carry passphrase bytes; that hack is retired.
"""
def __init__(
self,
*,
@@ -516,6 +783,10 @@ class AdminMessage(google.protobuf.message.Message):
enter_dfu_mode_request: builtins.bool = ...,
delete_file_request: builtins.str = ...,
set_scale: builtins.int = ...,
backup_preferences: global___AdminMessage.BackupLocation.ValueType = ...,
restore_preferences: global___AdminMessage.BackupLocation.ValueType = ...,
remove_backup_preferences: global___AdminMessage.BackupLocation.ValueType = ...,
send_input_event: global___AdminMessage.InputEvent | None = ...,
set_owner: meshtastic.protobuf.mesh_pb2.User | None = ...,
set_channel: meshtastic.protobuf.channel_pb2.Channel | None = ...,
set_config: meshtastic.protobuf.config_pb2.Config | None = ...,
@@ -528,22 +799,95 @@ class AdminMessage(google.protobuf.message.Message):
set_fixed_position: meshtastic.protobuf.mesh_pb2.Position | None = ...,
remove_fixed_position: builtins.bool = ...,
set_time_only: builtins.int = ...,
get_ui_config_request: builtins.bool = ...,
get_ui_config_response: meshtastic.protobuf.device_ui_pb2.DeviceUIConfig | None = ...,
store_ui_config: meshtastic.protobuf.device_ui_pb2.DeviceUIConfig | None = ...,
set_ignored_node: builtins.int = ...,
remove_ignored_node: builtins.int = ...,
toggle_muted_node: builtins.int = ...,
begin_edit_settings: builtins.bool = ...,
commit_edit_settings: builtins.bool = ...,
add_contact: global___SharedContact | None = ...,
key_verification: global___KeyVerificationAdmin | None = ...,
factory_reset_device: builtins.int = ...,
reboot_ota_seconds: builtins.int = ...,
exit_simulator: builtins.bool = ...,
reboot_seconds: builtins.int = ...,
shutdown_seconds: builtins.int = ...,
factory_reset_config: builtins.int = ...,
nodedb_reset: builtins.int = ...,
nodedb_reset: builtins.bool = ...,
ota_request: global___AdminMessage.OTAEvent | None = ...,
sensor_config: global___SensorConfig | None = ...,
lockdown_auth: global___LockdownAuth | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["begin_edit_settings", b"begin_edit_settings", "commit_edit_settings", b"commit_edit_settings", "delete_file_request", b"delete_file_request", "enter_dfu_mode_request", b"enter_dfu_mode_request", "exit_simulator", b"exit_simulator", "factory_reset_config", b"factory_reset_config", "factory_reset_device", b"factory_reset_device", "get_canned_message_module_messages_request", b"get_canned_message_module_messages_request", "get_canned_message_module_messages_response", b"get_canned_message_module_messages_response", "get_channel_request", b"get_channel_request", "get_channel_response", b"get_channel_response", "get_config_request", b"get_config_request", "get_config_response", b"get_config_response", "get_device_connection_status_request", b"get_device_connection_status_request", "get_device_connection_status_response", b"get_device_connection_status_response", "get_device_metadata_request", b"get_device_metadata_request", "get_device_metadata_response", b"get_device_metadata_response", "get_module_config_request", b"get_module_config_request", "get_module_config_response", b"get_module_config_response", "get_node_remote_hardware_pins_request", b"get_node_remote_hardware_pins_request", "get_node_remote_hardware_pins_response", b"get_node_remote_hardware_pins_response", "get_owner_request", b"get_owner_request", "get_owner_response", b"get_owner_response", "get_ringtone_request", b"get_ringtone_request", "get_ringtone_response", b"get_ringtone_response", "nodedb_reset", b"nodedb_reset", "payload_variant", b"payload_variant", "reboot_ota_seconds", b"reboot_ota_seconds", "reboot_seconds", b"reboot_seconds", "remove_by_nodenum", b"remove_by_nodenum", "remove_favorite_node", b"remove_favorite_node", "remove_fixed_position", b"remove_fixed_position", "set_canned_message_module_messages", b"set_canned_message_module_messages", "set_channel", b"set_channel", "set_config", b"set_config", "set_favorite_node", b"set_favorite_node", "set_fixed_position", b"set_fixed_position", "set_ham_mode", b"set_ham_mode", "set_module_config", b"set_module_config", "set_owner", b"set_owner", "set_ringtone_message", b"set_ringtone_message", "set_scale", b"set_scale", "set_time_only", b"set_time_only", "shutdown_seconds", b"shutdown_seconds"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["begin_edit_settings", b"begin_edit_settings", "commit_edit_settings", b"commit_edit_settings", "delete_file_request", b"delete_file_request", "enter_dfu_mode_request", b"enter_dfu_mode_request", "exit_simulator", b"exit_simulator", "factory_reset_config", b"factory_reset_config", "factory_reset_device", b"factory_reset_device", "get_canned_message_module_messages_request", b"get_canned_message_module_messages_request", "get_canned_message_module_messages_response", b"get_canned_message_module_messages_response", "get_channel_request", b"get_channel_request", "get_channel_response", b"get_channel_response", "get_config_request", b"get_config_request", "get_config_response", b"get_config_response", "get_device_connection_status_request", b"get_device_connection_status_request", "get_device_connection_status_response", b"get_device_connection_status_response", "get_device_metadata_request", b"get_device_metadata_request", "get_device_metadata_response", b"get_device_metadata_response", "get_module_config_request", b"get_module_config_request", "get_module_config_response", b"get_module_config_response", "get_node_remote_hardware_pins_request", b"get_node_remote_hardware_pins_request", "get_node_remote_hardware_pins_response", b"get_node_remote_hardware_pins_response", "get_owner_request", b"get_owner_request", "get_owner_response", b"get_owner_response", "get_ringtone_request", b"get_ringtone_request", "get_ringtone_response", b"get_ringtone_response", "nodedb_reset", b"nodedb_reset", "payload_variant", b"payload_variant", "reboot_ota_seconds", b"reboot_ota_seconds", "reboot_seconds", b"reboot_seconds", "remove_by_nodenum", b"remove_by_nodenum", "remove_favorite_node", b"remove_favorite_node", "remove_fixed_position", b"remove_fixed_position", "session_passkey", b"session_passkey", "set_canned_message_module_messages", b"set_canned_message_module_messages", "set_channel", b"set_channel", "set_config", b"set_config", "set_favorite_node", b"set_favorite_node", "set_fixed_position", b"set_fixed_position", "set_ham_mode", b"set_ham_mode", "set_module_config", b"set_module_config", "set_owner", b"set_owner", "set_ringtone_message", b"set_ringtone_message", "set_scale", b"set_scale", "set_time_only", b"set_time_only", "shutdown_seconds", b"shutdown_seconds"]) -> None: ...
def WhichOneof(self, oneof_group: typing.Literal["payload_variant", b"payload_variant"]) -> typing.Literal["get_channel_request", "get_channel_response", "get_owner_request", "get_owner_response", "get_config_request", "get_config_response", "get_module_config_request", "get_module_config_response", "get_canned_message_module_messages_request", "get_canned_message_module_messages_response", "get_device_metadata_request", "get_device_metadata_response", "get_ringtone_request", "get_ringtone_response", "get_device_connection_status_request", "get_device_connection_status_response", "set_ham_mode", "get_node_remote_hardware_pins_request", "get_node_remote_hardware_pins_response", "enter_dfu_mode_request", "delete_file_request", "set_scale", "set_owner", "set_channel", "set_config", "set_module_config", "set_canned_message_module_messages", "set_ringtone_message", "remove_by_nodenum", "set_favorite_node", "remove_favorite_node", "set_fixed_position", "remove_fixed_position", "set_time_only", "begin_edit_settings", "commit_edit_settings", "factory_reset_device", "reboot_ota_seconds", "exit_simulator", "reboot_seconds", "shutdown_seconds", "factory_reset_config", "nodedb_reset"] | None: ...
def HasField(self, field_name: typing.Literal["add_contact", b"add_contact", "backup_preferences", b"backup_preferences", "begin_edit_settings", b"begin_edit_settings", "commit_edit_settings", b"commit_edit_settings", "delete_file_request", b"delete_file_request", "enter_dfu_mode_request", b"enter_dfu_mode_request", "exit_simulator", b"exit_simulator", "factory_reset_config", b"factory_reset_config", "factory_reset_device", b"factory_reset_device", "get_canned_message_module_messages_request", b"get_canned_message_module_messages_request", "get_canned_message_module_messages_response", b"get_canned_message_module_messages_response", "get_channel_request", b"get_channel_request", "get_channel_response", b"get_channel_response", "get_config_request", b"get_config_request", "get_config_response", b"get_config_response", "get_device_connection_status_request", b"get_device_connection_status_request", "get_device_connection_status_response", b"get_device_connection_status_response", "get_device_metadata_request", b"get_device_metadata_request", "get_device_metadata_response", b"get_device_metadata_response", "get_module_config_request", b"get_module_config_request", "get_module_config_response", b"get_module_config_response", "get_node_remote_hardware_pins_request", b"get_node_remote_hardware_pins_request", "get_node_remote_hardware_pins_response", b"get_node_remote_hardware_pins_response", "get_owner_request", b"get_owner_request", "get_owner_response", b"get_owner_response", "get_ringtone_request", b"get_ringtone_request", "get_ringtone_response", b"get_ringtone_response", "get_ui_config_request", b"get_ui_config_request", "get_ui_config_response", b"get_ui_config_response", "key_verification", b"key_verification", "lockdown_auth", b"lockdown_auth", "nodedb_reset", b"nodedb_reset", "ota_request", b"ota_request", "payload_variant", b"payload_variant", "reboot_ota_seconds", b"reboot_ota_seconds", "reboot_seconds", b"reboot_seconds", "remove_backup_preferences", b"remove_backup_preferences", "remove_by_nodenum", b"remove_by_nodenum", "remove_favorite_node", b"remove_favorite_node", "remove_fixed_position", b"remove_fixed_position", "remove_ignored_node", b"remove_ignored_node", "restore_preferences", b"restore_preferences", "send_input_event", b"send_input_event", "sensor_config", b"sensor_config", "set_canned_message_module_messages", b"set_canned_message_module_messages", "set_channel", b"set_channel", "set_config", b"set_config", "set_favorite_node", b"set_favorite_node", "set_fixed_position", b"set_fixed_position", "set_ham_mode", b"set_ham_mode", "set_ignored_node", b"set_ignored_node", "set_module_config", b"set_module_config", "set_owner", b"set_owner", "set_ringtone_message", b"set_ringtone_message", "set_scale", b"set_scale", "set_time_only", b"set_time_only", "shutdown_seconds", b"shutdown_seconds", "store_ui_config", b"store_ui_config", "toggle_muted_node", b"toggle_muted_node"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["add_contact", b"add_contact", "backup_preferences", b"backup_preferences", "begin_edit_settings", b"begin_edit_settings", "commit_edit_settings", b"commit_edit_settings", "delete_file_request", b"delete_file_request", "enter_dfu_mode_request", b"enter_dfu_mode_request", "exit_simulator", b"exit_simulator", "factory_reset_config", b"factory_reset_config", "factory_reset_device", b"factory_reset_device", "get_canned_message_module_messages_request", b"get_canned_message_module_messages_request", "get_canned_message_module_messages_response", b"get_canned_message_module_messages_response", "get_channel_request", b"get_channel_request", "get_channel_response", b"get_channel_response", "get_config_request", b"get_config_request", "get_config_response", b"get_config_response", "get_device_connection_status_request", b"get_device_connection_status_request", "get_device_connection_status_response", b"get_device_connection_status_response", "get_device_metadata_request", b"get_device_metadata_request", "get_device_metadata_response", b"get_device_metadata_response", "get_module_config_request", b"get_module_config_request", "get_module_config_response", b"get_module_config_response", "get_node_remote_hardware_pins_request", b"get_node_remote_hardware_pins_request", "get_node_remote_hardware_pins_response", b"get_node_remote_hardware_pins_response", "get_owner_request", b"get_owner_request", "get_owner_response", b"get_owner_response", "get_ringtone_request", b"get_ringtone_request", "get_ringtone_response", b"get_ringtone_response", "get_ui_config_request", b"get_ui_config_request", "get_ui_config_response", b"get_ui_config_response", "key_verification", b"key_verification", "lockdown_auth", b"lockdown_auth", "nodedb_reset", b"nodedb_reset", "ota_request", b"ota_request", "payload_variant", b"payload_variant", "reboot_ota_seconds", b"reboot_ota_seconds", "reboot_seconds", b"reboot_seconds", "remove_backup_preferences", b"remove_backup_preferences", "remove_by_nodenum", b"remove_by_nodenum", "remove_favorite_node", b"remove_favorite_node", "remove_fixed_position", b"remove_fixed_position", "remove_ignored_node", b"remove_ignored_node", "restore_preferences", b"restore_preferences", "send_input_event", b"send_input_event", "sensor_config", b"sensor_config", "session_passkey", b"session_passkey", "set_canned_message_module_messages", b"set_canned_message_module_messages", "set_channel", b"set_channel", "set_config", b"set_config", "set_favorite_node", b"set_favorite_node", "set_fixed_position", b"set_fixed_position", "set_ham_mode", b"set_ham_mode", "set_ignored_node", b"set_ignored_node", "set_module_config", b"set_module_config", "set_owner", b"set_owner", "set_ringtone_message", b"set_ringtone_message", "set_scale", b"set_scale", "set_time_only", b"set_time_only", "shutdown_seconds", b"shutdown_seconds", "store_ui_config", b"store_ui_config", "toggle_muted_node", b"toggle_muted_node"]) -> None: ...
def WhichOneof(self, oneof_group: typing.Literal["payload_variant", b"payload_variant"]) -> typing.Literal["get_channel_request", "get_channel_response", "get_owner_request", "get_owner_response", "get_config_request", "get_config_response", "get_module_config_request", "get_module_config_response", "get_canned_message_module_messages_request", "get_canned_message_module_messages_response", "get_device_metadata_request", "get_device_metadata_response", "get_ringtone_request", "get_ringtone_response", "get_device_connection_status_request", "get_device_connection_status_response", "set_ham_mode", "get_node_remote_hardware_pins_request", "get_node_remote_hardware_pins_response", "enter_dfu_mode_request", "delete_file_request", "set_scale", "backup_preferences", "restore_preferences", "remove_backup_preferences", "send_input_event", "set_owner", "set_channel", "set_config", "set_module_config", "set_canned_message_module_messages", "set_ringtone_message", "remove_by_nodenum", "set_favorite_node", "remove_favorite_node", "set_fixed_position", "remove_fixed_position", "set_time_only", "get_ui_config_request", "get_ui_config_response", "store_ui_config", "set_ignored_node", "remove_ignored_node", "toggle_muted_node", "begin_edit_settings", "commit_edit_settings", "add_contact", "key_verification", "factory_reset_device", "reboot_ota_seconds", "exit_simulator", "reboot_seconds", "shutdown_seconds", "factory_reset_config", "nodedb_reset", "ota_request", "sensor_config", "lockdown_auth"] | None: ...
global___AdminMessage = AdminMessage
@typing.final
class LockdownAuth(google.protobuf.message.Message):
"""
Lockdown passphrase delivery payload.
One message handles three operations distinguished by content:
- Provision (first-time): passphrase set, lock_now=false. Firmware
generates DEK, wraps with passphrase-derived KEK, persists.
- Unlock: passphrase set, lock_now=false. Firmware verifies
passphrase against stored DEK, unlocks storage, authorizes the
connection that delivered this packet.
- Lock now: lock_now=true, passphrase ignored. Firmware revokes
all client auth and reboots into the locked state.
Firmware decides between provision and unlock based on its own state
(whether a DEK file already exists). Clients do not need to track
which case applies.
"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
PASSPHRASE_FIELD_NUMBER: builtins.int
BOOTS_REMAINING_FIELD_NUMBER: builtins.int
VALID_UNTIL_EPOCH_FIELD_NUMBER: builtins.int
LOCK_NOW_FIELD_NUMBER: builtins.int
passphrase: builtins.bytes
"""
Passphrase bytes (1-32). Empty when lock_now is true.
Capped to 32 to match the proto cap on related security fields.
"""
boots_remaining: builtins.int
"""
Optional override of the boot-count token TTL granted on success.
0 = use firmware default (TOKEN_DEFAULT_BOOTS).
On reboot the firmware decrements this; when it reaches 0 the
device boots fully locked and requires a fresh passphrase.
"""
valid_until_epoch: builtins.int
"""
Optional wall-clock expiry for the unlock token, as absolute
Unix-epoch seconds. 0 = no time limit (only the boot-count TTL
applies). On boot, if the device RTC is set and now > this value,
the token is treated as expired.
"""
lock_now: builtins.bool
"""
If true, ignore passphrase fields, immediately revoke all
connection-level admin authorization, and reboot the device into
the locked state. Always honoured regardless of current lock state.
"""
def __init__(
self,
*,
passphrase: builtins.bytes = ...,
boots_remaining: builtins.int = ...,
valid_until_epoch: builtins.int = ...,
lock_now: builtins.bool = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["boots_remaining", b"boots_remaining", "lock_now", b"lock_now", "passphrase", b"passphrase", "valid_until_epoch", b"valid_until_epoch"]) -> None: ...
global___LockdownAuth = LockdownAuth
@typing.final
class HamParameters(google.protobuf.message.Message):
"""
@@ -609,3 +953,352 @@ class NodeRemoteHardwarePinsResponse(google.protobuf.message.Message):
def ClearField(self, field_name: typing.Literal["node_remote_hardware_pins", b"node_remote_hardware_pins"]) -> None: ...
global___NodeRemoteHardwarePinsResponse = NodeRemoteHardwarePinsResponse
@typing.final
class SharedContact(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
NODE_NUM_FIELD_NUMBER: builtins.int
USER_FIELD_NUMBER: builtins.int
SHOULD_IGNORE_FIELD_NUMBER: builtins.int
MANUALLY_VERIFIED_FIELD_NUMBER: builtins.int
node_num: builtins.int
"""
The node number of the contact
"""
should_ignore: builtins.bool
"""
Add this contact to the blocked / ignored list
"""
manually_verified: builtins.bool
"""
Set the IS_KEY_MANUALLY_VERIFIED bit
"""
@property
def user(self) -> meshtastic.protobuf.mesh_pb2.User:
"""
The User of the contact
"""
def __init__(
self,
*,
node_num: builtins.int = ...,
user: meshtastic.protobuf.mesh_pb2.User | None = ...,
should_ignore: builtins.bool = ...,
manually_verified: builtins.bool = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["user", b"user"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["manually_verified", b"manually_verified", "node_num", b"node_num", "should_ignore", b"should_ignore", "user", b"user"]) -> None: ...
global___SharedContact = SharedContact
@typing.final
class KeyVerificationAdmin(google.protobuf.message.Message):
"""
This message is used by a client to initiate or complete a key verification
"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
class _MessageType:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
class _MessageTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[KeyVerificationAdmin._MessageType.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
INITIATE_VERIFICATION: KeyVerificationAdmin._MessageType.ValueType # 0
"""
This is the first stage, where a client initiates
"""
PROVIDE_SECURITY_NUMBER: KeyVerificationAdmin._MessageType.ValueType # 1
"""
After the nonce has been returned over the mesh, the client prompts for the security number
And uses this message to provide it to the node.
"""
DO_VERIFY: KeyVerificationAdmin._MessageType.ValueType # 2
"""
Once the user has compared the verification message, this message notifies the node.
"""
DO_NOT_VERIFY: KeyVerificationAdmin._MessageType.ValueType # 3
"""
This is the cancel path, can be taken at any point
"""
class MessageType(_MessageType, metaclass=_MessageTypeEnumTypeWrapper):
"""
Three stages of this request.
"""
INITIATE_VERIFICATION: KeyVerificationAdmin.MessageType.ValueType # 0
"""
This is the first stage, where a client initiates
"""
PROVIDE_SECURITY_NUMBER: KeyVerificationAdmin.MessageType.ValueType # 1
"""
After the nonce has been returned over the mesh, the client prompts for the security number
And uses this message to provide it to the node.
"""
DO_VERIFY: KeyVerificationAdmin.MessageType.ValueType # 2
"""
Once the user has compared the verification message, this message notifies the node.
"""
DO_NOT_VERIFY: KeyVerificationAdmin.MessageType.ValueType # 3
"""
This is the cancel path, can be taken at any point
"""
MESSAGE_TYPE_FIELD_NUMBER: builtins.int
REMOTE_NODENUM_FIELD_NUMBER: builtins.int
NONCE_FIELD_NUMBER: builtins.int
SECURITY_NUMBER_FIELD_NUMBER: builtins.int
message_type: global___KeyVerificationAdmin.MessageType.ValueType
remote_nodenum: builtins.int
"""
The nodenum we're requesting
"""
nonce: builtins.int
"""
The nonce is used to track the connection
"""
security_number: builtins.int
"""
The 4 digit code generated by the remote node, and communicated outside the mesh
"""
def __init__(
self,
*,
message_type: global___KeyVerificationAdmin.MessageType.ValueType = ...,
remote_nodenum: builtins.int = ...,
nonce: builtins.int = ...,
security_number: builtins.int | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["_security_number", b"_security_number", "security_number", b"security_number"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["_security_number", b"_security_number", "message_type", b"message_type", "nonce", b"nonce", "remote_nodenum", b"remote_nodenum", "security_number", b"security_number"]) -> None: ...
def WhichOneof(self, oneof_group: typing.Literal["_security_number", b"_security_number"]) -> typing.Literal["security_number"] | None: ...
global___KeyVerificationAdmin = KeyVerificationAdmin
@typing.final
class SensorConfig(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
SCD4X_CONFIG_FIELD_NUMBER: builtins.int
SEN5X_CONFIG_FIELD_NUMBER: builtins.int
SCD30_CONFIG_FIELD_NUMBER: builtins.int
SHTXX_CONFIG_FIELD_NUMBER: builtins.int
@property
def scd4x_config(self) -> global___SCD4X_config:
"""
SCD4X CO2 Sensor configuration
"""
@property
def sen5x_config(self) -> global___SEN5X_config:
"""
SEN5X PM Sensor configuration
"""
@property
def scd30_config(self) -> global___SCD30_config:
"""
SCD30 CO2 Sensor configuration
"""
@property
def shtxx_config(self) -> global___SHTXX_config:
"""
SHTXX temperature and relative humidity sensor configuration
"""
def __init__(
self,
*,
scd4x_config: global___SCD4X_config | None = ...,
sen5x_config: global___SEN5X_config | None = ...,
scd30_config: global___SCD30_config | None = ...,
shtxx_config: global___SHTXX_config | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["scd30_config", b"scd30_config", "scd4x_config", b"scd4x_config", "sen5x_config", b"sen5x_config", "shtxx_config", b"shtxx_config"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["scd30_config", b"scd30_config", "scd4x_config", b"scd4x_config", "sen5x_config", b"sen5x_config", "shtxx_config", b"shtxx_config"]) -> None: ...
global___SensorConfig = SensorConfig
@typing.final
class SCD4X_config(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
SET_ASC_FIELD_NUMBER: builtins.int
SET_TARGET_CO2_CONC_FIELD_NUMBER: builtins.int
SET_TEMPERATURE_FIELD_NUMBER: builtins.int
SET_ALTITUDE_FIELD_NUMBER: builtins.int
SET_AMBIENT_PRESSURE_FIELD_NUMBER: builtins.int
FACTORY_RESET_FIELD_NUMBER: builtins.int
SET_POWER_MODE_FIELD_NUMBER: builtins.int
set_asc: builtins.bool
"""
Set Automatic self-calibration enabled
"""
set_target_co2_conc: builtins.int
"""
Recalibration target CO2 concentration in ppm (FRC or ASC)
"""
set_temperature: builtins.float
"""
Reference temperature in degC
"""
set_altitude: builtins.int
"""
Altitude of sensor in meters above sea level. 0 - 3000m (overrides ambient pressure)
"""
set_ambient_pressure: builtins.int
"""
Sensor ambient pressure in Pa. 70000 - 120000 Pa (overrides altitude)
"""
factory_reset: builtins.bool
"""
Perform a factory reset of the sensor
"""
set_power_mode: builtins.bool
"""
Power mode for sensor (true for low power, false for normal)
"""
def __init__(
self,
*,
set_asc: builtins.bool | None = ...,
set_target_co2_conc: builtins.int | None = ...,
set_temperature: builtins.float | None = ...,
set_altitude: builtins.int | None = ...,
set_ambient_pressure: builtins.int | None = ...,
factory_reset: builtins.bool | None = ...,
set_power_mode: builtins.bool | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["_factory_reset", b"_factory_reset", "_set_altitude", b"_set_altitude", "_set_ambient_pressure", b"_set_ambient_pressure", "_set_asc", b"_set_asc", "_set_power_mode", b"_set_power_mode", "_set_target_co2_conc", b"_set_target_co2_conc", "_set_temperature", b"_set_temperature", "factory_reset", b"factory_reset", "set_altitude", b"set_altitude", "set_ambient_pressure", b"set_ambient_pressure", "set_asc", b"set_asc", "set_power_mode", b"set_power_mode", "set_target_co2_conc", b"set_target_co2_conc", "set_temperature", b"set_temperature"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["_factory_reset", b"_factory_reset", "_set_altitude", b"_set_altitude", "_set_ambient_pressure", b"_set_ambient_pressure", "_set_asc", b"_set_asc", "_set_power_mode", b"_set_power_mode", "_set_target_co2_conc", b"_set_target_co2_conc", "_set_temperature", b"_set_temperature", "factory_reset", b"factory_reset", "set_altitude", b"set_altitude", "set_ambient_pressure", b"set_ambient_pressure", "set_asc", b"set_asc", "set_power_mode", b"set_power_mode", "set_target_co2_conc", b"set_target_co2_conc", "set_temperature", b"set_temperature"]) -> None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_factory_reset", b"_factory_reset"]) -> typing.Literal["factory_reset"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_set_altitude", b"_set_altitude"]) -> typing.Literal["set_altitude"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_set_ambient_pressure", b"_set_ambient_pressure"]) -> typing.Literal["set_ambient_pressure"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_set_asc", b"_set_asc"]) -> typing.Literal["set_asc"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_set_power_mode", b"_set_power_mode"]) -> typing.Literal["set_power_mode"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_set_target_co2_conc", b"_set_target_co2_conc"]) -> typing.Literal["set_target_co2_conc"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_set_temperature", b"_set_temperature"]) -> typing.Literal["set_temperature"] | None: ...
global___SCD4X_config = SCD4X_config
@typing.final
class SEN5X_config(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
SET_TEMPERATURE_FIELD_NUMBER: builtins.int
SET_ONE_SHOT_MODE_FIELD_NUMBER: builtins.int
set_temperature: builtins.float
"""
Reference temperature in degC
"""
set_one_shot_mode: builtins.bool
"""
One-shot mode (true for low power - one-shot mode, false for normal - continuous mode)
"""
def __init__(
self,
*,
set_temperature: builtins.float | None = ...,
set_one_shot_mode: builtins.bool | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["_set_one_shot_mode", b"_set_one_shot_mode", "_set_temperature", b"_set_temperature", "set_one_shot_mode", b"set_one_shot_mode", "set_temperature", b"set_temperature"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["_set_one_shot_mode", b"_set_one_shot_mode", "_set_temperature", b"_set_temperature", "set_one_shot_mode", b"set_one_shot_mode", "set_temperature", b"set_temperature"]) -> None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_set_one_shot_mode", b"_set_one_shot_mode"]) -> typing.Literal["set_one_shot_mode"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_set_temperature", b"_set_temperature"]) -> typing.Literal["set_temperature"] | None: ...
global___SEN5X_config = SEN5X_config
@typing.final
class SCD30_config(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
SET_ASC_FIELD_NUMBER: builtins.int
SET_TARGET_CO2_CONC_FIELD_NUMBER: builtins.int
SET_TEMPERATURE_FIELD_NUMBER: builtins.int
SET_ALTITUDE_FIELD_NUMBER: builtins.int
SET_MEASUREMENT_INTERVAL_FIELD_NUMBER: builtins.int
SOFT_RESET_FIELD_NUMBER: builtins.int
set_asc: builtins.bool
"""
Set Automatic self-calibration enabled
"""
set_target_co2_conc: builtins.int
"""
Recalibration target CO2 concentration in ppm (FRC or ASC)
"""
set_temperature: builtins.float
"""
Reference temperature in degC
"""
set_altitude: builtins.int
"""
Altitude of sensor in meters above sea level. 0 - 3000m (overrides ambient pressure)
"""
set_measurement_interval: builtins.int
"""
Power mode for sensor (true for low power, false for normal)
"""
soft_reset: builtins.bool
"""
Perform a factory reset of the sensor
"""
def __init__(
self,
*,
set_asc: builtins.bool | None = ...,
set_target_co2_conc: builtins.int | None = ...,
set_temperature: builtins.float | None = ...,
set_altitude: builtins.int | None = ...,
set_measurement_interval: builtins.int | None = ...,
soft_reset: builtins.bool | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["_set_altitude", b"_set_altitude", "_set_asc", b"_set_asc", "_set_measurement_interval", b"_set_measurement_interval", "_set_target_co2_conc", b"_set_target_co2_conc", "_set_temperature", b"_set_temperature", "_soft_reset", b"_soft_reset", "set_altitude", b"set_altitude", "set_asc", b"set_asc", "set_measurement_interval", b"set_measurement_interval", "set_target_co2_conc", b"set_target_co2_conc", "set_temperature", b"set_temperature", "soft_reset", b"soft_reset"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["_set_altitude", b"_set_altitude", "_set_asc", b"_set_asc", "_set_measurement_interval", b"_set_measurement_interval", "_set_target_co2_conc", b"_set_target_co2_conc", "_set_temperature", b"_set_temperature", "_soft_reset", b"_soft_reset", "set_altitude", b"set_altitude", "set_asc", b"set_asc", "set_measurement_interval", b"set_measurement_interval", "set_target_co2_conc", b"set_target_co2_conc", "set_temperature", b"set_temperature", "soft_reset", b"soft_reset"]) -> None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_set_altitude", b"_set_altitude"]) -> typing.Literal["set_altitude"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_set_asc", b"_set_asc"]) -> typing.Literal["set_asc"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_set_measurement_interval", b"_set_measurement_interval"]) -> typing.Literal["set_measurement_interval"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_set_target_co2_conc", b"_set_target_co2_conc"]) -> typing.Literal["set_target_co2_conc"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_set_temperature", b"_set_temperature"]) -> typing.Literal["set_temperature"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_soft_reset", b"_soft_reset"]) -> typing.Literal["soft_reset"] | None: ...
global___SCD30_config = SCD30_config
@typing.final
class SHTXX_config(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
SET_ACCURACY_FIELD_NUMBER: builtins.int
set_accuracy: builtins.int
"""
Accuracy mode (0 = low, 1 = medium, 2 = high)
"""
def __init__(
self,
*,
set_accuracy: builtins.int | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["_set_accuracy", b"_set_accuracy", "set_accuracy", b"set_accuracy"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["_set_accuracy", b"_set_accuracy", "set_accuracy", b"set_accuracy"]) -> None: ...
def WhichOneof(self, oneof_group: typing.Literal["_set_accuracy", b"_set_accuracy"]) -> typing.Literal["set_accuracy"] | None: ...
global___SHTXX_config = SHTXX_config

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)

View File

File diff suppressed because one or more lines are too long

View File

File diff suppressed because it is too large Load Diff

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)

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)

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

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\"\xe2\x03\n\rDeviceProfile\x12\x1d\n\tlong_name\x18\x01 \x01(\tB\x05\x92?\x02\x08(H\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\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_messagesBf\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('
_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=647
# @@protoc_insertion_point(module_scope)

View File

File diff suppressed because one or more lines are too long

View File

@@ -9,6 +9,7 @@ import google.protobuf.descriptor
import google.protobuf.internal.containers
import google.protobuf.internal.enum_type_wrapper
import google.protobuf.message
import meshtastic.protobuf.device_ui_pb2
import sys
import typing
@@ -63,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
"""
@@ -107,6 +109,21 @@ class Config(google.protobuf.message.Message):
and automatic TAK PLI (position location information) broadcasts.
Uses position module configuration to determine TAK PLI broadcast interval.
"""
ROUTER_LATE: Config.DeviceConfig._Role.ValueType # 11
"""
Description: Will always rebroadcast packets, but will do so after all other modes.
Technical Details: Used for router nodes that are intended to provide additional coverage
in areas not already covered by other routers, or to bridge around problematic terrain,
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):
"""
@@ -139,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
"""
@@ -183,6 +201,21 @@ class Config(google.protobuf.message.Message):
and automatic TAK PLI (position location information) broadcasts.
Uses position module configuration to determine TAK PLI broadcast interval.
"""
ROUTER_LATE: Config.DeviceConfig.Role.ValueType # 11
"""
Description: Will always rebroadcast packets, but will do so after all other modes.
Technical Details: Used for router nodes that are intended to provide additional coverage
in areas not already covered by other routers, or to bridge around problematic terrain,
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)
@@ -210,6 +243,15 @@ class Config(google.protobuf.message.Message):
Ignores observed messages from foreign meshes like LOCAL_ONLY,
but takes it step further by also ignoring messages from nodenums not in the node's known list (NodeDB)
"""
NONE: Config.DeviceConfig._RebroadcastMode.ValueType # 4
"""
Only permitted for SENSOR, TRACKER and TAK_TRACKER roles, this will inhibit all rebroadcasts, not unlike CLIENT_MUTE role.
"""
CORE_PORTNUMS_ONLY: Config.DeviceConfig._RebroadcastMode.ValueType # 5
"""
Ignores packets from non-standard portnums such as: TAK, RangeTest, PaxCounter, etc.
Only rebroadcasts packets with standard portnums: NodeInfo, Text, Position, Telemetry, and Routing.
"""
class RebroadcastMode(_RebroadcastMode, metaclass=_RebroadcastModeEnumTypeWrapper):
"""
@@ -236,6 +278,82 @@ class Config(google.protobuf.message.Message):
Ignores observed messages from foreign meshes like LOCAL_ONLY,
but takes it step further by also ignoring messages from nodenums not in the node's known list (NodeDB)
"""
NONE: Config.DeviceConfig.RebroadcastMode.ValueType # 4
"""
Only permitted for SENSOR, TRACKER and TAK_TRACKER roles, this will inhibit all rebroadcasts, not unlike CLIENT_MUTE role.
"""
CORE_PORTNUMS_ONLY: Config.DeviceConfig.RebroadcastMode.ValueType # 5
"""
Ignores packets from non-standard portnums such as: TAK, RangeTest, PaxCounter, etc.
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
@@ -248,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
@@ -298,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,
*,
@@ -312,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):
@@ -580,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
"""
@@ -674,6 +799,35 @@ class Config(google.protobuf.message.Message):
use static ip address
"""
class _ProtocolFlags:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
class _ProtocolFlagsEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[Config.NetworkConfig._ProtocolFlags.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
NO_BROADCAST: Config.NetworkConfig._ProtocolFlags.ValueType # 0
"""
Do not broadcast packets over any network protocol
"""
UDP_BROADCAST: Config.NetworkConfig._ProtocolFlags.ValueType # 1
"""
Enable broadcasting packets via UDP over the local network
"""
class ProtocolFlags(_ProtocolFlags, metaclass=_ProtocolFlagsEnumTypeWrapper):
"""
Available flags auxiliary network protocols
"""
NO_BROADCAST: Config.NetworkConfig.ProtocolFlags.ValueType # 0
"""
Do not broadcast packets over any network protocol
"""
UDP_BROADCAST: Config.NetworkConfig.ProtocolFlags.ValueType # 1
"""
Enable broadcasting packets via UDP over the local network
"""
@typing.final
class IpV4Config(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
@@ -716,6 +870,8 @@ class Config(google.protobuf.message.Message):
ADDRESS_MODE_FIELD_NUMBER: builtins.int
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)
@@ -731,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
"""
@@ -745,6 +901,14 @@ class Config(google.protobuf.message.Message):
"""
rsyslog Server and Port
"""
enabled_protocols: builtins.int
"""
Flags for enabling/disabling network protocols
"""
ipv6_enabled: builtins.bool
"""
Enable/Disable ipv6 support
"""
@property
def ipv4_config(self) -> global___Config.NetworkConfig.IpV4Config:
"""
@@ -762,9 +926,11 @@ class Config(google.protobuf.message.Message):
address_mode: global___Config.NetworkConfig.AddressMode.ValueType = ...,
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", "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):
@@ -774,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)
@@ -886,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):
"""
@@ -908,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)
@@ -1048,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
@@ -1095,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 = ...,
@@ -1109,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):
@@ -1214,6 +1357,51 @@ 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)
"""
class RegionCode(_RegionCode, metaclass=_RegionCodeEnumTypeWrapper): ...
UNSET: Config.LoRaConfig.RegionCode.ValueType # 0
@@ -1304,6 +1492,51 @@ 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)
"""
class _ModemPreset:
ValueType = typing.NewType("ValueType", builtins.int)
@@ -1318,6 +1551,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
"""
@@ -1350,6 +1584,36 @@ 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.
"""
class ModemPreset(_ModemPreset, metaclass=_ModemPresetEnumTypeWrapper):
"""
@@ -1364,6 +1628,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
"""
@@ -1396,6 +1661,69 @@ 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.
"""
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
@@ -1415,6 +1743,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`
@@ -1512,6 +1842,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]:
"""
@@ -1541,8 +1879,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):
@@ -1684,6 +2024,7 @@ class Config(google.protobuf.message.Message):
BLUETOOTH_FIELD_NUMBER: builtins.int
SECURITY_FIELD_NUMBER: builtins.int
SESSIONKEY_FIELD_NUMBER: builtins.int
DEVICE_UI_FIELD_NUMBER: builtins.int
@property
def device(self) -> global___Config.DeviceConfig: ...
@property
@@ -1702,6 +2043,8 @@ class Config(google.protobuf.message.Message):
def security(self) -> global___Config.SecurityConfig: ...
@property
def sessionkey(self) -> global___Config.SessionkeyConfig: ...
@property
def device_ui(self) -> meshtastic.protobuf.device_ui_pb2.DeviceUIConfig: ...
def __init__(
self,
*,
@@ -1714,9 +2057,10 @@ class Config(google.protobuf.message.Message):
bluetooth: global___Config.BluetoothConfig | None = ...,
security: global___Config.SecurityConfig | None = ...,
sessionkey: global___Config.SessionkeyConfig | None = ...,
device_ui: meshtastic.protobuf.device_ui_pb2.DeviceUIConfig | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["bluetooth", b"bluetooth", "device", b"device", "display", b"display", "lora", b"lora", "network", b"network", "payload_variant", b"payload_variant", "position", b"position", "power", b"power", "security", b"security", "sessionkey", b"sessionkey"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["bluetooth", b"bluetooth", "device", b"device", "display", b"display", "lora", b"lora", "network", b"network", "payload_variant", b"payload_variant", "position", b"position", "power", b"power", "security", b"security", "sessionkey", b"sessionkey"]) -> None: ...
def WhichOneof(self, oneof_group: typing.Literal["payload_variant", b"payload_variant"]) -> typing.Literal["device", "position", "power", "network", "display", "lora", "bluetooth", "security", "sessionkey"] | None: ...
def HasField(self, field_name: typing.Literal["bluetooth", b"bluetooth", "device", b"device", "device_ui", b"device_ui", "display", b"display", "lora", b"lora", "network", b"network", "payload_variant", b"payload_variant", "position", b"position", "power", b"power", "security", b"security", "sessionkey", b"sessionkey"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["bluetooth", b"bluetooth", "device", b"device", "device_ui", b"device_ui", "display", b"display", "lora", b"lora", "network", b"network", "payload_variant", b"payload_variant", "position", b"position", "power", b"power", "security", b"security", "sessionkey", b"sessionkey"]) -> None: ...
def WhichOneof(self, oneof_group: typing.Literal["payload_variant", b"payload_variant"]) -> typing.Literal["device", "position", "power", "network", "display", "lora", "bluetooth", "security", "sessionkey", "device_ui"] | None: ...
global___Config = Config

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)

67
meshtastic/protobuf/device_ui_pb2.py generated Normal file
View File

@@ -0,0 +1,67 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: meshtastic/protobuf/device_ui.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/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\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)

649
meshtastic/protobuf/device_ui_pb2.pyi generated Normal file
View File

@@ -0,0 +1,649 @@
"""
@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
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
class _ThemeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_Theme.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
DARK: _Theme.ValueType # 0
"""
Dark
"""
LIGHT: _Theme.ValueType # 1
"""
Light
"""
RED: _Theme.ValueType # 2
"""
Red
"""
class Theme(_Theme, metaclass=_ThemeEnumTypeWrapper): ...
DARK: Theme.ValueType # 0
"""
Dark
"""
LIGHT: Theme.ValueType # 1
"""
Light
"""
RED: Theme.ValueType # 2
"""
Red
"""
global___Theme = Theme
class _Language:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
class _LanguageEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_Language.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
ENGLISH: _Language.ValueType # 0
"""
English
"""
FRENCH: _Language.ValueType # 1
"""
French
"""
GERMAN: _Language.ValueType # 2
"""
German
"""
ITALIAN: _Language.ValueType # 3
"""
Italian
"""
PORTUGUESE: _Language.ValueType # 4
"""
Portuguese
"""
SPANISH: _Language.ValueType # 5
"""
Spanish
"""
SWEDISH: _Language.ValueType # 6
"""
Swedish
"""
FINNISH: _Language.ValueType # 7
"""
Finnish
"""
POLISH: _Language.ValueType # 8
"""
Polish
"""
TURKISH: _Language.ValueType # 9
"""
Turkish
"""
SERBIAN: _Language.ValueType # 10
"""
Serbian
"""
RUSSIAN: _Language.ValueType # 11
"""
Russian
"""
DUTCH: _Language.ValueType # 12
"""
Dutch
"""
GREEK: _Language.ValueType # 13
"""
Greek
"""
NORWEGIAN: _Language.ValueType # 14
"""
Norwegian
"""
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)
"""
TRADITIONAL_CHINESE: _Language.ValueType # 31
"""
Traditional Chinese (experimental)
"""
class Language(_Language, metaclass=_LanguageEnumTypeWrapper):
"""
Localization
"""
ENGLISH: Language.ValueType # 0
"""
English
"""
FRENCH: Language.ValueType # 1
"""
French
"""
GERMAN: Language.ValueType # 2
"""
German
"""
ITALIAN: Language.ValueType # 3
"""
Italian
"""
PORTUGUESE: Language.ValueType # 4
"""
Portuguese
"""
SPANISH: Language.ValueType # 5
"""
Spanish
"""
SWEDISH: Language.ValueType # 6
"""
Swedish
"""
FINNISH: Language.ValueType # 7
"""
Finnish
"""
POLISH: Language.ValueType # 8
"""
Polish
"""
TURKISH: Language.ValueType # 9
"""
Turkish
"""
SERBIAN: Language.ValueType # 10
"""
Serbian
"""
RUSSIAN: Language.ValueType # 11
"""
Russian
"""
DUTCH: Language.ValueType # 12
"""
Dutch
"""
GREEK: Language.ValueType # 13
"""
Greek
"""
NORWEGIAN: Language.ValueType # 14
"""
Norwegian
"""
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)
"""
TRADITIONAL_CHINESE: Language.ValueType # 31
"""
Traditional Chinese (experimental)
"""
global___Language = Language
@typing.final
class DeviceUIConfig(google.protobuf.message.Message):
"""
Protobuf structures for device-ui persistency
"""
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
SCREEN_LOCK_FIELD_NUMBER: builtins.int
SETTINGS_LOCK_FIELD_NUMBER: builtins.int
PIN_CODE_FIELD_NUMBER: builtins.int
THEME_FIELD_NUMBER: builtins.int
ALERT_ENABLED_FIELD_NUMBER: builtins.int
BANNER_ENABLED_FIELD_NUMBER: builtins.int
RING_TONE_ID_FIELD_NUMBER: builtins.int
LANGUAGE_FIELD_NUMBER: builtins.int
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.
"""
screen_brightness: builtins.int
"""
TFT display brightness 1..255
"""
screen_timeout: builtins.int
"""
Screen timeout 0..900
"""
screen_lock: builtins.bool
"""
Screen/Settings lock enabled
"""
settings_lock: builtins.bool
pin_code: builtins.int
theme: global___Theme.ValueType
"""
Color theme
"""
alert_enabled: builtins.bool
"""
Audible message, banner and ring tone
"""
banner_enabled: builtins.bool
ring_tone_id: builtins.int
language: global___Language.ValueType
"""
Localization
"""
calibration_data: builtins.bytes
"""
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:
"""
Node list filter
"""
@property
def node_highlight(self) -> global___NodeHighlight:
"""
Node list highlightening
"""
@property
def map_data(self) -> global___Map:
"""
Map related data
"""
def __init__(
self,
*,
version: builtins.int = ...,
screen_brightness: builtins.int = ...,
screen_timeout: builtins.int = ...,
screen_lock: builtins.bool = ...,
settings_lock: builtins.bool = ...,
pin_code: builtins.int = ...,
theme: global___Theme.ValueType = ...,
alert_enabled: builtins.bool = ...,
banner_enabled: builtins.bool = ...,
ring_tone_id: builtins.int = ...,
language: global___Language.ValueType = ...,
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["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
@typing.final
class NodeFilter(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
UNKNOWN_SWITCH_FIELD_NUMBER: builtins.int
OFFLINE_SWITCH_FIELD_NUMBER: builtins.int
PUBLIC_KEY_SWITCH_FIELD_NUMBER: builtins.int
HOPS_AWAY_FIELD_NUMBER: builtins.int
POSITION_SWITCH_FIELD_NUMBER: builtins.int
NODE_NAME_FIELD_NUMBER: builtins.int
CHANNEL_FIELD_NUMBER: builtins.int
unknown_switch: builtins.bool
"""
Filter unknown nodes
"""
offline_switch: builtins.bool
"""
Filter offline nodes
"""
public_key_switch: builtins.bool
"""
Filter nodes w/o public key
"""
hops_away: builtins.int
"""
Filter based on hops away
"""
position_switch: builtins.bool
"""
Filter nodes w/o position
"""
node_name: builtins.str
"""
Filter nodes by matching name string
"""
channel: builtins.int
"""
Filter based on channel
"""
def __init__(
self,
*,
unknown_switch: builtins.bool = ...,
offline_switch: builtins.bool = ...,
public_key_switch: builtins.bool = ...,
hops_away: builtins.int = ...,
position_switch: builtins.bool = ...,
node_name: builtins.str = ...,
channel: builtins.int = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["channel", b"channel", "hops_away", b"hops_away", "node_name", b"node_name", "offline_switch", b"offline_switch", "position_switch", b"position_switch", "public_key_switch", b"public_key_switch", "unknown_switch", b"unknown_switch"]) -> None: ...
global___NodeFilter = NodeFilter
@typing.final
class NodeHighlight(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
CHAT_SWITCH_FIELD_NUMBER: builtins.int
POSITION_SWITCH_FIELD_NUMBER: builtins.int
TELEMETRY_SWITCH_FIELD_NUMBER: builtins.int
IAQ_SWITCH_FIELD_NUMBER: builtins.int
NODE_NAME_FIELD_NUMBER: builtins.int
chat_switch: builtins.bool
"""
Hightlight nodes w/ active chat
"""
position_switch: builtins.bool
"""
Highlight nodes w/ position
"""
telemetry_switch: builtins.bool
"""
Highlight nodes w/ telemetry data
"""
iaq_switch: builtins.bool
"""
Highlight nodes w/ iaq data
"""
node_name: builtins.str
"""
Highlight nodes by matching name string
"""
def __init__(
self,
*,
chat_switch: builtins.bool = ...,
position_switch: builtins.bool = ...,
telemetry_switch: builtins.bool = ...,
iaq_switch: builtins.bool = ...,
node_name: builtins.str = ...,
) -> None: ...
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

View File

@@ -12,39 +12,59 @@ _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 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 telemetry_pb2 as meshtastic_dot_protobuf_dot_telemetry__pb2
from meshtastic.protobuf import config_pb2 as meshtastic_dot_protobuf_dot_config__pb2
import nanopb_pb2 as nanopb__pb2
from meshtastic.protobuf import nanopb_pb2 as meshtastic_dot_protobuf_dot_nanopb__pb2
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n$meshtastic/protobuf/deviceonly.proto\x12\x13meshtastic.protobuf\x1a!meshtastic/protobuf/channel.proto\x1a#meshtastic/protobuf/localonly.proto\x1a\x1emeshtastic/protobuf/mesh.proto\x1a#meshtastic/protobuf/telemetry.proto\x1a meshtastic/protobuf/config.proto\x1a\x0cnanopb.proto\"\x99\x01\n\x0cPositionLite\x12\x12\n\nlatitude_i\x18\x01 \x01(\x0f\x12\x13\n\x0blongitude_i\x18\x02 \x01(\x0f\x12\x10\n\x08\x61ltitude\x18\x03 \x01(\x05\x12\x0c\n\x04time\x18\x04 \x01(\x07\x12@\n\x0flocation_source\x18\x05 \x01(\x0e\x32\'.meshtastic.protobuf.Position.LocSource\"\xe2\x01\n\x08UserLite\x12\x13\n\x07macaddr\x18\x01 \x01(\x0c\x42\x02\x18\x01\x12\x11\n\tlong_name\x18\x02 \x01(\t\x12\x12\n\nshort_name\x18\x03 \x01(\t\x12\x34\n\x08hw_model\x18\x04 \x01(\x0e\x32\".meshtastic.protobuf.HardwareModel\x12\x13\n\x0bis_licensed\x18\x05 \x01(\x08\x12;\n\x04role\x18\x06 \x01(\x0e\x32-.meshtastic.protobuf.Config.DeviceConfig.Role\x12\x12\n\npublic_key\x18\x07 \x01(\x0c\"\xb8\x02\n\x0cNodeInfoLite\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\x0f\n\x07\x63hannel\x18\x07 \x01(\r\x12\x10\n\x08via_mqtt\x18\x08 \x01(\x08\x12\x16\n\thops_away\x18\t \x01(\rH\x00\x88\x01\x01\x12\x13\n\x0bis_favorite\x18\n \x01(\x08\x42\x0c\n\n_hops_away\"\x82\x04\n\x0b\x44\x65viceState\x12\x30\n\x07my_node\x18\x02 \x01(\x0b\x32\x1f.meshtastic.protobuf.MyNodeInfo\x12(\n\x05owner\x18\x03 \x01(\x0b\x32\x19.meshtastic.protobuf.User\x12\x36\n\rreceive_queue\x18\x05 \x03(\x0b\x32\x1f.meshtastic.protobuf.MeshPacket\x12\x0f\n\x07version\x18\x08 \x01(\r\x12\x38\n\x0frx_text_message\x18\x07 \x01(\x0b\x32\x1f.meshtastic.protobuf.MeshPacket\x12\x13\n\x07no_save\x18\t \x01(\x08\x42\x02\x18\x01\x12\x15\n\rdid_gps_reset\x18\x0b \x01(\x08\x12\x34\n\x0brx_waypoint\x18\x0c \x01(\x0b\x32\x1f.meshtastic.protobuf.MeshPacket\x12M\n\x19node_remote_hardware_pins\x18\r \x03(\x0b\x32*.meshtastic.protobuf.NodeRemoteHardwarePin\x12\x63\n\x0cnode_db_lite\x18\x0e \x03(\x0b\x32!.meshtastic.protobuf.NodeInfoLiteB*\x92?\'\x92\x01$std::vector<meshtastic_NodeInfoLite>\"N\n\x0b\x43hannelFile\x12.\n\x08\x63hannels\x18\x01 \x03(\x0b\x32\x1c.meshtastic.protobuf.Channel\x12\x0f\n\x07version\x18\x02 \x01(\r\"\xb2\x02\n\x08OEMStore\x12\x16\n\x0eoem_icon_width\x18\x01 \x01(\r\x12\x17\n\x0foem_icon_height\x18\x02 \x01(\r\x12\x15\n\roem_icon_bits\x18\x03 \x01(\x0c\x12\x32\n\x08oem_font\x18\x04 \x01(\x0e\x32 .meshtastic.protobuf.ScreenFonts\x12\x10\n\x08oem_text\x18\x05 \x01(\t\x12\x13\n\x0boem_aes_key\x18\x06 \x01(\x0c\x12:\n\x10oem_local_config\x18\x07 \x01(\x0b\x32 .meshtastic.protobuf.LocalConfig\x12G\n\x17oem_local_module_config\x18\x08 \x01(\x0b\x32&.meshtastic.protobuf.LocalModuleConfig*>\n\x0bScreenFonts\x12\x0e\n\nFONT_SMALL\x10\x00\x12\x0f\n\x0b\x46ONT_MEDIUM\x10\x01\x12\x0e\n\nFONT_LARGE\x10\x02\x42m\n\x13\x63om.geeksville.meshB\nDeviceOnlyZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x92?\x0b\xc2\x01\x08<vector>b\x06proto3')
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n$meshtastic/protobuf/deviceonly.proto\x12\x13meshtastic.protobuf\x1a!meshtastic/protobuf/channel.proto\x1a meshtastic/protobuf/config.proto\x1a#meshtastic/protobuf/localonly.proto\x1a\x1emeshtastic/protobuf/mesh.proto\x1a#meshtastic/protobuf/telemetry.proto\x1a meshtastic/protobuf/nanopb.proto\"\x99\x01\n\x0cPositionLite\x12\x12\n\nlatitude_i\x18\x01 \x01(\x0f\x12\x13\n\x0blongitude_i\x18\x02 \x01(\x0f\x12\x10\n\x08\x61ltitude\x18\x03 \x01(\x05\x12\x0c\n\x04time\x18\x04 \x01(\x07\x12@\n\x0flocation_source\x18\x05 \x01(\x0e\x32\'.meshtastic.protobuf.Position.LocSource\"\xb0\x02\n\x08UserLite\x12\x1a\n\x07macaddr\x18\x01 \x01(\x0c\x42\t\x18\x01\x92?\x04\x08\x06x\x01\x12\x18\n\tlong_name\x18\x02 \x01(\tB\x05\x92?\x02\x08(\x12\x19\n\nshort_name\x18\x03 \x01(\tB\x05\x92?\x02\x08\x05\x12\x34\n\x08hw_model\x18\x04 \x01(\x0e\x32\".meshtastic.protobuf.HardwareModel\x12\x13\n\x0bis_licensed\x18\x05 \x01(\x08\x12;\n\x04role\x18\x06 \x01(\x0e\x32-.meshtastic.protobuf.Config.DeviceConfig.Role\x12\x19\n\npublic_key\x18\x07 \x01(\x0c\x42\x05\x92?\x02\x08 \x12\x1c\n\x0fis_unmessagable\x18\t \x01(\x08H\x00\x88\x01\x01\x42\x12\n\x10_is_unmessagable\"\x85\x03\n\x0cNodeInfoLite\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\"\xaf\x03\n\x0b\x44\x65viceState\x12\x30\n\x07my_node\x18\x02 \x01(\x0b\x32\x1f.meshtastic.protobuf.MyNodeInfo\x12(\n\x05owner\x18\x03 \x01(\x0b\x32\x19.meshtastic.protobuf.User\x12=\n\rreceive_queue\x18\x05 \x03(\x0b\x32\x1f.meshtastic.protobuf.MeshPacketB\x05\x92?\x02\x10\x01\x12\x0f\n\x07version\x18\x08 \x01(\r\x12\x38\n\x0frx_text_message\x18\x07 \x01(\x0b\x32\x1f.meshtastic.protobuf.MeshPacket\x12\x13\n\x07no_save\x18\t \x01(\x08\x42\x02\x18\x01\x12\x19\n\rdid_gps_reset\x18\x0b \x01(\x08\x42\x02\x18\x01\x12\x34\n\x0brx_waypoint\x18\x0c \x01(\x0b\x32\x1f.meshtastic.protobuf.MeshPacket\x12T\n\x19node_remote_hardware_pins\x18\r \x03(\x0b\x32*.meshtastic.protobuf.NodeRemoteHardwarePinB\x05\x92?\x02\x10\x0c\"}\n\x0cNodeDatabase\x12\x0f\n\x07version\x18\x01 \x01(\r\x12\\\n\x05nodes\x18\x02 \x03(\x0b\x32!.meshtastic.protobuf.NodeInfoLiteB*\x92?\'\x92\x01$std::vector<meshtastic_NodeInfoLite>\"U\n\x0b\x43hannelFile\x12\x35\n\x08\x63hannels\x18\x01 \x03(\x0b\x32\x1c.meshtastic.protobuf.ChannelB\x05\x92?\x02\x10\x08\x12\x0f\n\x07version\x18\x02 \x01(\r\"\x86\x02\n\x11\x42\x61\x63kupPreferences\x12\x0f\n\x07version\x18\x01 \x01(\r\x12\x11\n\ttimestamp\x18\x02 \x01(\x07\x12\x30\n\x06\x63onfig\x18\x03 \x01(\x0b\x32 .meshtastic.protobuf.LocalConfig\x12=\n\rmodule_config\x18\x04 \x01(\x0b\x32&.meshtastic.protobuf.LocalModuleConfig\x12\x32\n\x08\x63hannels\x18\x05 \x01(\x0b\x32 .meshtastic.protobuf.ChannelFile\x12(\n\x05owner\x18\x06 \x01(\x0b\x32\x19.meshtastic.protobuf.UserBn\n\x14org.meshtastic.protoB\nDeviceOnlyZ\"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_pb2', _globals)
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\nDeviceOnlyZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000\222?\013\302\001\010<vector>'
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\nDeviceOnlyZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000\222?\013\302\001\010<vector>'
_USERLITE.fields_by_name['macaddr']._options = None
_USERLITE.fields_by_name['macaddr']._serialized_options = b'\030\001'
_USERLITE.fields_by_name['macaddr']._serialized_options = b'\030\001\222?\004\010\006x\001'
_USERLITE.fields_by_name['long_name']._options = None
_USERLITE.fields_by_name['long_name']._serialized_options = b'\222?\002\010('
_USERLITE.fields_by_name['short_name']._options = None
_USERLITE.fields_by_name['short_name']._serialized_options = b'\222?\002\010\005'
_USERLITE.fields_by_name['public_key']._options = None
_USERLITE.fields_by_name['public_key']._serialized_options = b'\222?\002\010 '
_NODEINFOLITE.fields_by_name['channel']._options = None
_NODEINFOLITE.fields_by_name['channel']._serialized_options = b'\222?\0028\010'
_NODEINFOLITE.fields_by_name['hops_away']._options = None
_NODEINFOLITE.fields_by_name['hops_away']._serialized_options = b'\222?\0028\010'
_NODEINFOLITE.fields_by_name['next_hop']._options = None
_NODEINFOLITE.fields_by_name['next_hop']._serialized_options = b'\222?\0028\010'
_DEVICESTATE.fields_by_name['receive_queue']._options = None
_DEVICESTATE.fields_by_name['receive_queue']._serialized_options = b'\222?\002\020\001'
_DEVICESTATE.fields_by_name['no_save']._options = None
_DEVICESTATE.fields_by_name['no_save']._serialized_options = b'\030\001'
_DEVICESTATE.fields_by_name['node_db_lite']._options = None
_DEVICESTATE.fields_by_name['node_db_lite']._serialized_options = b'\222?\'\222\001$std::vector<meshtastic_NodeInfoLite>'
_globals['_SCREENFONTS']._serialized_start=1856
_globals['_SCREENFONTS']._serialized_end=1918
_globals['_POSITIONLITE']._serialized_start=251
_globals['_POSITIONLITE']._serialized_end=404
_globals['_USERLITE']._serialized_start=407
_globals['_USERLITE']._serialized_end=633
_globals['_NODEINFOLITE']._serialized_start=636
_globals['_NODEINFOLITE']._serialized_end=948
_globals['_DEVICESTATE']._serialized_start=951
_globals['_DEVICESTATE']._serialized_end=1465
_globals['_CHANNELFILE']._serialized_start=1467
_globals['_CHANNELFILE']._serialized_end=1545
_globals['_OEMSTORE']._serialized_start=1548
_globals['_OEMSTORE']._serialized_end=1854
_DEVICESTATE.fields_by_name['did_gps_reset']._options = None
_DEVICESTATE.fields_by_name['did_gps_reset']._serialized_options = b'\030\001'
_DEVICESTATE.fields_by_name['node_remote_hardware_pins']._options = None
_DEVICESTATE.fields_by_name['node_remote_hardware_pins']._serialized_options = b'\222?\002\020\014'
_NODEDATABASE.fields_by_name['nodes']._options = None
_NODEDATABASE.fields_by_name['nodes']._serialized_options = b'\222?\'\222\001$std::vector<meshtastic_NodeInfoLite>'
_CHANNELFILE.fields_by_name['channels']._options = None
_CHANNELFILE.fields_by_name['channels']._serialized_options = b'\222?\002\020\010'
_globals['_POSITIONLITE']._serialized_start=271
_globals['_POSITIONLITE']._serialized_end=424
_globals['_USERLITE']._serialized_start=427
_globals['_USERLITE']._serialized_end=731
_globals['_NODEINFOLITE']._serialized_start=734
_globals['_NODEINFOLITE']._serialized_end=1123
_globals['_DEVICESTATE']._serialized_start=1126
_globals['_DEVICESTATE']._serialized_end=1557
_globals['_NODEDATABASE']._serialized_start=1559
_globals['_NODEDATABASE']._serialized_end=1684
_globals['_CHANNELFILE']._serialized_start=1686
_globals['_CHANNELFILE']._serialized_end=1771
_globals['_BACKUPPREFERENCES']._serialized_start=1774
_globals['_BACKUPPREFERENCES']._serialized_end=2036
# @@protoc_insertion_point(module_scope)

View File

@@ -7,61 +7,16 @@ 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 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 sys
import typing
if sys.version_info >= (3, 10):
import typing as typing_extensions
else:
import typing_extensions
DESCRIPTOR: google.protobuf.descriptor.FileDescriptor
class _ScreenFonts:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
class _ScreenFontsEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_ScreenFonts.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
FONT_SMALL: _ScreenFonts.ValueType # 0
"""
TODO: REPLACE
"""
FONT_MEDIUM: _ScreenFonts.ValueType # 1
"""
TODO: REPLACE
"""
FONT_LARGE: _ScreenFonts.ValueType # 2
"""
TODO: REPLACE
"""
class ScreenFonts(_ScreenFonts, metaclass=_ScreenFontsEnumTypeWrapper):
"""
Font sizes for the device screen
"""
FONT_SMALL: ScreenFonts.ValueType # 0
"""
TODO: REPLACE
"""
FONT_MEDIUM: ScreenFonts.ValueType # 1
"""
TODO: REPLACE
"""
FONT_LARGE: ScreenFonts.ValueType # 2
"""
TODO: REPLACE
"""
global___ScreenFonts = ScreenFonts
@typing.final
class PositionLite(google.protobuf.message.Message):
"""
@@ -124,6 +79,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.
@@ -159,6 +115,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,
*,
@@ -169,8 +129,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
@@ -188,6 +151,9 @@ class NodeInfoLite(google.protobuf.message.Message):
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
"""
The node number
@@ -211,13 +177,28 @@ class NodeInfoLite(google.protobuf.message.Message):
"""
hops_away: builtins.int
"""
Number of hops away from us this node is (0 if adjacent)
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.
"""
bitfield: builtins.int
"""
Bitfield for storing booleans.
LSB 0 is_key_manually_verified
LSB 1 is_muted
"""
@property
def user(self) -> global___UserLite:
"""
@@ -250,9 +231,12 @@ class NodeInfoLite(google.protobuf.message.Message):
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", "channel", b"channel", "device_metrics", b"device_metrics", "hops_away", b"hops_away", "is_favorite", b"is_favorite", "last_heard", b"last_heard", "num", b"num", "position", b"position", "snr", b"snr", "user", b"user", "via_mqtt", b"via_mqtt"]) -> None: ...
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 = NodeInfoLite
@@ -278,7 +262,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
@@ -293,7 +276,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:
@@ -335,12 +319,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,
*,
@@ -353,13 +331,40 @@ 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 NodeDatabase(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
VERSION_FIELD_NUMBER: builtins.int
NODES_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
"""
def __init__(
self,
*,
version: builtins.int = ...,
nodes: collections.abc.Iterable[global___NodeInfoLite] | None = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["nodes", b"nodes", "version", b"version"]) -> None: ...
global___NodeDatabase = NodeDatabase
@typing.final
class ChannelFile(google.protobuf.message.Message):
"""
@@ -393,71 +398,62 @@ class ChannelFile(google.protobuf.message.Message):
global___ChannelFile = ChannelFile
@typing.final
class OEMStore(google.protobuf.message.Message):
class BackupPreferences(google.protobuf.message.Message):
"""
This can be used for customizing the firmware distribution. If populated,
show a secondary bootup screen with custom logo and text for 2.5 seconds.
The on-disk backup of the node's preferences
"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
OEM_ICON_WIDTH_FIELD_NUMBER: builtins.int
OEM_ICON_HEIGHT_FIELD_NUMBER: builtins.int
OEM_ICON_BITS_FIELD_NUMBER: builtins.int
OEM_FONT_FIELD_NUMBER: builtins.int
OEM_TEXT_FIELD_NUMBER: builtins.int
OEM_AES_KEY_FIELD_NUMBER: builtins.int
OEM_LOCAL_CONFIG_FIELD_NUMBER: builtins.int
OEM_LOCAL_MODULE_CONFIG_FIELD_NUMBER: builtins.int
oem_icon_width: builtins.int
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 Logo width in Px
The version of the backup
"""
oem_icon_height: builtins.int
timestamp: builtins.int
"""
The Logo height in Px
"""
oem_icon_bits: builtins.bytes
"""
The Logo in XBM bytechar format
"""
oem_font: global___ScreenFonts.ValueType
"""
Use this font for the OEM text.
"""
oem_text: builtins.str
"""
Use this font for the OEM text.
"""
oem_aes_key: builtins.bytes
"""
The default device encryption key, 16 or 32 byte
The timestamp of the backup (if node has time)
"""
@property
def oem_local_config(self) -> meshtastic.protobuf.localonly_pb2.LocalConfig:
def config(self) -> meshtastic.protobuf.localonly_pb2.LocalConfig:
"""
A Preset LocalConfig to apply during factory reset
The node's configuration
"""
@property
def oem_local_module_config(self) -> meshtastic.protobuf.localonly_pb2.LocalModuleConfig:
def module_config(self) -> meshtastic.protobuf.localonly_pb2.LocalModuleConfig:
"""
A Preset LocalModuleConfig to apply during factory reset
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,
*,
oem_icon_width: builtins.int = ...,
oem_icon_height: builtins.int = ...,
oem_icon_bits: builtins.bytes = ...,
oem_font: global___ScreenFonts.ValueType = ...,
oem_text: builtins.str = ...,
oem_aes_key: builtins.bytes = ...,
oem_local_config: meshtastic.protobuf.localonly_pb2.LocalConfig | None = ...,
oem_local_module_config: meshtastic.protobuf.localonly_pb2.LocalModuleConfig | None = ...,
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["oem_local_config", b"oem_local_config", "oem_local_module_config", b"oem_local_module_config"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["oem_aes_key", b"oem_aes_key", "oem_font", b"oem_font", "oem_icon_bits", b"oem_icon_bits", "oem_icon_height", b"oem_icon_height", "oem_icon_width", b"oem_icon_width", "oem_local_config", b"oem_local_config", "oem_local_module_config", b"oem_local_module_config", "oem_text", b"oem_text"]) -> 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___OEMStore = OEMStore
global___BackupPreferences = BackupPreferences

33
meshtastic/protobuf/interdevice_pb2.py generated Normal file
View File

@@ -0,0 +1,33 @@
# -*- 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\"s\n\nSensorData\x12.\n\x04type\x18\x01 \x01(\x0e\x32 .meshtastic.protobuf.MessageType\x12\x15\n\x0b\x66loat_value\x18\x02 \x01(\x02H\x00\x12\x16\n\x0cuint32_value\x18\x03 \x01(\rH\x00\x42\x06\n\x04\x64\x61ta\"g\n\x12InterdeviceMessage\x12\x16\n\x04nmea\x18\x01 \x01(\tB\x06\x92?\x03\x08\x80\x08H\x00\x12\x31\n\x06sensor\x18\x02 \x01(\x0b\x32\x1f.meshtastic.protobuf.SensorDataH\x00\x42\x06\n\x04\x64\x61ta*\xd5\x01\n\x0bMessageType\x12\x07\n\x03\x41\x43K\x10\x00\x12\x15\n\x10\x43OLLECT_INTERVAL\x10\xa0\x01\x12\x0c\n\x07\x42\x45\x45P_ON\x10\xa1\x01\x12\r\n\x08\x42\x45\x45P_OFF\x10\xa2\x01\x12\r\n\x08SHUTDOWN\x10\xa3\x01\x12\r\n\x08POWER_ON\x10\xa4\x01\x12\x0f\n\nSCD41_TEMP\x10\xb0\x01\x12\x13\n\x0eSCD41_HUMIDITY\x10\xb1\x01\x12\x0e\n\tSCD41_CO2\x10\xb2\x01\x12\x0f\n\nAHT20_TEMP\x10\xb3\x01\x12\x13\n\x0e\x41HT20_HUMIDITY\x10\xb4\x01\x12\x0f\n\nTVOC_INDEX\x10\xb5\x01\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'
_INTERDEVICEMESSAGE.fields_by_name['nmea']._options = None
_INTERDEVICEMESSAGE.fields_by_name['nmea']._serialized_options = b'\222?\003\010\200\010'
_globals['_MESSAGETYPE']._serialized_start=319
_globals['_MESSAGETYPE']._serialized_end=532
_globals['_SENSORDATA']._serialized_start=96
_globals['_SENSORDATA']._serialized_end=211
_globals['_INTERDEVICEMESSAGE']._serialized_start=213
_globals['_INTERDEVICEMESSAGE']._serialized_end=316
# @@protoc_insertion_point(module_scope)

105
meshtastic/protobuf/interdevice_pb2.pyi generated Normal file
View File

@@ -0,0 +1,105 @@
"""
@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
class _MessageType:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
class _MessageTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_MessageType.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
ACK: _MessageType.ValueType # 0
COLLECT_INTERVAL: _MessageType.ValueType # 160
"""in ms"""
BEEP_ON: _MessageType.ValueType # 161
"""duration ms"""
BEEP_OFF: _MessageType.ValueType # 162
"""cancel prematurely"""
SHUTDOWN: _MessageType.ValueType # 163
POWER_ON: _MessageType.ValueType # 164
SCD41_TEMP: _MessageType.ValueType # 176
SCD41_HUMIDITY: _MessageType.ValueType # 177
SCD41_CO2: _MessageType.ValueType # 178
AHT20_TEMP: _MessageType.ValueType # 179
AHT20_HUMIDITY: _MessageType.ValueType # 180
TVOC_INDEX: _MessageType.ValueType # 181
class MessageType(_MessageType, metaclass=_MessageTypeEnumTypeWrapper):
"""encapsulate up to 1k of NMEA string data"""
ACK: MessageType.ValueType # 0
COLLECT_INTERVAL: MessageType.ValueType # 160
"""in ms"""
BEEP_ON: MessageType.ValueType # 161
"""duration ms"""
BEEP_OFF: MessageType.ValueType # 162
"""cancel prematurely"""
SHUTDOWN: MessageType.ValueType # 163
POWER_ON: MessageType.ValueType # 164
SCD41_TEMP: MessageType.ValueType # 176
SCD41_HUMIDITY: MessageType.ValueType # 177
SCD41_CO2: MessageType.ValueType # 178
AHT20_TEMP: MessageType.ValueType # 179
AHT20_HUMIDITY: MessageType.ValueType # 180
TVOC_INDEX: MessageType.ValueType # 181
global___MessageType = MessageType
@typing.final
class SensorData(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
TYPE_FIELD_NUMBER: builtins.int
FLOAT_VALUE_FIELD_NUMBER: builtins.int
UINT32_VALUE_FIELD_NUMBER: builtins.int
type: global___MessageType.ValueType
"""The message type"""
float_value: builtins.float
uint32_value: builtins.int
def __init__(
self,
*,
type: global___MessageType.ValueType = ...,
float_value: builtins.float = ...,
uint32_value: builtins.int = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["data", b"data", "float_value", b"float_value", "uint32_value", b"uint32_value"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["data", b"data", "float_value", b"float_value", "type", b"type", "uint32_value", b"uint32_value"]) -> None: ...
def WhichOneof(self, oneof_group: typing.Literal["data", b"data"]) -> typing.Literal["float_value", "uint32_value"] | None: ...
global___SensorData = SensorData
@typing.final
class InterdeviceMessage(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
NMEA_FIELD_NUMBER: builtins.int
SENSOR_FIELD_NUMBER: builtins.int
nmea: builtins.str
@property
def sensor(self) -> global___SensorData: ...
def __init__(
self,
*,
nmea: builtins.str = ...,
sensor: global___SensorData | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["data", b"data", "nmea", b"nmea", "sensor", b"sensor"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["data", b"data", "nmea", b"nmea", "sensor", b"sensor"]) -> None: ...
def WhichOneof(self, oneof_group: typing.Literal["data", b"data"]) -> typing.Literal["nmea", "sensor"] | None: ...
global___InterdeviceMessage = InterdeviceMessage

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\"\xcf\t\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\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=1876
# @@protoc_insertion_point(module_scope)

View File

@@ -119,6 +119,9 @@ 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
VERSION_FIELD_NUMBER: builtins.int
version: builtins.int
"""
@@ -204,6 +207,24 @@ 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
"""
def __init__(
self,
*,
@@ -220,9 +241,12 @@ 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 = ...,
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", "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", "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

View File

File diff suppressed because one or more lines are too long

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because one or more lines are too long

View File

@@ -9,6 +9,7 @@ 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 sys
import typing
@@ -112,7 +113,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 +166,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 +175,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):
@@ -225,6 +232,7 @@ class ModuleConfig(google.protobuf.message.Message):
ENABLED_FIELD_NUMBER: builtins.int
UPDATE_INTERVAL_FIELD_NUMBER: builtins.int
TRANSMIT_OVER_LORA_FIELD_NUMBER: builtins.int
enabled: builtins.bool
"""
Whether the Module is enabled
@@ -232,15 +240,21 @@ class ModuleConfig(google.protobuf.message.Message):
update_interval: builtins.int
"""
Interval in seconds of how often we should try to send our
Neighbor Info to the mesh
Neighbor Info (minimum is 14400, i.e., 4 hours)
"""
transmit_over_lora: builtins.bool
"""
Whether in addition to sending it to MQTT and the PhoneAPI, our NeighborInfo should be transmitted over LoRa.
Note that this is not available on a channel with default key and name.
"""
def __init__(
self,
*,
enabled: builtins.bool = ...,
update_interval: builtins.int = ...,
transmit_over_lora: builtins.bool = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["enabled", b"enabled", "update_interval", b"update_interval"]) -> None: ...
def ClearField(self, field_name: typing.Literal["enabled", b"enabled", "transmit_over_lora", b"transmit_over_lora", "update_interval", b"update_interval"]) -> None: ...
@typing.final
class DetectionSensorConfig(google.protobuf.message.Message):
@@ -479,6 +493,105 @@ 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
ENABLED_FIELD_NUMBER: builtins.int
POSITION_DEDUP_ENABLED_FIELD_NUMBER: builtins.int
POSITION_PRECISION_BITS_FIELD_NUMBER: builtins.int
POSITION_MIN_INTERVAL_SECS_FIELD_NUMBER: builtins.int
NODEINFO_DIRECT_RESPONSE_FIELD_NUMBER: builtins.int
NODEINFO_DIRECT_RESPONSE_MAX_HOPS_FIELD_NUMBER: builtins.int
RATE_LIMIT_ENABLED_FIELD_NUMBER: builtins.int
RATE_LIMIT_WINDOW_SECS_FIELD_NUMBER: builtins.int
RATE_LIMIT_MAX_PACKETS_FIELD_NUMBER: builtins.int
DROP_UNKNOWN_ENABLED_FIELD_NUMBER: builtins.int
UNKNOWN_PACKET_THRESHOLD_FIELD_NUMBER: builtins.int
EXHAUST_HOP_TELEMETRY_FIELD_NUMBER: builtins.int
EXHAUST_HOP_POSITION_FIELD_NUMBER: builtins.int
ROUTER_PRESERVE_HOPS_FIELD_NUMBER: builtins.int
enabled: builtins.bool
"""
Master enable for traffic management module
"""
position_dedup_enabled: builtins.bool
"""
Enable position deduplication to drop redundant position broadcasts
"""
position_precision_bits: builtins.int
"""
Number of bits of precision for position deduplication (0-32)
"""
position_min_interval_secs: builtins.int
"""
Minimum interval in seconds between position updates from the same node
"""
nodeinfo_direct_response: builtins.bool
"""
Enable direct response to NodeInfo requests from local cache
"""
nodeinfo_direct_response_max_hops: builtins.int
"""
Minimum hop distance from requestor before responding to NodeInfo requests
"""
rate_limit_enabled: builtins.bool
"""
Enable per-node rate limiting to throttle chatty nodes
"""
rate_limit_window_secs: builtins.int
"""
Time window in seconds for rate limiting calculations
"""
rate_limit_max_packets: builtins.int
"""
Maximum packets allowed per node within the rate limit window
"""
drop_unknown_enabled: builtins.bool
"""
Enable dropping of unknown/undecryptable packets per rate_limit_window_secs
"""
unknown_packet_threshold: builtins.int
"""
Number of unknown packets before dropping from a node
"""
exhaust_hop_telemetry: builtins.bool
"""
Set hop_limit to 0 for relayed telemetry broadcasts (own packets unaffected)
"""
exhaust_hop_position: builtins.bool
"""
Set hop_limit to 0 for relayed position broadcasts (own packets unaffected)
"""
router_preserve_hops: builtins.bool
"""
Preserve hop_limit for router-to-router traffic
"""
def __init__(
self,
*,
enabled: builtins.bool = ...,
position_dedup_enabled: builtins.bool = ...,
position_precision_bits: builtins.int = ...,
position_min_interval_secs: builtins.int = ...,
nodeinfo_direct_response: builtins.bool = ...,
nodeinfo_direct_response_max_hops: builtins.int = ...,
rate_limit_enabled: builtins.bool = ...,
rate_limit_window_secs: builtins.int = ...,
rate_limit_max_packets: builtins.int = ...,
drop_unknown_enabled: builtins.bool = ...,
unknown_packet_threshold: builtins.int = ...,
exhaust_hop_telemetry: builtins.bool = ...,
exhaust_hop_position: builtins.bool = ...,
router_preserve_hops: builtins.bool = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["drop_unknown_enabled", b"drop_unknown_enabled", "enabled", b"enabled", "exhaust_hop_position", b"exhaust_hop_position", "exhaust_hop_telemetry", b"exhaust_hop_telemetry", "nodeinfo_direct_response", b"nodeinfo_direct_response", "nodeinfo_direct_response_max_hops", b"nodeinfo_direct_response_max_hops", "position_dedup_enabled", b"position_dedup_enabled", "position_min_interval_secs", b"position_min_interval_secs", "position_precision_bits", b"position_precision_bits", "rate_limit_enabled", b"rate_limit_enabled", "rate_limit_max_packets", b"rate_limit_max_packets", "rate_limit_window_secs", b"rate_limit_window_secs", "router_preserve_hops", b"router_preserve_hops", "unknown_packet_threshold", b"unknown_packet_threshold"]) -> None: ...
@typing.final
class SerialConfig(google.protobuf.message.Message):
"""
@@ -547,6 +660,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):
"""
@@ -562,6 +689,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
@@ -795,6 +936,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
@@ -808,14 +950,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):
@@ -838,6 +986,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
@@ -898,6 +1048,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,
*,
@@ -914,13 +1073,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
@@ -1121,6 +1282,54 @@ 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 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
@@ -1134,6 +1343,9 @@ 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
@property
def mqtt(self) -> global___ModuleConfig.MQTTConfig:
"""
@@ -1212,6 +1424,24 @@ 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
"""
def __init__(
self,
*,
@@ -1228,10 +1458,13 @@ 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 = ...,
) -> 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", "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", "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"] | None: ...
global___ModuleConfig = ModuleConfig

View File

@@ -13,18 +13,27 @@ _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\"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\"\x9f\x04\n\tMapReport\x12\x18\n\tlong_name\x18\x01 \x01(\tB\x05\x92?\x02\x08(\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'
_MAPREPORT.fields_by_name['long_name']._options = None
_MAPREPORT.fields_by_name['long_name']._serialized_options = b'\222?\002\010('
_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=261
_globals['_MAPREPORT']._serialized_start=264
_globals['_MAPREPORT']._serialized_end=807
# @@protoc_insertion_point(module_scope)

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
meshtastic/protobuf/nanopb_pb2.py generated Normal file
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
meshtastic/protobuf/nanopb_pb2.pyi generated Normal file
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]

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)

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*\xa2\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\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*\xfd\x05\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\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\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=606
_globals['_PORTNUM']._serialized_end=825
# @@protoc_insertion_point(module_scope)

View File

@@ -93,6 +93,18 @@ class _PortNumEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTy
Same as Text Message but originating from Detection Sensor Module.
NOTE: This portnum traffic is not sent to the public MQTT starting at firmware version 2.2.9
"""
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.
@@ -109,6 +121,20 @@ 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.
"""
SERIAL_APP: _PortNum.ValueType # 64
"""
Provides a hardware serial interface to send and receive from the Meshtastic network.
@@ -175,6 +201,34 @@ 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.
"""
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.
@@ -278,6 +332,18 @@ DETECTION_SENSOR_APP: PortNum.ValueType # 10
Same as Text Message but originating from Detection Sensor Module.
NOTE: This portnum traffic is not sent to the public MQTT starting at firmware version 2.2.9
"""
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.
@@ -294,6 +360,20 @@ 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.
"""
SERIAL_APP: PortNum.ValueType # 64
"""
Provides a hardware serial interface to send and receive from the Meshtastic network.
@@ -360,6 +440,34 @@ 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.
"""
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.

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

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

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
meshtastic/protobuf/serial_hal_pb2.py generated Normal file
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
meshtastic/protobuf/serial_hal_pb2.pyi generated Normal file
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

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)

View File

File diff suppressed because one or more lines are too long

View File

@@ -4,7 +4,9 @@ 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
@@ -53,7 +55,7 @@ class _TelemetrySensorTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wra
"""
SHTC3: _TelemetrySensorType.ValueType # 7
"""
High accuracy temperature and humidity
TODO - REMOVE High accuracy temperature and humidity
"""
LPS22: _TelemetrySensorType.ValueType # 8
"""
@@ -73,7 +75,7 @@ class _TelemetrySensorTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wra
"""
SHT31: _TelemetrySensorType.ValueType # 12
"""
High accuracy temperature and humidity
TODO - REMOVE High accuracy temperature and humidity
"""
PMSA003I: _TelemetrySensorType.ValueType # 13
"""
@@ -93,7 +95,7 @@ class _TelemetrySensorTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wra
"""
SHT4X: _TelemetrySensorType.ValueType # 17
"""
Sensirion High accuracy temperature and humidity
TODO - REMOVE Sensirion High accuracy temperature and humidity
"""
VEML7700: _TelemetrySensorType.ValueType # 18
"""
@@ -149,7 +151,95 @@ class _TelemetrySensorTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wra
"""
MLX90614: _TelemetrySensorType.ValueType # 31
"""
MLX90614 non-contact IR temperature sensor.
MLX90614 non-contact IR temperature sensor
"""
SCD4X: _TelemetrySensorType.ValueType # 32
"""
SCD40/SCD41 CO2, humidity, temperature sensor
"""
RADSENS: _TelemetrySensorType.ValueType # 33
"""
ClimateGuard RadSens, radiation, Geiger-Muller Tube
"""
INA226: _TelemetrySensorType.ValueType # 34
"""
High accuracy current and voltage
"""
DFROBOT_RAIN: _TelemetrySensorType.ValueType # 35
"""
DFRobot Gravity tipping bucket rain gauge
"""
DPS310: _TelemetrySensorType.ValueType # 36
"""
Infineon DPS310 High accuracy pressure and temperature
"""
RAK12035: _TelemetrySensorType.ValueType # 37
"""
RAKWireless RAK12035 Soil Moisture Sensor Module
"""
MAX17261: _TelemetrySensorType.ValueType # 38
"""
MAX17261 lipo battery gauge
"""
PCT2075: _TelemetrySensorType.ValueType # 39
"""
PCT2075 Temperature Sensor
"""
ADS1X15: _TelemetrySensorType.ValueType # 40
"""
ADS1X15 ADC
"""
ADS1X15_ALT: _TelemetrySensorType.ValueType # 41
"""
ADS1X15 ADC_ALT
"""
SFA30: _TelemetrySensorType.ValueType # 42
"""
Sensirion SFA30 Formaldehyde sensor
"""
SEN5X: _TelemetrySensorType.ValueType # 43
"""
SEN5X PM SENSORS
"""
TSL2561: _TelemetrySensorType.ValueType # 44
"""
TSL2561 light sensor
"""
BH1750: _TelemetrySensorType.ValueType # 45
"""
BH1750 light sensor
"""
HDC1080: _TelemetrySensorType.ValueType # 46
"""
HDC1080 Temperature and Humidity Sensor
"""
SHT21: _TelemetrySensorType.ValueType # 47
"""
TODO - REMOVE STH21 Temperature and R. Humidity sensor
"""
STC31: _TelemetrySensorType.ValueType # 48
"""
Sensirion STC31 CO2 sensor
"""
SCD30: _TelemetrySensorType.ValueType # 49
"""
SCD30 CO2, humidity, temperature sensor
"""
SHTXX: _TelemetrySensorType.ValueType # 50
"""
SHT family of sensors for temperature and humidity
"""
DS248X: _TelemetrySensorType.ValueType # 51
"""
DS248X Bridge for one-wire temperature sensors
"""
MMC5983MA: _TelemetrySensorType.ValueType # 52
"""
MMC5983MA 3-Axis Digital Magnetic Sensor
"""
ICM42607P: _TelemetrySensorType.ValueType # 53
"""
ICM-42607-P 6Axis IMU
"""
class TelemetrySensorType(_TelemetrySensorType, metaclass=_TelemetrySensorTypeEnumTypeWrapper):
@@ -187,7 +277,7 @@ High accuracy temperature and pressure
"""
SHTC3: TelemetrySensorType.ValueType # 7
"""
High accuracy temperature and humidity
TODO - REMOVE High accuracy temperature and humidity
"""
LPS22: TelemetrySensorType.ValueType # 8
"""
@@ -207,7 +297,7 @@ QMC5883L: TelemetrySensorType.ValueType # 11
"""
SHT31: TelemetrySensorType.ValueType # 12
"""
High accuracy temperature and humidity
TODO - REMOVE High accuracy temperature and humidity
"""
PMSA003I: TelemetrySensorType.ValueType # 13
"""
@@ -227,7 +317,7 @@ RCWL-9620 Doppler Radar Distance Sensor, used for water level detection
"""
SHT4X: TelemetrySensorType.ValueType # 17
"""
Sensirion High accuracy temperature and humidity
TODO - REMOVE Sensirion High accuracy temperature and humidity
"""
VEML7700: TelemetrySensorType.ValueType # 18
"""
@@ -283,7 +373,95 @@ MAX30102 Pulse Oximeter and Heart-Rate Sensor
"""
MLX90614: TelemetrySensorType.ValueType # 31
"""
MLX90614 non-contact IR temperature sensor.
MLX90614 non-contact IR temperature sensor
"""
SCD4X: TelemetrySensorType.ValueType # 32
"""
SCD40/SCD41 CO2, humidity, temperature sensor
"""
RADSENS: TelemetrySensorType.ValueType # 33
"""
ClimateGuard RadSens, radiation, Geiger-Muller Tube
"""
INA226: TelemetrySensorType.ValueType # 34
"""
High accuracy current and voltage
"""
DFROBOT_RAIN: TelemetrySensorType.ValueType # 35
"""
DFRobot Gravity tipping bucket rain gauge
"""
DPS310: TelemetrySensorType.ValueType # 36
"""
Infineon DPS310 High accuracy pressure and temperature
"""
RAK12035: TelemetrySensorType.ValueType # 37
"""
RAKWireless RAK12035 Soil Moisture Sensor Module
"""
MAX17261: TelemetrySensorType.ValueType # 38
"""
MAX17261 lipo battery gauge
"""
PCT2075: TelemetrySensorType.ValueType # 39
"""
PCT2075 Temperature Sensor
"""
ADS1X15: TelemetrySensorType.ValueType # 40
"""
ADS1X15 ADC
"""
ADS1X15_ALT: TelemetrySensorType.ValueType # 41
"""
ADS1X15 ADC_ALT
"""
SFA30: TelemetrySensorType.ValueType # 42
"""
Sensirion SFA30 Formaldehyde sensor
"""
SEN5X: TelemetrySensorType.ValueType # 43
"""
SEN5X PM SENSORS
"""
TSL2561: TelemetrySensorType.ValueType # 44
"""
TSL2561 light sensor
"""
BH1750: TelemetrySensorType.ValueType # 45
"""
BH1750 light sensor
"""
HDC1080: TelemetrySensorType.ValueType # 46
"""
HDC1080 Temperature and Humidity Sensor
"""
SHT21: TelemetrySensorType.ValueType # 47
"""
TODO - REMOVE STH21 Temperature and R. Humidity sensor
"""
STC31: TelemetrySensorType.ValueType # 48
"""
Sensirion STC31 CO2 sensor
"""
SCD30: TelemetrySensorType.ValueType # 49
"""
SCD30 CO2, humidity, temperature sensor
"""
SHTXX: TelemetrySensorType.ValueType # 50
"""
SHT family of sensors for temperature and humidity
"""
DS248X: TelemetrySensorType.ValueType # 51
"""
DS248X Bridge for one-wire temperature sensors
"""
MMC5983MA: TelemetrySensorType.ValueType # 52
"""
MMC5983MA 3-Axis Digital Magnetic Sensor
"""
ICM42607P: TelemetrySensorType.ValueType # 53
"""
ICM-42607-P 6Axis IMU
"""
global___TelemetrySensorType = TelemetrySensorType
@@ -369,6 +547,12 @@ class EnvironmentMetrics(google.protobuf.message.Message):
WEIGHT_FIELD_NUMBER: builtins.int
WIND_GUST_FIELD_NUMBER: builtins.int
WIND_LULL_FIELD_NUMBER: builtins.int
RADIATION_FIELD_NUMBER: builtins.int
RAINFALL_1H_FIELD_NUMBER: builtins.int
RAINFALL_24H_FIELD_NUMBER: builtins.int
SOIL_MOISTURE_FIELD_NUMBER: builtins.int
SOIL_TEMPERATURE_FIELD_NUMBER: builtins.int
ONE_WIRE_TEMPERATURE_FIELD_NUMBER: builtins.int
temperature: builtins.float
"""
Temperature measured
@@ -439,6 +623,32 @@ class EnvironmentMetrics(google.protobuf.message.Message):
"""
Wind lull in m/s
"""
radiation: builtins.float
"""
Radiation in µR/h
"""
rainfall_1h: builtins.float
"""
Rainfall in the last hour in mm
"""
rainfall_24h: builtins.float
"""
Rainfall in the last 24 hours in mm
"""
soil_moisture: builtins.int
"""
Soil moisture measured (% 1-100)
"""
soil_temperature: builtins.float
"""
Soil temperature measured (*C)
"""
@property
def one_wire_temperature(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.float]:
"""
One-wire temperature (*C)
"""
def __init__(
self,
*,
@@ -459,9 +669,15 @@ class EnvironmentMetrics(google.protobuf.message.Message):
weight: builtins.float | None = ...,
wind_gust: builtins.float | None = ...,
wind_lull: builtins.float | None = ...,
radiation: builtins.float | None = ...,
rainfall_1h: builtins.float | None = ...,
rainfall_24h: builtins.float | None = ...,
soil_moisture: builtins.int | None = ...,
soil_temperature: builtins.float | None = ...,
one_wire_temperature: collections.abc.Iterable[builtins.float] | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["_barometric_pressure", b"_barometric_pressure", "_current", b"_current", "_distance", b"_distance", "_gas_resistance", b"_gas_resistance", "_iaq", b"_iaq", "_ir_lux", b"_ir_lux", "_lux", b"_lux", "_relative_humidity", b"_relative_humidity", "_temperature", b"_temperature", "_uv_lux", b"_uv_lux", "_voltage", b"_voltage", "_weight", b"_weight", "_white_lux", b"_white_lux", "_wind_direction", b"_wind_direction", "_wind_gust", b"_wind_gust", "_wind_lull", b"_wind_lull", "_wind_speed", b"_wind_speed", "barometric_pressure", b"barometric_pressure", "current", b"current", "distance", b"distance", "gas_resistance", b"gas_resistance", "iaq", b"iaq", "ir_lux", b"ir_lux", "lux", b"lux", "relative_humidity", b"relative_humidity", "temperature", b"temperature", "uv_lux", b"uv_lux", "voltage", b"voltage", "weight", b"weight", "white_lux", b"white_lux", "wind_direction", b"wind_direction", "wind_gust", b"wind_gust", "wind_lull", b"wind_lull", "wind_speed", b"wind_speed"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["_barometric_pressure", b"_barometric_pressure", "_current", b"_current", "_distance", b"_distance", "_gas_resistance", b"_gas_resistance", "_iaq", b"_iaq", "_ir_lux", b"_ir_lux", "_lux", b"_lux", "_relative_humidity", b"_relative_humidity", "_temperature", b"_temperature", "_uv_lux", b"_uv_lux", "_voltage", b"_voltage", "_weight", b"_weight", "_white_lux", b"_white_lux", "_wind_direction", b"_wind_direction", "_wind_gust", b"_wind_gust", "_wind_lull", b"_wind_lull", "_wind_speed", b"_wind_speed", "barometric_pressure", b"barometric_pressure", "current", b"current", "distance", b"distance", "gas_resistance", b"gas_resistance", "iaq", b"iaq", "ir_lux", b"ir_lux", "lux", b"lux", "relative_humidity", b"relative_humidity", "temperature", b"temperature", "uv_lux", b"uv_lux", "voltage", b"voltage", "weight", b"weight", "white_lux", b"white_lux", "wind_direction", b"wind_direction", "wind_gust", b"wind_gust", "wind_lull", b"wind_lull", "wind_speed", b"wind_speed"]) -> None: ...
def HasField(self, field_name: typing.Literal["_barometric_pressure", b"_barometric_pressure", "_current", b"_current", "_distance", b"_distance", "_gas_resistance", b"_gas_resistance", "_iaq", b"_iaq", "_ir_lux", b"_ir_lux", "_lux", b"_lux", "_radiation", b"_radiation", "_rainfall_1h", b"_rainfall_1h", "_rainfall_24h", b"_rainfall_24h", "_relative_humidity", b"_relative_humidity", "_soil_moisture", b"_soil_moisture", "_soil_temperature", b"_soil_temperature", "_temperature", b"_temperature", "_uv_lux", b"_uv_lux", "_voltage", b"_voltage", "_weight", b"_weight", "_white_lux", b"_white_lux", "_wind_direction", b"_wind_direction", "_wind_gust", b"_wind_gust", "_wind_lull", b"_wind_lull", "_wind_speed", b"_wind_speed", "barometric_pressure", b"barometric_pressure", "current", b"current", "distance", b"distance", "gas_resistance", b"gas_resistance", "iaq", b"iaq", "ir_lux", b"ir_lux", "lux", b"lux", "radiation", b"radiation", "rainfall_1h", b"rainfall_1h", "rainfall_24h", b"rainfall_24h", "relative_humidity", b"relative_humidity", "soil_moisture", b"soil_moisture", "soil_temperature", b"soil_temperature", "temperature", b"temperature", "uv_lux", b"uv_lux", "voltage", b"voltage", "weight", b"weight", "white_lux", b"white_lux", "wind_direction", b"wind_direction", "wind_gust", b"wind_gust", "wind_lull", b"wind_lull", "wind_speed", b"wind_speed"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["_barometric_pressure", b"_barometric_pressure", "_current", b"_current", "_distance", b"_distance", "_gas_resistance", b"_gas_resistance", "_iaq", b"_iaq", "_ir_lux", b"_ir_lux", "_lux", b"_lux", "_radiation", b"_radiation", "_rainfall_1h", b"_rainfall_1h", "_rainfall_24h", b"_rainfall_24h", "_relative_humidity", b"_relative_humidity", "_soil_moisture", b"_soil_moisture", "_soil_temperature", b"_soil_temperature", "_temperature", b"_temperature", "_uv_lux", b"_uv_lux", "_voltage", b"_voltage", "_weight", b"_weight", "_white_lux", b"_white_lux", "_wind_direction", b"_wind_direction", "_wind_gust", b"_wind_gust", "_wind_lull", b"_wind_lull", "_wind_speed", b"_wind_speed", "barometric_pressure", b"barometric_pressure", "current", b"current", "distance", b"distance", "gas_resistance", b"gas_resistance", "iaq", b"iaq", "ir_lux", b"ir_lux", "lux", b"lux", "one_wire_temperature", b"one_wire_temperature", "radiation", b"radiation", "rainfall_1h", b"rainfall_1h", "rainfall_24h", b"rainfall_24h", "relative_humidity", b"relative_humidity", "soil_moisture", b"soil_moisture", "soil_temperature", b"soil_temperature", "temperature", b"temperature", "uv_lux", b"uv_lux", "voltage", b"voltage", "weight", b"weight", "white_lux", b"white_lux", "wind_direction", b"wind_direction", "wind_gust", b"wind_gust", "wind_lull", b"wind_lull", "wind_speed", b"wind_speed"]) -> None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_barometric_pressure", b"_barometric_pressure"]) -> typing.Literal["barometric_pressure"] | None: ...
@typing.overload
@@ -477,8 +693,18 @@ class EnvironmentMetrics(google.protobuf.message.Message):
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_lux", b"_lux"]) -> typing.Literal["lux"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_radiation", b"_radiation"]) -> typing.Literal["radiation"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_rainfall_1h", b"_rainfall_1h"]) -> typing.Literal["rainfall_1h"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_rainfall_24h", b"_rainfall_24h"]) -> typing.Literal["rainfall_24h"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_relative_humidity", b"_relative_humidity"]) -> typing.Literal["relative_humidity"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_soil_moisture", b"_soil_moisture"]) -> typing.Literal["soil_moisture"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_soil_temperature", b"_soil_temperature"]) -> typing.Literal["soil_temperature"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_temperature", b"_temperature"]) -> typing.Literal["temperature"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_uv_lux", b"_uv_lux"]) -> typing.Literal["uv_lux"] | None: ...
@@ -513,6 +739,16 @@ class PowerMetrics(google.protobuf.message.Message):
CH2_CURRENT_FIELD_NUMBER: builtins.int
CH3_VOLTAGE_FIELD_NUMBER: builtins.int
CH3_CURRENT_FIELD_NUMBER: builtins.int
CH4_VOLTAGE_FIELD_NUMBER: builtins.int
CH4_CURRENT_FIELD_NUMBER: builtins.int
CH5_VOLTAGE_FIELD_NUMBER: builtins.int
CH5_CURRENT_FIELD_NUMBER: builtins.int
CH6_VOLTAGE_FIELD_NUMBER: builtins.int
CH6_CURRENT_FIELD_NUMBER: builtins.int
CH7_VOLTAGE_FIELD_NUMBER: builtins.int
CH7_CURRENT_FIELD_NUMBER: builtins.int
CH8_VOLTAGE_FIELD_NUMBER: builtins.int
CH8_CURRENT_FIELD_NUMBER: builtins.int
ch1_voltage: builtins.float
"""
Voltage (Ch1)
@@ -537,6 +773,46 @@ class PowerMetrics(google.protobuf.message.Message):
"""
Current (Ch3)
"""
ch4_voltage: builtins.float
"""
Voltage (Ch4)
"""
ch4_current: builtins.float
"""
Current (Ch4)
"""
ch5_voltage: builtins.float
"""
Voltage (Ch5)
"""
ch5_current: builtins.float
"""
Current (Ch5)
"""
ch6_voltage: builtins.float
"""
Voltage (Ch6)
"""
ch6_current: builtins.float
"""
Current (Ch6)
"""
ch7_voltage: builtins.float
"""
Voltage (Ch7)
"""
ch7_current: builtins.float
"""
Current (Ch7)
"""
ch8_voltage: builtins.float
"""
Voltage (Ch8)
"""
ch8_current: builtins.float
"""
Current (Ch8)
"""
def __init__(
self,
*,
@@ -546,9 +822,19 @@ class PowerMetrics(google.protobuf.message.Message):
ch2_current: builtins.float | None = ...,
ch3_voltage: builtins.float | None = ...,
ch3_current: builtins.float | None = ...,
ch4_voltage: builtins.float | None = ...,
ch4_current: builtins.float | None = ...,
ch5_voltage: builtins.float | None = ...,
ch5_current: builtins.float | None = ...,
ch6_voltage: builtins.float | None = ...,
ch6_current: builtins.float | None = ...,
ch7_voltage: builtins.float | None = ...,
ch7_current: builtins.float | None = ...,
ch8_voltage: builtins.float | None = ...,
ch8_current: builtins.float | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["_ch1_current", b"_ch1_current", "_ch1_voltage", b"_ch1_voltage", "_ch2_current", b"_ch2_current", "_ch2_voltage", b"_ch2_voltage", "_ch3_current", b"_ch3_current", "_ch3_voltage", b"_ch3_voltage", "ch1_current", b"ch1_current", "ch1_voltage", b"ch1_voltage", "ch2_current", b"ch2_current", "ch2_voltage", b"ch2_voltage", "ch3_current", b"ch3_current", "ch3_voltage", b"ch3_voltage"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["_ch1_current", b"_ch1_current", "_ch1_voltage", b"_ch1_voltage", "_ch2_current", b"_ch2_current", "_ch2_voltage", b"_ch2_voltage", "_ch3_current", b"_ch3_current", "_ch3_voltage", b"_ch3_voltage", "ch1_current", b"ch1_current", "ch1_voltage", b"ch1_voltage", "ch2_current", b"ch2_current", "ch2_voltage", b"ch2_voltage", "ch3_current", b"ch3_current", "ch3_voltage", b"ch3_voltage"]) -> None: ...
def HasField(self, field_name: typing.Literal["_ch1_current", b"_ch1_current", "_ch1_voltage", b"_ch1_voltage", "_ch2_current", b"_ch2_current", "_ch2_voltage", b"_ch2_voltage", "_ch3_current", b"_ch3_current", "_ch3_voltage", b"_ch3_voltage", "_ch4_current", b"_ch4_current", "_ch4_voltage", b"_ch4_voltage", "_ch5_current", b"_ch5_current", "_ch5_voltage", b"_ch5_voltage", "_ch6_current", b"_ch6_current", "_ch6_voltage", b"_ch6_voltage", "_ch7_current", b"_ch7_current", "_ch7_voltage", b"_ch7_voltage", "_ch8_current", b"_ch8_current", "_ch8_voltage", b"_ch8_voltage", "ch1_current", b"ch1_current", "ch1_voltage", b"ch1_voltage", "ch2_current", b"ch2_current", "ch2_voltage", b"ch2_voltage", "ch3_current", b"ch3_current", "ch3_voltage", b"ch3_voltage", "ch4_current", b"ch4_current", "ch4_voltage", b"ch4_voltage", "ch5_current", b"ch5_current", "ch5_voltage", b"ch5_voltage", "ch6_current", b"ch6_current", "ch6_voltage", b"ch6_voltage", "ch7_current", b"ch7_current", "ch7_voltage", b"ch7_voltage", "ch8_current", b"ch8_current", "ch8_voltage", b"ch8_voltage"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["_ch1_current", b"_ch1_current", "_ch1_voltage", b"_ch1_voltage", "_ch2_current", b"_ch2_current", "_ch2_voltage", b"_ch2_voltage", "_ch3_current", b"_ch3_current", "_ch3_voltage", b"_ch3_voltage", "_ch4_current", b"_ch4_current", "_ch4_voltage", b"_ch4_voltage", "_ch5_current", b"_ch5_current", "_ch5_voltage", b"_ch5_voltage", "_ch6_current", b"_ch6_current", "_ch6_voltage", b"_ch6_voltage", "_ch7_current", b"_ch7_current", "_ch7_voltage", b"_ch7_voltage", "_ch8_current", b"_ch8_current", "_ch8_voltage", b"_ch8_voltage", "ch1_current", b"ch1_current", "ch1_voltage", b"ch1_voltage", "ch2_current", b"ch2_current", "ch2_voltage", b"ch2_voltage", "ch3_current", b"ch3_current", "ch3_voltage", b"ch3_voltage", "ch4_current", b"ch4_current", "ch4_voltage", b"ch4_voltage", "ch5_current", b"ch5_current", "ch5_voltage", b"ch5_voltage", "ch6_current", b"ch6_current", "ch6_voltage", b"ch6_voltage", "ch7_current", b"ch7_current", "ch7_voltage", b"ch7_voltage", "ch8_current", b"ch8_current", "ch8_voltage", b"ch8_voltage"]) -> None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_ch1_current", b"_ch1_current"]) -> typing.Literal["ch1_current"] | None: ...
@typing.overload
@@ -561,6 +847,26 @@ class PowerMetrics(google.protobuf.message.Message):
def WhichOneof(self, oneof_group: typing.Literal["_ch3_current", b"_ch3_current"]) -> typing.Literal["ch3_current"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_ch3_voltage", b"_ch3_voltage"]) -> typing.Literal["ch3_voltage"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_ch4_current", b"_ch4_current"]) -> typing.Literal["ch4_current"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_ch4_voltage", b"_ch4_voltage"]) -> typing.Literal["ch4_voltage"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_ch5_current", b"_ch5_current"]) -> typing.Literal["ch5_current"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_ch5_voltage", b"_ch5_voltage"]) -> typing.Literal["ch5_voltage"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_ch6_current", b"_ch6_current"]) -> typing.Literal["ch6_current"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_ch6_voltage", b"_ch6_voltage"]) -> typing.Literal["ch6_voltage"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_ch7_current", b"_ch7_current"]) -> typing.Literal["ch7_current"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_ch7_voltage", b"_ch7_voltage"]) -> typing.Literal["ch7_voltage"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_ch8_current", b"_ch8_current"]) -> typing.Literal["ch8_current"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_ch8_voltage", b"_ch8_voltage"]) -> typing.Literal["ch8_voltage"] | None: ...
global___PowerMetrics = PowerMetrics
@@ -584,53 +890,118 @@ class AirQualityMetrics(google.protobuf.message.Message):
PARTICLES_25UM_FIELD_NUMBER: builtins.int
PARTICLES_50UM_FIELD_NUMBER: builtins.int
PARTICLES_100UM_FIELD_NUMBER: builtins.int
CO2_FIELD_NUMBER: builtins.int
CO2_TEMPERATURE_FIELD_NUMBER: builtins.int
CO2_HUMIDITY_FIELD_NUMBER: builtins.int
FORM_FORMALDEHYDE_FIELD_NUMBER: builtins.int
FORM_HUMIDITY_FIELD_NUMBER: builtins.int
FORM_TEMPERATURE_FIELD_NUMBER: builtins.int
PM40_STANDARD_FIELD_NUMBER: builtins.int
PARTICLES_40UM_FIELD_NUMBER: builtins.int
PM_TEMPERATURE_FIELD_NUMBER: builtins.int
PM_HUMIDITY_FIELD_NUMBER: builtins.int
PM_VOC_IDX_FIELD_NUMBER: builtins.int
PM_NOX_IDX_FIELD_NUMBER: builtins.int
PARTICLES_TPS_FIELD_NUMBER: builtins.int
pm10_standard: builtins.int
"""
Concentration Units Standard PM1.0
Concentration Units Standard PM1.0 in ug/m3
"""
pm25_standard: builtins.int
"""
Concentration Units Standard PM2.5
Concentration Units Standard PM2.5 in ug/m3
"""
pm100_standard: builtins.int
"""
Concentration Units Standard PM10.0
Concentration Units Standard PM10.0 in ug/m3
"""
pm10_environmental: builtins.int
"""
Concentration Units Environmental PM1.0
Concentration Units Environmental PM1.0 in ug/m3
"""
pm25_environmental: builtins.int
"""
Concentration Units Environmental PM2.5
Concentration Units Environmental PM2.5 in ug/m3
"""
pm100_environmental: builtins.int
"""
Concentration Units Environmental PM10.0
Concentration Units Environmental PM10.0 in ug/m3
"""
particles_03um: builtins.int
"""
0.3um Particle Count
0.3um Particle Count in #/0.1l
"""
particles_05um: builtins.int
"""
0.5um Particle Count
0.5um Particle Count in #/0.1l
"""
particles_10um: builtins.int
"""
1.0um Particle Count
1.0um Particle Count in #/0.1l
"""
particles_25um: builtins.int
"""
2.5um Particle Count
2.5um Particle Count in #/0.1l
"""
particles_50um: builtins.int
"""
5.0um Particle Count
5.0um Particle Count in #/0.1l
"""
particles_100um: builtins.int
"""
10.0um Particle Count
10.0um Particle Count in #/0.1l
"""
co2: builtins.int
"""
CO2 concentration in ppm
"""
co2_temperature: builtins.float
"""
CO2 sensor temperature in degC
"""
co2_humidity: builtins.float
"""
CO2 sensor relative humidity in %
"""
form_formaldehyde: builtins.float
"""
Formaldehyde sensor formaldehyde concentration in ppb
"""
form_humidity: builtins.float
"""
Formaldehyde sensor relative humidity in %RH
"""
form_temperature: builtins.float
"""
Formaldehyde sensor temperature in degrees Celsius
"""
pm40_standard: builtins.int
"""
Concentration Units Standard PM4.0 in ug/m3
"""
particles_40um: builtins.int
"""
4.0um Particle Count in #/0.1l
"""
pm_temperature: builtins.float
"""
PM Sensor Temperature
"""
pm_humidity: builtins.float
"""
PM Sensor humidity
"""
pm_voc_idx: builtins.float
"""
PM Sensor VOC Index
"""
pm_nox_idx: builtins.float
"""
PM Sensor NOx Index
"""
particles_tps: builtins.float
"""
Typical Particle Size in um
"""
def __init__(
self,
@@ -647,9 +1018,34 @@ class AirQualityMetrics(google.protobuf.message.Message):
particles_25um: builtins.int | None = ...,
particles_50um: builtins.int | None = ...,
particles_100um: builtins.int | None = ...,
co2: builtins.int | None = ...,
co2_temperature: builtins.float | None = ...,
co2_humidity: builtins.float | None = ...,
form_formaldehyde: builtins.float | None = ...,
form_humidity: builtins.float | None = ...,
form_temperature: builtins.float | None = ...,
pm40_standard: builtins.int | None = ...,
particles_40um: builtins.int | None = ...,
pm_temperature: builtins.float | None = ...,
pm_humidity: builtins.float | None = ...,
pm_voc_idx: builtins.float | None = ...,
pm_nox_idx: builtins.float | None = ...,
particles_tps: builtins.float | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["_particles_03um", b"_particles_03um", "_particles_05um", b"_particles_05um", "_particles_100um", b"_particles_100um", "_particles_10um", b"_particles_10um", "_particles_25um", b"_particles_25um", "_particles_50um", b"_particles_50um", "_pm100_environmental", b"_pm100_environmental", "_pm100_standard", b"_pm100_standard", "_pm10_environmental", b"_pm10_environmental", "_pm10_standard", b"_pm10_standard", "_pm25_environmental", b"_pm25_environmental", "_pm25_standard", b"_pm25_standard", "particles_03um", b"particles_03um", "particles_05um", b"particles_05um", "particles_100um", b"particles_100um", "particles_10um", b"particles_10um", "particles_25um", b"particles_25um", "particles_50um", b"particles_50um", "pm100_environmental", b"pm100_environmental", "pm100_standard", b"pm100_standard", "pm10_environmental", b"pm10_environmental", "pm10_standard", b"pm10_standard", "pm25_environmental", b"pm25_environmental", "pm25_standard", b"pm25_standard"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["_particles_03um", b"_particles_03um", "_particles_05um", b"_particles_05um", "_particles_100um", b"_particles_100um", "_particles_10um", b"_particles_10um", "_particles_25um", b"_particles_25um", "_particles_50um", b"_particles_50um", "_pm100_environmental", b"_pm100_environmental", "_pm100_standard", b"_pm100_standard", "_pm10_environmental", b"_pm10_environmental", "_pm10_standard", b"_pm10_standard", "_pm25_environmental", b"_pm25_environmental", "_pm25_standard", b"_pm25_standard", "particles_03um", b"particles_03um", "particles_05um", b"particles_05um", "particles_100um", b"particles_100um", "particles_10um", b"particles_10um", "particles_25um", b"particles_25um", "particles_50um", b"particles_50um", "pm100_environmental", b"pm100_environmental", "pm100_standard", b"pm100_standard", "pm10_environmental", b"pm10_environmental", "pm10_standard", b"pm10_standard", "pm25_environmental", b"pm25_environmental", "pm25_standard", b"pm25_standard"]) -> None: ...
def HasField(self, field_name: typing.Literal["_co2", b"_co2", "_co2_humidity", b"_co2_humidity", "_co2_temperature", b"_co2_temperature", "_form_formaldehyde", b"_form_formaldehyde", "_form_humidity", b"_form_humidity", "_form_temperature", b"_form_temperature", "_particles_03um", b"_particles_03um", "_particles_05um", b"_particles_05um", "_particles_100um", b"_particles_100um", "_particles_10um", b"_particles_10um", "_particles_25um", b"_particles_25um", "_particles_40um", b"_particles_40um", "_particles_50um", b"_particles_50um", "_particles_tps", b"_particles_tps", "_pm100_environmental", b"_pm100_environmental", "_pm100_standard", b"_pm100_standard", "_pm10_environmental", b"_pm10_environmental", "_pm10_standard", b"_pm10_standard", "_pm25_environmental", b"_pm25_environmental", "_pm25_standard", b"_pm25_standard", "_pm40_standard", b"_pm40_standard", "_pm_humidity", b"_pm_humidity", "_pm_nox_idx", b"_pm_nox_idx", "_pm_temperature", b"_pm_temperature", "_pm_voc_idx", b"_pm_voc_idx", "co2", b"co2", "co2_humidity", b"co2_humidity", "co2_temperature", b"co2_temperature", "form_formaldehyde", b"form_formaldehyde", "form_humidity", b"form_humidity", "form_temperature", b"form_temperature", "particles_03um", b"particles_03um", "particles_05um", b"particles_05um", "particles_100um", b"particles_100um", "particles_10um", b"particles_10um", "particles_25um", b"particles_25um", "particles_40um", b"particles_40um", "particles_50um", b"particles_50um", "particles_tps", b"particles_tps", "pm100_environmental", b"pm100_environmental", "pm100_standard", b"pm100_standard", "pm10_environmental", b"pm10_environmental", "pm10_standard", b"pm10_standard", "pm25_environmental", b"pm25_environmental", "pm25_standard", b"pm25_standard", "pm40_standard", b"pm40_standard", "pm_humidity", b"pm_humidity", "pm_nox_idx", b"pm_nox_idx", "pm_temperature", b"pm_temperature", "pm_voc_idx", b"pm_voc_idx"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["_co2", b"_co2", "_co2_humidity", b"_co2_humidity", "_co2_temperature", b"_co2_temperature", "_form_formaldehyde", b"_form_formaldehyde", "_form_humidity", b"_form_humidity", "_form_temperature", b"_form_temperature", "_particles_03um", b"_particles_03um", "_particles_05um", b"_particles_05um", "_particles_100um", b"_particles_100um", "_particles_10um", b"_particles_10um", "_particles_25um", b"_particles_25um", "_particles_40um", b"_particles_40um", "_particles_50um", b"_particles_50um", "_particles_tps", b"_particles_tps", "_pm100_environmental", b"_pm100_environmental", "_pm100_standard", b"_pm100_standard", "_pm10_environmental", b"_pm10_environmental", "_pm10_standard", b"_pm10_standard", "_pm25_environmental", b"_pm25_environmental", "_pm25_standard", b"_pm25_standard", "_pm40_standard", b"_pm40_standard", "_pm_humidity", b"_pm_humidity", "_pm_nox_idx", b"_pm_nox_idx", "_pm_temperature", b"_pm_temperature", "_pm_voc_idx", b"_pm_voc_idx", "co2", b"co2", "co2_humidity", b"co2_humidity", "co2_temperature", b"co2_temperature", "form_formaldehyde", b"form_formaldehyde", "form_humidity", b"form_humidity", "form_temperature", b"form_temperature", "particles_03um", b"particles_03um", "particles_05um", b"particles_05um", "particles_100um", b"particles_100um", "particles_10um", b"particles_10um", "particles_25um", b"particles_25um", "particles_40um", b"particles_40um", "particles_50um", b"particles_50um", "particles_tps", b"particles_tps", "pm100_environmental", b"pm100_environmental", "pm100_standard", b"pm100_standard", "pm10_environmental", b"pm10_environmental", "pm10_standard", b"pm10_standard", "pm25_environmental", b"pm25_environmental", "pm25_standard", b"pm25_standard", "pm40_standard", b"pm40_standard", "pm_humidity", b"pm_humidity", "pm_nox_idx", b"pm_nox_idx", "pm_temperature", b"pm_temperature", "pm_voc_idx", b"pm_voc_idx"]) -> None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_co2", b"_co2"]) -> typing.Literal["co2"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_co2_humidity", b"_co2_humidity"]) -> typing.Literal["co2_humidity"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_co2_temperature", b"_co2_temperature"]) -> typing.Literal["co2_temperature"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_form_formaldehyde", b"_form_formaldehyde"]) -> typing.Literal["form_formaldehyde"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_form_humidity", b"_form_humidity"]) -> typing.Literal["form_humidity"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_form_temperature", b"_form_temperature"]) -> typing.Literal["form_temperature"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_particles_03um", b"_particles_03um"]) -> typing.Literal["particles_03um"] | None: ...
@typing.overload
@@ -661,8 +1057,12 @@ class AirQualityMetrics(google.protobuf.message.Message):
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_particles_25um", b"_particles_25um"]) -> typing.Literal["particles_25um"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_particles_40um", b"_particles_40um"]) -> typing.Literal["particles_40um"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_particles_50um", b"_particles_50um"]) -> typing.Literal["particles_50um"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_particles_tps", b"_particles_tps"]) -> typing.Literal["particles_tps"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_pm100_environmental", b"_pm100_environmental"]) -> typing.Literal["pm100_environmental"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_pm100_standard", b"_pm100_standard"]) -> typing.Literal["pm100_standard"] | None: ...
@@ -674,6 +1074,16 @@ class AirQualityMetrics(google.protobuf.message.Message):
def WhichOneof(self, oneof_group: typing.Literal["_pm25_environmental", b"_pm25_environmental"]) -> typing.Literal["pm25_environmental"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_pm25_standard", b"_pm25_standard"]) -> typing.Literal["pm25_standard"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_pm40_standard", b"_pm40_standard"]) -> typing.Literal["pm40_standard"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_pm_humidity", b"_pm_humidity"]) -> typing.Literal["pm_humidity"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_pm_nox_idx", b"_pm_nox_idx"]) -> typing.Literal["pm_nox_idx"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_pm_temperature", b"_pm_temperature"]) -> typing.Literal["pm_temperature"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_pm_voc_idx", b"_pm_voc_idx"]) -> typing.Literal["pm_voc_idx"] | None: ...
global___AirQualityMetrics = AirQualityMetrics
@@ -696,6 +1106,10 @@ class LocalStats(google.protobuf.message.Message):
NUM_RX_DUPE_FIELD_NUMBER: builtins.int
NUM_TX_RELAY_FIELD_NUMBER: builtins.int
NUM_TX_RELAY_CANCELED_FIELD_NUMBER: builtins.int
HEAP_TOTAL_BYTES_FIELD_NUMBER: builtins.int
HEAP_FREE_BYTES_FIELD_NUMBER: builtins.int
NUM_TX_DROPPED_FIELD_NUMBER: builtins.int
NOISE_FLOOR_FIELD_NUMBER: builtins.int
uptime_seconds: builtins.int
"""
How long the device has been running since the last reboot (in seconds)
@@ -742,6 +1156,22 @@ class LocalStats(google.protobuf.message.Message):
Number of times we canceled a packet to be relayed, because someone else did it before us.
This will always be zero for ROUTERs/REPEATERs. If this number is high, some other node(s) is/are relaying faster than you.
"""
heap_total_bytes: builtins.int
"""
Number of bytes used in the heap
"""
heap_free_bytes: builtins.int
"""
Number of bytes free in the heap
"""
num_tx_dropped: builtins.int
"""
Number of packets that were dropped because the transmit queue was full.
"""
noise_floor: builtins.int
"""
Noise floor value measured in dBm
"""
def __init__(
self,
*,
@@ -756,11 +1186,73 @@ class LocalStats(google.protobuf.message.Message):
num_rx_dupe: builtins.int = ...,
num_tx_relay: builtins.int = ...,
num_tx_relay_canceled: builtins.int = ...,
heap_total_bytes: builtins.int = ...,
heap_free_bytes: builtins.int = ...,
num_tx_dropped: builtins.int = ...,
noise_floor: builtins.int = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["air_util_tx", b"air_util_tx", "channel_utilization", b"channel_utilization", "num_online_nodes", b"num_online_nodes", "num_packets_rx", b"num_packets_rx", "num_packets_rx_bad", b"num_packets_rx_bad", "num_packets_tx", b"num_packets_tx", "num_rx_dupe", b"num_rx_dupe", "num_total_nodes", b"num_total_nodes", "num_tx_relay", b"num_tx_relay", "num_tx_relay_canceled", b"num_tx_relay_canceled", "uptime_seconds", b"uptime_seconds"]) -> None: ...
def ClearField(self, field_name: typing.Literal["air_util_tx", b"air_util_tx", "channel_utilization", b"channel_utilization", "heap_free_bytes", b"heap_free_bytes", "heap_total_bytes", b"heap_total_bytes", "noise_floor", b"noise_floor", "num_online_nodes", b"num_online_nodes", "num_packets_rx", b"num_packets_rx", "num_packets_rx_bad", b"num_packets_rx_bad", "num_packets_tx", b"num_packets_tx", "num_rx_dupe", b"num_rx_dupe", "num_total_nodes", b"num_total_nodes", "num_tx_dropped", b"num_tx_dropped", "num_tx_relay", b"num_tx_relay", "num_tx_relay_canceled", b"num_tx_relay_canceled", "uptime_seconds", b"uptime_seconds"]) -> None: ...
global___LocalStats = LocalStats
@typing.final
class TrafficManagementStats(google.protobuf.message.Message):
"""
Traffic management statistics for mesh network optimization
"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
PACKETS_INSPECTED_FIELD_NUMBER: builtins.int
POSITION_DEDUP_DROPS_FIELD_NUMBER: builtins.int
NODEINFO_CACHE_HITS_FIELD_NUMBER: builtins.int
RATE_LIMIT_DROPS_FIELD_NUMBER: builtins.int
UNKNOWN_PACKET_DROPS_FIELD_NUMBER: builtins.int
HOP_EXHAUSTED_PACKETS_FIELD_NUMBER: builtins.int
ROUTER_HOPS_PRESERVED_FIELD_NUMBER: builtins.int
packets_inspected: builtins.int
"""
Total number of packets inspected by traffic management
"""
position_dedup_drops: builtins.int
"""
Number of position packets dropped due to deduplication
"""
nodeinfo_cache_hits: builtins.int
"""
Number of NodeInfo requests answered from cache
"""
rate_limit_drops: builtins.int
"""
Number of packets dropped due to rate limiting
"""
unknown_packet_drops: builtins.int
"""
Number of unknown/undecryptable packets dropped
"""
hop_exhausted_packets: builtins.int
"""
Number of packets with hop_limit exhausted for local-only broadcast
"""
router_hops_preserved: builtins.int
"""
Number of times router hop preservation was applied
"""
def __init__(
self,
*,
packets_inspected: builtins.int = ...,
position_dedup_drops: builtins.int = ...,
nodeinfo_cache_hits: builtins.int = ...,
rate_limit_drops: builtins.int = ...,
unknown_packet_drops: builtins.int = ...,
hop_exhausted_packets: builtins.int = ...,
router_hops_preserved: builtins.int = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["hop_exhausted_packets", b"hop_exhausted_packets", "nodeinfo_cache_hits", b"nodeinfo_cache_hits", "packets_inspected", b"packets_inspected", "position_dedup_drops", b"position_dedup_drops", "rate_limit_drops", b"rate_limit_drops", "router_hops_preserved", b"router_hops_preserved", "unknown_packet_drops", b"unknown_packet_drops"]) -> None: ...
global___TrafficManagementStats = TrafficManagementStats
@typing.final
class HealthMetrics(google.protobuf.message.Message):
"""
@@ -802,6 +1294,84 @@ class HealthMetrics(google.protobuf.message.Message):
global___HealthMetrics = HealthMetrics
@typing.final
class HostMetrics(google.protobuf.message.Message):
"""
Linux host metrics
"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
UPTIME_SECONDS_FIELD_NUMBER: builtins.int
FREEMEM_BYTES_FIELD_NUMBER: builtins.int
DISKFREE1_BYTES_FIELD_NUMBER: builtins.int
DISKFREE2_BYTES_FIELD_NUMBER: builtins.int
DISKFREE3_BYTES_FIELD_NUMBER: builtins.int
LOAD1_FIELD_NUMBER: builtins.int
LOAD5_FIELD_NUMBER: builtins.int
LOAD15_FIELD_NUMBER: builtins.int
USER_STRING_FIELD_NUMBER: builtins.int
uptime_seconds: builtins.int
"""
Host system uptime
"""
freemem_bytes: builtins.int
"""
Host system free memory
"""
diskfree1_bytes: builtins.int
"""
Host system disk space free for /
"""
diskfree2_bytes: builtins.int
"""
Secondary system disk space free
"""
diskfree3_bytes: builtins.int
"""
Tertiary disk space free
"""
load1: builtins.int
"""
Host system one minute load in 1/100ths
"""
load5: builtins.int
"""
Host system five minute load in 1/100ths
"""
load15: builtins.int
"""
Host system fifteen minute load in 1/100ths
"""
user_string: builtins.str
"""
Optional User-provided string for arbitrary host system information
that doesn't make sense as a dedicated entry.
"""
def __init__(
self,
*,
uptime_seconds: builtins.int = ...,
freemem_bytes: builtins.int = ...,
diskfree1_bytes: builtins.int = ...,
diskfree2_bytes: builtins.int | None = ...,
diskfree3_bytes: builtins.int | None = ...,
load1: builtins.int = ...,
load5: builtins.int = ...,
load15: builtins.int = ...,
user_string: builtins.str | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["_diskfree2_bytes", b"_diskfree2_bytes", "_diskfree3_bytes", b"_diskfree3_bytes", "_user_string", b"_user_string", "diskfree2_bytes", b"diskfree2_bytes", "diskfree3_bytes", b"diskfree3_bytes", "user_string", b"user_string"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["_diskfree2_bytes", b"_diskfree2_bytes", "_diskfree3_bytes", b"_diskfree3_bytes", "_user_string", b"_user_string", "diskfree1_bytes", b"diskfree1_bytes", "diskfree2_bytes", b"diskfree2_bytes", "diskfree3_bytes", b"diskfree3_bytes", "freemem_bytes", b"freemem_bytes", "load1", b"load1", "load15", b"load15", "load5", b"load5", "uptime_seconds", b"uptime_seconds", "user_string", b"user_string"]) -> None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_diskfree2_bytes", b"_diskfree2_bytes"]) -> typing.Literal["diskfree2_bytes"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_diskfree3_bytes", b"_diskfree3_bytes"]) -> typing.Literal["diskfree3_bytes"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_user_string", b"_user_string"]) -> typing.Literal["user_string"] | None: ...
global___HostMetrics = HostMetrics
@typing.final
class Telemetry(google.protobuf.message.Message):
"""
@@ -817,6 +1387,8 @@ class Telemetry(google.protobuf.message.Message):
POWER_METRICS_FIELD_NUMBER: builtins.int
LOCAL_STATS_FIELD_NUMBER: builtins.int
HEALTH_METRICS_FIELD_NUMBER: builtins.int
HOST_METRICS_FIELD_NUMBER: builtins.int
TRAFFIC_MANAGEMENT_STATS_FIELD_NUMBER: builtins.int
time: builtins.int
"""
Seconds since 1970 - or 0 for unknown/unset
@@ -857,6 +1429,18 @@ class Telemetry(google.protobuf.message.Message):
Health telemetry metrics
"""
@property
def host_metrics(self) -> global___HostMetrics:
"""
Linux host metrics
"""
@property
def traffic_management_stats(self) -> global___TrafficManagementStats:
"""
Traffic management statistics
"""
def __init__(
self,
*,
@@ -867,10 +1451,12 @@ class Telemetry(google.protobuf.message.Message):
power_metrics: global___PowerMetrics | None = ...,
local_stats: global___LocalStats | None = ...,
health_metrics: global___HealthMetrics | None = ...,
host_metrics: global___HostMetrics | None = ...,
traffic_management_stats: global___TrafficManagementStats | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["air_quality_metrics", b"air_quality_metrics", "device_metrics", b"device_metrics", "environment_metrics", b"environment_metrics", "health_metrics", b"health_metrics", "local_stats", b"local_stats", "power_metrics", b"power_metrics", "variant", b"variant"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["air_quality_metrics", b"air_quality_metrics", "device_metrics", b"device_metrics", "environment_metrics", b"environment_metrics", "health_metrics", b"health_metrics", "local_stats", b"local_stats", "power_metrics", b"power_metrics", "time", b"time", "variant", b"variant"]) -> None: ...
def WhichOneof(self, oneof_group: typing.Literal["variant", b"variant"]) -> typing.Literal["device_metrics", "environment_metrics", "air_quality_metrics", "power_metrics", "local_stats", "health_metrics"] | None: ...
def HasField(self, field_name: typing.Literal["air_quality_metrics", b"air_quality_metrics", "device_metrics", b"device_metrics", "environment_metrics", b"environment_metrics", "health_metrics", b"health_metrics", "host_metrics", b"host_metrics", "local_stats", b"local_stats", "power_metrics", b"power_metrics", "traffic_management_stats", b"traffic_management_stats", "variant", b"variant"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["air_quality_metrics", b"air_quality_metrics", "device_metrics", b"device_metrics", "environment_metrics", b"environment_metrics", "health_metrics", b"health_metrics", "host_metrics", b"host_metrics", "local_stats", b"local_stats", "power_metrics", b"power_metrics", "time", b"time", "traffic_management_stats", b"traffic_management_stats", "variant", b"variant"]) -> None: ...
def WhichOneof(self, oneof_group: typing.Literal["variant", b"variant"]) -> typing.Literal["device_metrics", "environment_metrics", "air_quality_metrics", "power_metrics", "local_stats", "health_metrics", "host_metrics", "traffic_management_stats"] | None: ...
global___Telemetry = Telemetry
@@ -901,3 +1487,62 @@ class Nau7802Config(google.protobuf.message.Message):
def ClearField(self, field_name: typing.Literal["calibrationFactor", b"calibrationFactor", "zeroOffset", b"zeroOffset"]) -> None: ...
global___Nau7802Config = Nau7802Config
@typing.final
class SEN5XState(google.protobuf.message.Message):
"""
SEN5X State, for saving to flash
"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
LAST_CLEANING_TIME_FIELD_NUMBER: builtins.int
LAST_CLEANING_VALID_FIELD_NUMBER: builtins.int
ONE_SHOT_MODE_FIELD_NUMBER: builtins.int
VOC_STATE_TIME_FIELD_NUMBER: builtins.int
VOC_STATE_VALID_FIELD_NUMBER: builtins.int
VOC_STATE_ARRAY_FIELD_NUMBER: builtins.int
last_cleaning_time: builtins.int
"""
Last cleaning time for SEN5X
"""
last_cleaning_valid: builtins.bool
"""
Last cleaning time for SEN5X - valid flag
"""
one_shot_mode: builtins.bool
"""
Config flag for one-shot mode (see admin.proto)
"""
voc_state_time: builtins.int
"""
Last VOC state time for SEN55
"""
voc_state_valid: builtins.bool
"""
Last VOC state validity flag for SEN55
"""
voc_state_array: builtins.int
"""
VOC state array (8x uint8t) for SEN55
"""
def __init__(
self,
*,
last_cleaning_time: builtins.int = ...,
last_cleaning_valid: builtins.bool = ...,
one_shot_mode: builtins.bool = ...,
voc_state_time: builtins.int | None = ...,
voc_state_valid: builtins.bool | None = ...,
voc_state_array: builtins.int | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["_voc_state_array", b"_voc_state_array", "_voc_state_time", b"_voc_state_time", "_voc_state_valid", b"_voc_state_valid", "voc_state_array", b"voc_state_array", "voc_state_time", b"voc_state_time", "voc_state_valid", b"voc_state_valid"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["_voc_state_array", b"_voc_state_array", "_voc_state_time", b"_voc_state_time", "_voc_state_valid", b"_voc_state_valid", "last_cleaning_time", b"last_cleaning_time", "last_cleaning_valid", b"last_cleaning_valid", "one_shot_mode", b"one_shot_mode", "voc_state_array", b"voc_state_array", "voc_state_time", b"voc_state_time", "voc_state_valid", b"voc_state_valid"]) -> None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_voc_state_array", b"_voc_state_array"]) -> typing.Literal["voc_state_array"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_voc_state_time", b"_voc_state_time"]) -> typing.Literal["voc_state_time"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_voc_state_valid", b"_voc_state_valid"]) -> typing.Literal["voc_state_valid"] | None: ...
global___SEN5XState = SEN5XState

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)

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

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)

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

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

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
]

View File

@@ -1,13 +1,18 @@
"""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
logger = logging.getLogger(__name__)
class TCPInterface(StreamInterface):
"""Interface class for meshtastic devices over a TCP link"""
@@ -16,71 +21,129 @@ 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 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:
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()

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

View File

@@ -6,7 +6,11 @@ import sys
import pytest
from meshtastic.analysis.__main__ import main
try:
# Depends upon matplotlib & other packages in poetry's analysis group, not installed by default
from meshtastic.analysis.__main__ import main
except ImportError:
pytest.skip("Can't import meshtastic.analysis", allow_module_level=True)
@pytest.mark.unit

View File

@@ -0,0 +1,69 @@
"""Meshtastic unit tests for ble_interface.py"""
from unittest.mock import MagicMock, patch
import pytest
from bleak.exc import BleakError
from ..ble_interface import BLEInterface
@pytest.mark.unit
def test_ble_error_default_kind_unknown():
"""BLEError defaults to UNKNOWN kind."""
error = BLEInterface.BLEError("test")
assert error.kind == BLEInterface.BLEError.UNKNOWN
@pytest.mark.unit
def test_ble_find_device_not_found_sets_kind():
"""find_device emits DEVICE_NOT_FOUND for no scan results."""
iface = object.__new__(BLEInterface)
with patch("meshtastic.ble_interface.BLEInterface.scan", return_value=[]):
with pytest.raises(BLEInterface.BLEError) as excinfo:
iface.find_device("missing")
assert excinfo.value.kind == BLEInterface.BLEError.DEVICE_NOT_FOUND
@pytest.mark.unit
def test_ble_find_device_multiple_sets_kind():
"""find_device emits MULTIPLE_DEVICES for ambiguous matches."""
iface = object.__new__(BLEInterface)
first = MagicMock()
first.name = "dup"
first.address = "AA:AA:AA:AA:AA:01"
second = MagicMock()
second.name = "dup"
second.address = "AA:AA:AA:AA:AA:02"
with patch(
"meshtastic.ble_interface.BLEInterface.scan", return_value=[first, second]
):
with pytest.raises(BLEInterface.BLEError) as excinfo:
iface.find_device("dup")
assert excinfo.value.kind == BLEInterface.BLEError.MULTIPLE_DEVICES
@pytest.mark.unit
def test_ble_send_to_radio_wraps_write_errors_with_kind():
"""_sendToRadioImpl wraps write failures with WRITE_ERROR."""
iface = object.__new__(BLEInterface)
iface.client = MagicMock()
iface.client.write_gatt_char.side_effect = RuntimeError("boom")
to_radio = MagicMock()
to_radio.SerializeToString.return_value = b"\x01"
with pytest.raises(BLEInterface.BLEError) as excinfo:
iface._sendToRadioImpl(to_radio)
assert excinfo.value.kind == BLEInterface.BLEError.WRITE_ERROR
@pytest.mark.unit
def test_ble_receive_wraps_unexpected_bleak_error_with_kind():
"""_receiveFromRadioImpl wraps unexpected BleakError with READ_ERROR."""
iface = object.__new__(BLEInterface)
iface.should_read = True
iface._want_receive = True
iface.client = MagicMock()
iface.client.read_gatt_char.side_effect = BleakError("some other BLE failure")
with pytest.raises(BLEInterface.BLEError) as excinfo:
iface._receiveFromRadioImpl()
assert excinfo.value.kind == BLEInterface.BLEError.READ_ERROR

View File

@@ -0,0 +1,646 @@
"""Tests for bin/inject_nanopb_options.py — the nanopb options injection script
and the generated protobuf descriptors it produces.
Part 1 (test_parse_*, test_inject_*): unit-tests the script's logic directly,
using small synthetic proto snippets.
Part 2 (test_descriptor_*): smoke-tests the already-generated _pb2.py files to
confirm the regen pipeline embedded the expected nanopb options.
"""
import importlib.util
import sys
import textwrap
from pathlib import Path
from unittest.mock import patch
import pytest
from hypothesis import given, strategies as st
from meshtastic.protobuf import (
atak_pb2,
config_pb2,
mesh_pb2,
nanopb_pb2,
telemetry_pb2,
)
# ---------------------------------------------------------------------------
# Load bin/inject_nanopb_options.py as a module without adding it to the
# package. __main__ guard means no side-effects on import.
# ---------------------------------------------------------------------------
_SCRIPT_PATH = Path(__file__).parent.parent.parent / "bin" / "inject_nanopb_options.py"
def _load_inject_module():
spec = importlib.util.spec_from_file_location("inject_nanopb_options", _SCRIPT_PATH)
mod = importlib.util.module_from_spec(spec)
with patch.object(sys, "argv", ["inject_nanopb_options.py"]):
spec.loader.exec_module(mod)
return mod
_inj = _load_inject_module()
parse_value = _inj.parse_value
parse_options_file = _inj.parse_options_file
format_nanopb_opts = _inj.format_nanopb_opts
inject_into_proto = _inj.inject_into_proto
message_path_matches = _inj.message_path_matches
# Convenience: the nanopb import path the script uses after the sed fixup
NANOPB_IMPORT = 'import "meshtastic/protobuf/nanopb.proto";'
# ===========================================================================
# Part 1 — Script unit tests
# ===========================================================================
# ---------------------------------------------------------------------------
# parse_value
# ---------------------------------------------------------------------------
@pytest.mark.unit
def test_parse_value_integer():
"""parse_value converts a decimal string to int."""
assert parse_value("40") == 40
@pytest.mark.unit
def test_parse_value_negative_integer():
"""parse_value handles negative integer strings."""
assert parse_value("-1") == -1
@pytest.mark.unit
def test_parse_value_true():
"""parse_value converts 'true' to Python True."""
assert parse_value("true") is True
@pytest.mark.unit
def test_parse_value_false():
"""parse_value converts 'false' to Python False."""
assert parse_value("false") is False
@pytest.mark.unit
def test_parse_value_string():
"""parse_value returns non-numeric, non-boolean strings as-is."""
assert parse_value("IS_8") == "IS_8"
@pytest.mark.unit
@given(st.integers())
def test_parse_value_any_integer_returns_int(n):
"""parse_value always returns int for any decimal integer string."""
assert parse_value(str(n)) == n
@pytest.mark.unit
@given(st.text())
def test_parse_value_never_crashes(s):
"""parse_value never raises on arbitrary input."""
result = parse_value(s)
assert isinstance(result, (int, bool, str))
@pytest.mark.unit
@given(st.text().filter(lambda s: not s.lstrip("-").isdigit() and s.lower() not in ("true", "false")))
def test_parse_value_non_numeric_non_bool_returns_str(s):
"""parse_value returns the original string when it is neither an integer nor a boolean."""
assert parse_value(s) == s
# ---------------------------------------------------------------------------
# parse_options_file
# ---------------------------------------------------------------------------
def _write_options(tmp_path: Path, content: str) -> Path:
p = tmp_path / "test.options"
p.write_text(textwrap.dedent(content))
return p
@pytest.mark.unit
def test_parse_wildcard(tmp_path):
"""Wildcard pattern (no dot) lands in the wildcard dict."""
f = _write_options(tmp_path, "*macaddr max_size:6 fixed_length:true\n")
specific, wildcard = parse_options_file(f)
assert "macaddr" in wildcard
assert wildcard["macaddr"] == {"max_size": 6, "fixed_length": True}
assert specific == {}
@pytest.mark.unit
def test_parse_specific(tmp_path):
"""Single-dot pattern lands in the specific dict with a 2-tuple key."""
f = _write_options(tmp_path, "*User.long_name max_size:40\n")
specific, wildcard = parse_options_file(f)
assert ("User", "long_name") in specific
assert specific[("User", "long_name")] == {"max_size": 40}
assert wildcard == {}
@pytest.mark.unit
def test_parse_multilevel(tmp_path):
"""Three-part pattern (Route.Link.uid) produces a 3-tuple key."""
f = _write_options(tmp_path, "*Route.Link.uid max_size:48\n")
specific, _ = parse_options_file(f)
assert ("Route", "Link", "uid") in specific
assert specific[("Route", "Link", "uid")] == {"max_size": 48}
@pytest.mark.unit
def test_parse_strips_inline_comments(tmp_path):
"""Text after # is ignored."""
f = _write_options(tmp_path, "*id max_size:16 # node id strings\n")
_, wildcard = parse_options_file(f)
assert wildcard["id"] == {"max_size": 16}
@pytest.mark.unit
def test_parse_skips_comment_only_lines(tmp_path):
"""Lines that are entirely comments produce no entries."""
f = _write_options(tmp_path, "# this is a comment\n*id max_size:16\n")
_, wildcard = parse_options_file(f)
assert list(wildcard.keys()) == ["id"]
@pytest.mark.unit
def test_parse_skips_blank_lines(tmp_path):
"""Blank lines are silently ignored."""
f = _write_options(tmp_path, "\n\n*id max_size:16\n\n")
_, wildcard = parse_options_file(f)
assert "id" in wildcard
@pytest.mark.unit
def test_parse_skips_non_python_options(tmp_path):
"""Options not in FIELD_OPTIONS (e.g. anonymous_oneof) are dropped."""
f = _write_options(tmp_path, "*MeshPacket.payload_variant anonymous_oneof:true\n")
specific, wildcard = parse_options_file(f)
# anonymous_oneof is not in FIELD_OPTIONS → no entry should be produced
assert specific == {}
assert wildcard == {}
@pytest.mark.unit
def test_parse_merges_repeated_patterns(tmp_path):
"""Two lines for the same pattern are merged."""
f = _write_options(
tmp_path,
"*SecurityConfig.admin_key max_size:32\n"
"*SecurityConfig.admin_key max_count:3\n",
)
specific, _ = parse_options_file(f)
assert specific[("SecurityConfig", "admin_key")] == {"max_size": 32, "max_count": 3}
@pytest.mark.unit
def test_parse_int_and_bool_values(tmp_path):
"""int_size parses as int; fixed_length parses as bool."""
f = _write_options(tmp_path, "*Data.payload max_size:233 fixed_length:false\n")
specific, _ = parse_options_file(f)
opts = specific[("Data", "payload")]
assert opts["max_size"] == 233
assert opts["fixed_length"] is False
# ---------------------------------------------------------------------------
# message_path_matches
# ---------------------------------------------------------------------------
@pytest.mark.unit
def test_message_path_matches_simple():
"""A single-element path matches the current message on the stack."""
stack = [("message", "User")]
assert message_path_matches(stack, ("User",))
@pytest.mark.unit
def test_message_path_matches_nested():
"""Both a 1-element and 2-element path match correctly against a nested stack."""
stack = [("message", "Config"), ("message", "DeviceConfig")]
assert message_path_matches(stack, ("DeviceConfig",))
assert message_path_matches(stack, ("Config", "DeviceConfig"))
@pytest.mark.unit
def test_message_path_matches_with_oneof_in_stack():
"""oneof frames in the stack are skipped when looking for messages."""
stack = [("message", "MeshPacket"), ("oneof", "payload_variant")]
assert message_path_matches(stack, ("MeshPacket",))
@pytest.mark.unit
def test_message_path_no_match():
"""A path with the wrong message name does not match."""
stack = [("message", "User")]
assert not message_path_matches(stack, ("Route",))
@pytest.mark.unit
def test_message_path_multilevel_partial_match():
"""A 2-element path must match the last 2 message names on the stack."""
stack = [("message", "Route"), ("message", "Link")]
assert message_path_matches(stack, ("Route", "Link"))
assert not message_path_matches(stack, ("Other", "Link"))
# ---------------------------------------------------------------------------
# format_nanopb_opts
# ---------------------------------------------------------------------------
@pytest.mark.unit
def test_format_max_size():
"""max_size is rendered as an integer literal."""
assert format_nanopb_opts({"max_size": 40}) == "(nanopb).max_size = 40"
@pytest.mark.unit
def test_format_int_size_as_enum():
"""int_size numeric values are rendered as IS_8/IS_16/IS_32/IS_64 enum names."""
assert format_nanopb_opts({"int_size": 8}) == "(nanopb).int_size = IS_8"
assert format_nanopb_opts({"int_size": 16}) == "(nanopb).int_size = IS_16"
assert format_nanopb_opts({"int_size": 32}) == "(nanopb).int_size = IS_32"
assert format_nanopb_opts({"int_size": 64}) == "(nanopb).int_size = IS_64"
@pytest.mark.unit
def test_format_bool_true():
"""True is rendered as the proto literal 'true'."""
assert format_nanopb_opts({"fixed_length": True}) == "(nanopb).fixed_length = true"
@pytest.mark.unit
def test_format_bool_false():
"""False is rendered as the proto literal 'false'."""
assert format_nanopb_opts({"fixed_length": False}) == "(nanopb).fixed_length = false"
# ---------------------------------------------------------------------------
# inject_into_proto — helpers
# ---------------------------------------------------------------------------
_NANOPB_IMPORT_PATH = "meshtastic/protobuf/nanopb.proto"
def _inject(proto_src: str, specific=None, wildcard=None) -> str:
"""Run inject_into_proto with empty dicts as defaults."""
return inject_into_proto(
textwrap.dedent(proto_src),
specific or {},
wildcard or {},
_NANOPB_IMPORT_PATH,
)
# ---------------------------------------------------------------------------
# inject_into_proto — option injection
# ---------------------------------------------------------------------------
@pytest.mark.unit
def test_inject_adds_option_to_plain_field():
"""A field with no existing options gets a nanopb annotation."""
proto = """\
syntax = "proto3";
import "meshtastic/protobuf/channel.proto";
message User {
string long_name = 1;
}
"""
result = _inject(proto, specific={("User", "long_name"): {"max_size": 40}})
assert "long_name = 1 [(nanopb).max_size = 40];" in result
@pytest.mark.unit
def test_inject_merges_with_existing_options():
"""nanopb annotation is appended after existing field options."""
proto = """\
syntax = "proto3";
import "meshtastic/protobuf/channel.proto";
message User {
bytes macaddr = 4 [deprecated = true];
}
"""
result = _inject(proto, wildcard={"macaddr": {"max_size": 6}})
assert "[deprecated = true, (nanopb).max_size = 6];" in result
@pytest.mark.unit
def test_inject_int_size_uses_enum_name():
"""int_size values are written as IS_N enum names, not raw integers."""
proto = """\
syntax = "proto3";
import "meshtastic/protobuf/mesh.proto";
message Foo {
uint32 hop_limit = 9;
}
"""
result = _inject(proto, specific={("Foo", "hop_limit"): {"int_size": 8}})
assert "(nanopb).int_size = IS_8" in result
@pytest.mark.unit
def test_inject_wildcard_applied_across_messages():
"""A wildcard option hits the matching field in every message."""
proto = """\
syntax = "proto3";
import "meshtastic/protobuf/mesh.proto";
message A {
bytes macaddr = 1;
}
message B {
bytes macaddr = 2;
}
"""
result = _inject(proto, wildcard={"macaddr": {"max_size": 6}})
assert result.count("(nanopb).max_size = 6") == 2
@pytest.mark.unit
def test_inject_specific_not_leaking_to_other_messages():
"""A message-specific option does NOT apply to a different message's field."""
proto = """\
syntax = "proto3";
import "meshtastic/protobuf/mesh.proto";
message User {
string id = 1;
}
message Other {
string id = 1;
}
"""
result = _inject(proto, specific={("User", "id"): {"max_size": 16}})
# Easier: count annotations — should be exactly one
assert result.count("(nanopb).max_size = 16") == 1
@pytest.mark.unit
def test_inject_nested_message():
"""A 2-part specific key only hits the field in the correct nested message."""
proto = """\
syntax = "proto3";
import "meshtastic/protobuf/mesh.proto";
message Route {
message Link {
string uid = 1;
}
string uid = 2;
}
"""
# Route.Link.uid → key = ('Route', 'Link', 'uid')
result = _inject(proto, specific={("Route", "Link", "uid"): {"max_size": 48}})
lines = result.splitlines()
# Only the uid inside Link should have the annotation
assert result.count("(nanopb).max_size = 48") == 1
# Confirm it's the inner one (it has 4 spaces more indent than outer uid)
annotated = next(l for l in lines if "(nanopb).max_size = 48" in l)
plain = next(l for l in lines if "uid = 2" in l)
assert annotated.index("uid") > plain.index("uid")
@pytest.mark.unit
def test_inject_skips_enum_body_values():
"""Enum value lines must not be treated as field declarations."""
proto = """\
syntax = "proto3";
message Foo {
enum Role {
CLIENT = 0;
ROUTER = 2;
}
Role role = 1;
}
"""
# Wildcard for 'role' should only hit the field, not enum values
result = _inject(proto, wildcard={"role": {"max_size": 8}})
assert result.count("(nanopb)") == 1
assert "(nanopb)" not in next(l for l in result.splitlines() if "CLIENT" in l)
@pytest.mark.unit
def test_inject_optional_qualifier_preserved():
"""The 'optional' qualifier is kept when a field gets an annotation."""
proto = """\
syntax = "proto3";
import "meshtastic/protobuf/mesh.proto";
message Foo {
optional uint32 altitude = 3;
}
"""
result = _inject(proto, specific={("Foo", "altitude"): {"int_size": 16}})
assert "optional uint32 altitude = 3 [(nanopb).int_size = IS_16];" in result
@pytest.mark.unit
def test_inject_repeated_qualifier_preserved():
"""The 'repeated' qualifier is kept when a field gets an annotation."""
proto = """\
syntax = "proto3";
import "meshtastic/protobuf/mesh.proto";
message Foo {
repeated int32 snr = 2;
}
"""
result = _inject(proto, specific={("Foo", "snr"): {"max_count": 8}})
assert "repeated int32 snr = 2 [(nanopb).max_count = 8];" in result
@pytest.mark.unit
def test_inject_multiple_options_on_one_field():
"""Multiple options from the same pattern are all injected on one field."""
proto = """\
syntax = "proto3";
import "meshtastic/protobuf/mesh.proto";
message Foo {
repeated int32 snr = 1;
}
"""
result = _inject(proto, specific={("Foo", "snr"): {"max_count": 8, "int_size": 8}})
assert "(nanopb).max_count = 8" in result
assert "(nanopb).int_size = IS_8" in result
# ---------------------------------------------------------------------------
# inject_into_proto — import insertion
# ---------------------------------------------------------------------------
@pytest.mark.unit
def test_inject_adds_nanopb_import_when_absent():
"""nanopb.proto import is added when the file has other imports."""
proto = """\
syntax = "proto3";
import "meshtastic/protobuf/mesh.proto";
message Foo {
string name = 1;
}
"""
result = _inject(proto, wildcard={"name": {"max_size": 30}})
assert NANOPB_IMPORT in result
@pytest.mark.unit
def test_inject_no_duplicate_nanopb_import():
"""nanopb.proto import is NOT added a second time if already present."""
proto = """\
syntax = "proto3";
import "meshtastic/protobuf/mesh.proto";
import "meshtastic/protobuf/nanopb.proto";
message Foo {
string name = 1;
}
"""
result = _inject(proto, wildcard={"name": {"max_size": 30}})
assert result.count(NANOPB_IMPORT) == 1
@pytest.mark.unit
def test_inject_import_placed_after_existing_imports():
"""nanopb import appears after the last existing import, not at the top."""
proto = """\
syntax = "proto3";
import "meshtastic/protobuf/mesh.proto";
message Foo {
string name = 1;
}
"""
result = _inject(proto, wildcard={"name": {"max_size": 30}})
lines = result.splitlines()
mesh_idx = next(i for i, l in enumerate(lines) if "mesh.proto" in l)
nanopb_idx = next(i for i, l in enumerate(lines) if "nanopb.proto" in l)
assert nanopb_idx == mesh_idx + 1
@pytest.mark.unit
def test_inject_import_after_syntax_when_no_existing_imports():
"""When a proto has no imports, nanopb import goes AFTER the syntax line,
not before it (regression test for the last_import_idx == -1 bug)."""
proto = """\
syntax = "proto3";
message XModem {
uint32 seq = 2;
}
"""
result = _inject(proto, specific={("XModem", "seq"): {"int_size": 16}})
lines = result.splitlines()
syntax_idx = next(i for i, l in enumerate(lines) if l.strip().startswith("syntax"))
nanopb_idx = next(i for i, l in enumerate(lines) if "nanopb.proto" in l)
assert nanopb_idx > syntax_idx, "nanopb import must come after the syntax line"
# syntax line must still be first non-blank line
first_non_blank = next(l.strip() for l in lines if l.strip())
assert first_non_blank.startswith("syntax")
@pytest.mark.unit
def test_inject_noop_when_no_options():
"""Proto file is returned unchanged when there are no options to inject."""
proto = 'syntax = "proto3";\nmessage Foo { string x = 1; }\n'
result = _inject(proto)
assert result == proto
# ===========================================================================
# Part 2 — Descriptor integration tests
# Verify that regen-protobufs.sh produced _pb2.py files with nanopb options
# embedded in the serialized descriptors.
# ===========================================================================
def _field_opts(descriptor, *path):
"""Walk a descriptor by field/nested-type path and return its nanopb opts.
Elements of *path that are message names are looked up in nested_types_by_name;
the final element is looked up in fields_by_name.
"""
desc = descriptor
for step in path[:-1]:
desc = desc.nested_types_by_name[step]
field = desc.fields_by_name[path[-1]]
return field.GetOptions().Extensions[nanopb_pb2.nanopb]
@pytest.mark.unit
def test_descriptor_user_long_name():
"""User.long_name has max_size = 40 from mesh.options."""
opts = _field_opts(mesh_pb2.DESCRIPTOR.message_types_by_name["User"], "long_name")
assert opts.max_size == 40
@pytest.mark.unit
def test_descriptor_user_short_name():
"""User.short_name has max_size = 5 from mesh.options."""
opts = _field_opts(mesh_pb2.DESCRIPTOR.message_types_by_name["User"], "short_name")
assert opts.max_size == 5
@pytest.mark.unit
def test_descriptor_wildcard_macaddr():
"""Wildcard option from mesh.options applied to User.macaddr."""
opts = _field_opts(mesh_pb2.DESCRIPTOR.message_types_by_name["User"], "macaddr")
assert opts.max_size == 6
assert opts.fixed_length is True
@pytest.mark.unit
def test_descriptor_meshpacket_hop_limit():
"""MeshPacket.hop_limit has int_size = IS_8 from mesh.options."""
opts = _field_opts(mesh_pb2.DESCRIPTOR.message_types_by_name["MeshPacket"], "hop_limit")
assert opts.int_size == nanopb_pb2.IS_8
@pytest.mark.unit
def test_descriptor_routediscovery_snr_towards():
"""RouteDiscovery.snr_towards has max_count = 8 and int_size = IS_8 from mesh.options."""
opts = _field_opts(
mesh_pb2.DESCRIPTOR.message_types_by_name["RouteDiscovery"], "snr_towards"
)
assert opts.max_count == 8
assert opts.int_size == nanopb_pb2.IS_8
@pytest.mark.unit
def test_descriptor_data_payload():
"""Data.payload has max_size = 233 from mesh.options."""
opts = _field_opts(mesh_pb2.DESCRIPTOR.message_types_by_name["Data"], "payload")
assert opts.max_size == 233
@pytest.mark.unit
def test_descriptor_nested_deviceconfig_tzdef():
"""Config.DeviceConfig.tzdef — option on a field inside a nested message."""
config = config_pb2.DESCRIPTOR.message_types_by_name["Config"]
opts = _field_opts(config, "DeviceConfig", "tzdef")
assert opts.max_size == 65
@pytest.mark.unit
def test_descriptor_nested_securityconfig_admin_key():
"""Config.SecurityConfig.admin_key — two options merged from two .options lines."""
config = config_pb2.DESCRIPTOR.message_types_by_name["Config"]
opts = _field_opts(config, "SecurityConfig", "admin_key")
assert opts.max_size == 32
assert opts.max_count == 3
@pytest.mark.unit
def test_descriptor_multilevel_nested_route_link_uid():
"""Route.Link.uid — three-level nested pattern from atak.options."""
route = atak_pb2.DESCRIPTOR.message_types_by_name["Route"]
opts = _field_opts(route, "Link", "uid")
assert opts.max_size == 48
@pytest.mark.unit
def test_descriptor_telemetry_environment_one_wire_temperature():
"""EnvironmentMetrics.one_wire_temperature has max_count = 8 from telemetry.options."""
env = telemetry_pb2.DESCRIPTOR.message_types_by_name["EnvironmentMetrics"]
opts = _field_opts(env, "one_wire_temperature")
assert opts.max_count == 8

View File

@@ -6,9 +6,12 @@ import os
import platform
import re
import sys
import tempfile
from types import SimpleNamespace
from unittest.mock import mock_open, MagicMock, patch
import pytest
import meshtastic.__main__ as mt_main
from meshtastic.__main__ import (
export_config,
@@ -18,12 +21,14 @@ from meshtastic.__main__ import (
onNode,
onReceive,
tunnelMain,
set_missing_flags_false,
)
from meshtastic import mt_config
from ..protobuf.channel_pb2 import Channel # pylint: disable=E0611
# from ..ble_interface import BLEInterface
from ..mesh_interface import MeshInterface
from ..node import Node
# from ..radioconfig_pb2 import UserPreferences
@@ -34,7 +39,6 @@ from ..tcp_interface import TCPInterface
# from ..remote_hardware import onGPIOreceive
# from ..config_pb2 import Config
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_main_init_parser_no_args(capsys):
@@ -259,6 +263,113 @@ def test_main_info_with_permission_error(patched_getlogin, capsys, caplog):
assert err == ""
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_main_ble_device_not_found_message(capsys):
"""Test BLE device-not-found help text."""
sys.argv = ["", "--info", "--ble", "any"]
mt_config.args = sys.argv
with patch("meshtastic.__main__.BLEInterface.__init__") as mock_ble_init:
mock_ble_init.side_effect = mt_main.BLEInterface.BLEError(
"missing",
mt_main.BLEInterface.BLEError.DEVICE_NOT_FOUND,
)
with pytest.raises(SystemExit) as excinfo:
main()
out, err = capsys.readouterr()
assert excinfo.value.code == 1
assert re.search(r"BLE device not found", out, re.MULTILINE)
assert re.search(r"--ble-scan", out, re.MULTILINE)
assert err == ""
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_main_ble_multiple_devices_message(capsys):
"""Test BLE multiple-devices help text."""
sys.argv = ["", "--info", "--ble", "any"]
mt_config.args = sys.argv
with patch("meshtastic.__main__.BLEInterface.__init__") as mock_ble_init:
mock_ble_init.side_effect = mt_main.BLEInterface.BLEError(
"multiple",
mt_main.BLEInterface.BLEError.MULTIPLE_DEVICES,
)
with pytest.raises(SystemExit) as excinfo:
main()
out, err = capsys.readouterr()
assert excinfo.value.code == 1
assert re.search(r"Multiple Meshtastic BLE devices found", out, re.MULTILINE)
assert re.search(r"meshtastic --ble <name_or_address>", out, re.MULTILINE)
assert err == ""
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_main_ble_write_error_message(capsys):
"""Test BLE write-error help text."""
sys.argv = ["", "--info", "--ble", "any"]
mt_config.args = sys.argv
with patch("meshtastic.__main__.BLEInterface.__init__") as mock_ble_init:
mock_ble_init.side_effect = mt_main.BLEInterface.BLEError(
"write fail",
mt_main.BLEInterface.BLEError.WRITE_ERROR,
)
with pytest.raises(SystemExit) as excinfo:
main()
out, err = capsys.readouterr()
assert excinfo.value.code == 1
assert re.search(r"Failed to write to BLE device", out, re.MULTILINE)
assert re.search(r"user not in 'bluetooth' group", out, re.MULTILINE)
assert err == ""
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_main_ble_read_error_message(capsys):
"""Test BLE read-error help text."""
sys.argv = ["", "--info", "--ble", "any"]
mt_config.args = sys.argv
with patch("meshtastic.__main__.BLEInterface.__init__") as mock_ble_init:
mock_ble_init.side_effect = mt_main.BLEInterface.BLEError(
"read fail",
mt_main.BLEInterface.BLEError.READ_ERROR,
)
with pytest.raises(SystemExit) as excinfo:
main()
out, err = capsys.readouterr()
assert excinfo.value.code == 1
assert re.search(r"Failed to read from BLE device", out, re.MULTILINE)
assert re.search(r"Move closer to the device", out, re.MULTILINE)
assert err == ""
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_main_serial_timeout_message(capsys):
"""Test serial timeout help text."""
sys.argv = ["", "--info"]
mt_config.args = sys.argv
with patch("meshtastic.serial_interface.SerialInterface") as mock_serial:
mock_serial.side_effect = MeshInterface.MeshInterfaceError("Timed out waiting")
with pytest.raises(SystemExit) as excinfo:
main()
out, err = capsys.readouterr()
assert excinfo.value.code == 1
assert re.search(r"Connection timed out", out, re.MULTILINE)
assert re.search(r"Device is rebooting", out, re.MULTILINE)
assert err == ""
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_main_info_with_tcp_interface(capsys):
@@ -408,8 +519,8 @@ def test_main_nodes(capsys):
iface = MagicMock(autospec=SerialInterface)
def mock_showNodes():
print("inside mocked showNodes")
def mock_showNodes(includeSelf, showFields):
print(f"inside mocked showNodes: {includeSelf} {showFields}")
iface.showNodes.side_effect = mock_showNodes
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo:
@@ -454,6 +565,37 @@ def test_main_set_owner_short_to_bob(capsys):
assert err == ""
mo.assert_called()
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_main_set_is_unmessageable_to_true(capsys):
"""Test --set-is-unmessageable true"""
sys.argv = ["", "--set-is-unmessageable", "true"]
mt_config.args = sys.argv
iface = MagicMock(autospec=SerialInterface)
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo:
main()
out, err = capsys.readouterr()
assert re.search(r"Connected to radio", out, re.MULTILINE)
assert re.search(r"Setting device owner is_unmessageable to True", out, re.MULTILINE)
assert err == ""
mo.assert_called()
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_main_set_is_unmessagable_to_true(capsys):
"""Test --set-is-unmessagable true"""
sys.argv = ["", "--set-is-unmessagable", "true"]
mt_config.args = sys.argv
iface = MagicMock(autospec=SerialInterface)
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo:
main()
out, err = capsys.readouterr()
assert re.search(r"Connected to radio", out, re.MULTILINE)
assert re.search(r"Setting device owner is_unmessageable to True", out, re.MULTILINE)
assert err == ""
mo.assert_called()
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
@@ -494,6 +636,44 @@ def test_main_get_canned_messages(capsys, caplog, iface_with_nodes):
assert err == ""
mo.assert_called()
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_main_set_ringtone(capsys):
"""Test --set-ringtone"""
sys.argv = ["", "--set-ringtone", "foo,bar"]
mt_config.args = sys.argv
iface = MagicMock(autospec=SerialInterface)
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo:
main()
out, err = capsys.readouterr()
assert re.search(r"Connected to radio", out, re.MULTILINE)
assert re.search(r"Setting ringtone to foo,bar", out, re.MULTILINE)
assert err == ""
mo.assert_called()
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_main_get_ringtone(capsys, caplog, iface_with_nodes):
"""Test --get-ringtone"""
sys.argv = ["", "--get-ringtone"]
mt_config.args = sys.argv
iface = iface_with_nodes
iface.devPath = "bar"
mocked_node = MagicMock(autospec=Node)
mocked_node.get_ringtone.return_value = "foo,bar"
iface.localNode = mocked_node
with caplog.at_level(logging.DEBUG):
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo:
main()
out, err = capsys.readouterr()
assert re.search(r"Connected to radio", out, re.MULTILINE)
assert re.search(r"ringtone:foo,bar", out, re.MULTILINE)
assert err == ""
mo.assert_called()
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
@@ -593,10 +773,10 @@ def test_main_sendtext(capsys):
iface = MagicMock(autospec=SerialInterface)
def mock_sendText(
text, dest, wantAck=False, wantResponse=False, onResponse=None, channelIndex=0
text, dest, wantAck=False, wantResponse=False, onResponse=None, channelIndex=0, portNum=0
):
print("inside mocked sendText")
print(f"{text} {dest} {wantAck} {wantResponse} {channelIndex}")
print(f"{text} {dest} {wantAck} {wantResponse} {channelIndex} {portNum}")
iface.sendText.side_effect = mock_sendText
@@ -620,10 +800,10 @@ def test_main_sendtext_with_channel(capsys):
iface = MagicMock(autospec=SerialInterface)
def mock_sendText(
text, dest, wantAck=False, wantResponse=False, onResponse=None, channelIndex=0
text, dest, wantAck=False, wantResponse=False, onResponse=None, channelIndex=0, portNum=0
):
print("inside mocked sendText")
print(f"{text} {dest} {wantAck} {wantResponse} {channelIndex}")
print(f"{text} {dest} {wantAck} {wantResponse} {channelIndex} {portNum}")
iface.sendText.side_effect = mock_sendText
@@ -688,12 +868,11 @@ def test_main_sendtext_with_invalid_channel_nine(caplog, capsys):
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
@patch("termios.tcsetattr")
@patch("termios.tcgetattr")
@patch("meshtastic.serial_interface.SerialInterface._set_hupcl_with_termios")
@patch("builtins.open", new_callable=mock_open, read_data="data")
@patch("serial.Serial")
@patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"])
def test_main_sendtext_with_dest(mock_findPorts, mock_serial, mocked_open, mock_get, mock_set, capsys, caplog, iface_with_nodes):
def test_main_sendtext_with_dest(mock_findPorts, mock_serial, mocked_open, mock_hupcl, capsys, caplog, iface_with_nodes):
"""Test --sendtext with --dest"""
sys.argv = ["", "--sendtext", "hello", "--dest", "foo"]
mt_config.args = sys.argv
@@ -887,12 +1066,11 @@ def test_main_seturl(capsys):
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
@patch("termios.tcsetattr")
@patch("termios.tcgetattr")
@patch("meshtastic.serial_interface.SerialInterface._set_hupcl_with_termios")
@patch("builtins.open", new_callable=mock_open, read_data="data")
@patch("serial.Serial")
@patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"])
def test_main_set_valid(mocked_findports, mocked_serial, mocked_open, mocked_get, mocked_set, capsys):
def test_main_set_valid(mocked_findports, mocked_serial, mocked_open, mocked_hupcl, capsys):
"""Test --set with valid field"""
sys.argv = ["", "--set", "network.wifi_ssid", "foo"]
mt_config.args = sys.argv
@@ -912,12 +1090,11 @@ def test_main_set_valid(mocked_findports, mocked_serial, mocked_open, mocked_get
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
@patch("termios.tcsetattr")
@patch("termios.tcgetattr")
@patch("meshtastic.serial_interface.SerialInterface._set_hupcl_with_termios")
@patch("builtins.open", new_callable=mock_open, read_data="data")
@patch("serial.Serial")
@patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"])
def test_main_set_valid_wifi_psk(mocked_findports, mocked_serial, mocked_open, mocked_get, mocked_set, capsys):
def test_main_set_valid_wifi_psk(mocked_findports, mocked_serial, mocked_open, mocked_hupcl, capsys):
"""Test --set with valid field"""
sys.argv = ["", "--set", "network.wifi_psk", "123456789"]
mt_config.args = sys.argv
@@ -937,12 +1114,11 @@ def test_main_set_valid_wifi_psk(mocked_findports, mocked_serial, mocked_open, m
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
@patch("termios.tcsetattr")
@patch("termios.tcgetattr")
@patch("meshtastic.serial_interface.SerialInterface._set_hupcl_with_termios")
@patch("builtins.open", new_callable=mock_open, read_data="data")
@patch("serial.Serial")
@patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"])
def test_main_set_invalid_wifi_psk(mocked_findports, mocked_serial, mocked_open, mocked_get, mocked_set, capsys):
def test_main_set_invalid_wifi_psk(mocked_findports, mocked_serial, mocked_open, mocked_hupcl, capsys):
"""Test --set with an invalid value (psk must be 8 or more characters)"""
sys.argv = ["", "--set", "network.wifi_psk", "1234567"]
mt_config.args = sys.argv
@@ -965,12 +1141,11 @@ def test_main_set_invalid_wifi_psk(mocked_findports, mocked_serial, mocked_open,
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
@patch("termios.tcsetattr")
@patch("termios.tcgetattr")
@patch("meshtastic.serial_interface.SerialInterface._set_hupcl_with_termios")
@patch("builtins.open", new_callable=mock_open, read_data="data")
@patch("serial.Serial")
@patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"])
def test_main_set_valid_camel_case(mocked_findports, mocked_serial, mocked_open, mocked_get, mocked_set, capsys):
def test_main_set_valid_camel_case(mocked_findports, mocked_serial, mocked_open, mocked_hupcl, capsys):
"""Test --set with valid field"""
sys.argv = ["", "--set", "network.wifi_ssid", "foo"]
mt_config.args = sys.argv
@@ -991,12 +1166,11 @@ def test_main_set_valid_camel_case(mocked_findports, mocked_serial, mocked_open,
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
@patch("termios.tcsetattr")
@patch("termios.tcgetattr")
@patch("meshtastic.serial_interface.SerialInterface._set_hupcl_with_termios")
@patch("builtins.open", new_callable=mock_open, read_data="data")
@patch("serial.Serial")
@patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"])
def test_main_set_with_invalid(mocked_findports, mocked_serial, mocked_open, mocked_get, mocked_set, capsys):
def test_main_set_with_invalid(mocked_findports, mocked_serial, mocked_open, mocked_hupcl, capsys):
"""Test --set with invalid field"""
sys.argv = ["", "--set", "foo", "foo"]
mt_config.args = sys.argv
@@ -1017,12 +1191,11 @@ def test_main_set_with_invalid(mocked_findports, mocked_serial, mocked_open, moc
# TODO: write some negative --configure tests
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
@patch("termios.tcsetattr")
@patch("termios.tcgetattr")
@patch("meshtastic.serial_interface.SerialInterface._set_hupcl_with_termios")
@patch("builtins.open", new_callable=mock_open, read_data="data")
@patch("serial.Serial")
@patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"])
def test_main_configure_with_snake_case(mocked_findports, mocked_serial, mocked_open, mocked_get, mocked_set, capsys):
def test_main_configure_with_snake_case(mocked_findports, mocked_serial, mocked_open, mocked_hupcl, capsys):
"""Test --configure with valid file"""
sys.argv = ["", "--configure", "example_config.yaml"]
mt_config.args = sys.argv
@@ -1050,12 +1223,11 @@ def test_main_configure_with_snake_case(mocked_findports, mocked_serial, mocked_
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
@patch("termios.tcsetattr")
@patch("termios.tcgetattr")
@patch("meshtastic.serial_interface.SerialInterface._set_hupcl_with_termios")
@patch("builtins.open", new_callable=mock_open, read_data="data")
@patch("serial.Serial")
@patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"])
def test_main_configure_with_camel_case_keys(mocked_findports, mocked_serial, mocked_open, mocked_get, mocked_set, capsys):
def test_main_configure_with_camel_case_keys(mocked_findports, mocked_serial, mocked_open, mocked_hupcl, capsys):
"""Test --configure with valid file"""
sys.argv = ["", "--configure", "exampleConfig.yaml"]
mt_config.args = sys.argv
@@ -1608,7 +1780,7 @@ def test_main_onReceive_empty(caplog, capsys):
assert re.search(r"in onReceive", caplog.text, re.MULTILINE)
out, err = capsys.readouterr()
assert re.search(
r"Warning: There is no field 'to' in the packet.", out, re.MULTILINE
r"Warning: Error processing received packet: 'to'.", out, re.MULTILINE
)
assert err == ""
@@ -1660,11 +1832,9 @@ def test_main_onReceive_with_sendtext(caplog, capsys):
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_main_onReceive_with_text(caplog, capsys):
"""Test onReceive with text"""
args = MagicMock()
args.sendtext.return_value = "foo"
mt_config.args = args
def test_main_onReceive_with_text_replies_on_target_channel(caplog, capsys):
"""Test onReceive replies when channel matches --ch-index (default 0)."""
mt_config.args = SimpleNamespace(reply=True, ch_index=0, sendtext=None)
# Note: 'TEXT_MESSAGE_APP' value is 1
# Note: Some of this is faked below.
@@ -1690,6 +1860,83 @@ def test_main_onReceive_with_text(caplog, capsys):
assert re.search(r"in onReceive", caplog.text, re.MULTILINE)
out, err = capsys.readouterr()
assert re.search(r"Sending reply", out, re.MULTILINE)
iface.sendText.assert_called_once_with(
"got msg 'faked' with rxSnr: 6.0 and hopLimit: 3", channelIndex=0
)
assert err == ""
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_main_onReceive_with_text_ignores_non_target_channel(caplog, capsys):
"""Test onReceive does not reply when packet channel differs from --ch-index."""
mt_config.args = SimpleNamespace(reply=True, ch_index=1, sendtext=None)
packet = {
"to": 4294967295,
"decoded": {"portnum": 1, "payload": "hello", "text": "faked"},
"id": 334776977,
"hop_limit": 3,
"want_ack": True,
"rxSnr": 6.0,
"hopLimit": 3,
"raw": "faked",
"fromId": "!28b5465c",
"toId": "^all",
"channel": 0,
}
iface = MagicMock(autospec=SerialInterface)
iface.myInfo.my_node_num = 4294967295
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface):
with caplog.at_level(logging.DEBUG):
onReceive(packet, iface)
assert re.search(r"in onReceive", caplog.text, re.MULTILINE)
out, err = capsys.readouterr()
assert re.search(
r"Ignored message on channel 0 \(waiting for channel 1\)", out, re.MULTILINE
)
iface.sendText.assert_not_called()
assert err == ""
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_main_onReceive_with_text_replies_on_explicit_matching_channel(caplog, capsys):
"""Test onReceive replies when explicit packet channel matches --ch-index."""
mt_config.args = SimpleNamespace(reply=True, ch_index=2, sendtext=None)
packet = {
"to": 4294967295,
"decoded": {"portnum": 1, "payload": "hello", "text": "faked"},
"id": 334776977,
"hop_limit": 3,
"want_ack": True,
"rxSnr": 6.0,
"hopLimit": 3,
"raw": "faked",
"fromId": "!28b5465c",
"toId": "^all",
"channel": 2,
}
iface = MagicMock(autospec=SerialInterface)
iface.myInfo.my_node_num = 4294967295
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface):
with caplog.at_level(logging.DEBUG):
onReceive(packet, iface)
assert re.search(r"in onReceive", caplog.text, re.MULTILINE)
out, err = capsys.readouterr()
assert re.search(
r"Received channel 2\. Sending reply: got msg 'faked' with rxSnr: 6.0 and hopLimit: 3",
out,
re.MULTILINE,
)
iface.sendText.assert_called_once_with(
"got msg 'faked' with rxSnr: 6.0 and hopLimit: 3", channelIndex=2
)
assert err == ""
@@ -1724,6 +1971,8 @@ def test_main_export_config(capsys):
mo.getLongName.return_value = "foo"
mo.getShortName.return_value = "oof"
mo.localNode.getURL.return_value = "bar"
mo.getCannedMessage.return_value = "foo|bar"
mo.getRingtone.return_value = "24:d=32,o=5"
mo.getMyNodeInfo().get.return_value = {
"latitudeI": 1100000000,
"longitudeI": 1200000000,
@@ -1738,7 +1987,8 @@ position_broadcast_smart: true
fixed_position: true
position_flags: 35"""
export_config(mo)
out, err = capsys.readouterr()
out = export_config(mo)
err = ""
# ensure we do not output this line
assert not re.search(r"Connected to radio", out, re.MULTILINE)
@@ -1825,6 +2075,41 @@ position_flags: 35"""
# mo.assert_called()
@pytest.mark.unit
def test_set_missing_flags_false():
"""Test set_missing_flags_false() function"""
config = {
"bluetooth": {
"enabled": True
},
"lora": {
"txEnabled": True
}
}
false_defaults = {
("bluetooth", "enabled"),
("lora", "sx126xRxBoostedGain"),
("lora", "txEnabled"),
("lora", "usePreset"),
("position", "positionBroadcastSmartEnabled"),
("security", "serialEnabled"),
("mqtt", "encryptionEnabled"),
}
set_missing_flags_false(config, false_defaults)
# Preserved
assert config["bluetooth"]["enabled"] is True
assert config["lora"]["txEnabled"] is True
# Added
assert config["lora"]["usePreset"] is False
assert config["lora"]["sx126xRxBoostedGain"] is False
assert config["position"]["positionBroadcastSmartEnabled"] is False
assert config["security"]["serialEnabled"] is False
assert config["mqtt"]["encryptionEnabled"] is False
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_main_gpio_rd_no_gpio_channel(capsys):
@@ -2614,16 +2899,16 @@ def test_tunnel_subnet_arg_with_no_devices(mock_platform_system, caplog, capsys)
assert err == ""
@pytest.mark.skipif(sys.platform == "win32", reason="on windows is no fcntl module")
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
@patch("platform.system")
@patch("termios.tcsetattr")
@patch("termios.tcgetattr")
@patch("meshtastic.serial_interface.SerialInterface._set_hupcl_with_termios")
@patch("builtins.open", new_callable=mock_open, read_data="data")
@patch("serial.Serial")
@patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"])
def test_tunnel_tunnel_arg(
mocked_findPorts, mocked_serial, mocked_open, mock_get, mock_set, mock_platform_system, caplog, iface_with_nodes, capsys
mocked_findPorts, mocked_serial, mocked_open, mock_hupcl, mock_platform_system, caplog, iface_with_nodes, capsys
):
"""Test tunnel with tunnel arg (act like we are on a linux system)"""
@@ -2652,3 +2937,267 @@ def test_tunnel_tunnel_arg(
out, err = capsys.readouterr()
assert re.search(r"Connected to radio", out, re.MULTILINE)
assert err == ""
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_set_favorite_node():
"""Test --set-favorite-node node"""
sys.argv = ["", "--set-favorite-node", "!12345678"]
mt_config.args = sys.argv
mocked_node = MagicMock(autospec=Node)
iface = MagicMock(autospec=SerialInterface)
iface.getNode.return_value = mocked_node
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface):
main()
mocked_node.setFavorite.assert_called_once_with("!12345678")
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_remove_favorite_node():
"""Test --remove-favorite-node node"""
sys.argv = ["", "--remove-favorite-node", "!12345678"]
mt_config.args = sys.argv
mocked_node = MagicMock(autospec=Node)
iface = MagicMock(autospec=SerialInterface)
iface.getNode.return_value = mocked_node
mocked_node.iface = iface
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface):
main()
mocked_node.removeFavorite.assert_called_once_with("!12345678")
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_set_ignored_node():
"""Test --set-ignored-node node"""
sys.argv = ["", "--set-ignored-node", "!12345678"]
mt_config.args = sys.argv
mocked_node = MagicMock(autospec=Node)
iface = MagicMock(autospec=SerialInterface)
iface.getNode.return_value = mocked_node
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface):
main()
mocked_node.setIgnored.assert_called_once_with("!12345678")
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_remove_ignored_node():
"""Test --remove-ignored-node node"""
sys.argv = ["", "--remove-ignored-node", "!12345678"]
mt_config.args = sys.argv
mocked_node = MagicMock(autospec=Node)
iface = MagicMock(autospec=SerialInterface)
iface.getNode.return_value = mocked_node
mocked_node.iface = iface
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface):
main()
mocked_node.removeIgnored.assert_called_once_with("!12345678")
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_add_contact_url():
"""Test --add-contact with a shareable URL"""
url = "https://meshtastic.org/v/#CKqkvZgIElEKCSE4MzBmNTIyYRIQUm9hZHJ1bm5lciBSaWRnZRoEUktTTiIGAAAAAAAAKAk4AkIgRxo_Fw_ergQIhRqBbrHasLYy3gU-Ay8hrhu4OVnIPQc=" # pylint: disable=line-too-long
sys.argv = ["", "--add-contact", url]
mt_config.args = sys.argv
mocked_node = MagicMock(autospec=Node)
iface = MagicMock(autospec=SerialInterface)
iface.getNode.return_value = mocked_node
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface):
main()
mocked_node.addContactURL.assert_called_once_with(url)
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_contact_qr():
"""Test --contact-qr with a node ID"""
sys.argv = ["", "--contact-qr", "!830f522a"]
mt_config.args = sys.argv
mocked_node = MagicMock(autospec=Node)
iface = MagicMock(autospec=SerialInterface)
iface.getNode.return_value = mocked_node
mocked_node.iface = iface
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface):
main()
mocked_node.getContactURL.assert_called_once_with("!830f522a", should_ignore=False, manually_verified=False)
mocked_node.getContactURL.reset_mock()
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_contact_qr_with_flags():
"""Test --contact-qr with --contact-verified and --contact-ignore"""
sys.argv = ["", "--contact-qr", "!830f522a", "--contact-verified", "--contact-ignore"]
mt_config.args = sys.argv
mocked_node = MagicMock(autospec=Node)
iface = MagicMock(autospec=SerialInterface)
iface.getNode.return_value = mocked_node
mocked_node.iface = iface
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface):
main()
mocked_node.getContactURL.assert_called_once_with("!830f522a", should_ignore=True, manually_verified=True)
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_main_set_owner_whitespace_only(capsys):
"""Test --set-owner with whitespace-only name"""
sys.argv = ["", "--set-owner", " "]
mt_config.args = sys.argv
with pytest.raises(SystemExit) as excinfo:
main()
out, _ = capsys.readouterr()
assert "ERROR: Long Name cannot be empty or contain only whitespace characters" in out
assert excinfo.value.code == 1
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_main_set_owner_empty_string(capsys):
"""Test --set-owner with empty string"""
sys.argv = ["", "--set-owner", ""]
mt_config.args = sys.argv
with pytest.raises(SystemExit) as excinfo:
main()
out, _ = capsys.readouterr()
assert "ERROR: Long Name cannot be empty or contain only whitespace characters" in out
assert excinfo.value.code == 1
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_main_set_owner_short_whitespace_only(capsys):
"""Test --set-owner-short with whitespace-only name"""
sys.argv = ["", "--set-owner-short", " "]
mt_config.args = sys.argv
with pytest.raises(SystemExit) as excinfo:
main()
out, _ = capsys.readouterr()
assert "ERROR: Short Name cannot be empty or contain only whitespace characters" in out
assert excinfo.value.code == 1
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_main_set_owner_short_empty_string(capsys):
"""Test --set-owner-short with empty string"""
sys.argv = ["", "--set-owner-short", ""]
mt_config.args = sys.argv
with pytest.raises(SystemExit) as excinfo:
main()
out, _ = capsys.readouterr()
assert "ERROR: Short Name cannot be empty or contain only whitespace characters" in out
assert excinfo.value.code == 1
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_main_set_ham_whitespace_only(capsys):
"""Test --set-ham with whitespace-only name"""
sys.argv = ["", "--set-ham", " "]
mt_config.args = sys.argv
with pytest.raises(SystemExit) as excinfo:
main()
out, _ = capsys.readouterr()
assert "ERROR: Ham radio callsign cannot be empty or contain only whitespace characters" in out
assert excinfo.value.code == 1
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_main_set_ham_empty_string(capsys):
"""Test --set-ham with empty string"""
sys.argv = ["", "--set-ham", ""]
mt_config.args = sys.argv
with pytest.raises(SystemExit) as excinfo:
main()
out, _ = capsys.readouterr()
assert "ERROR: Ham radio callsign cannot be empty or contain only whitespace characters" in out
assert excinfo.value.code == 1
# OTA-related tests
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_main_ota_update_file_not_found(capsys):
"""Test --ota-update with non-existent file"""
sys.argv = [
"",
"--ota-update",
"/nonexistent/firmware.bin",
"--host",
"192.168.1.100",
]
mt_config.args = sys.argv
with pytest.raises(SystemExit) as pytest_wrapped_e:
main()
assert pytest_wrapped_e.type == SystemExit
assert pytest_wrapped_e.value.code == 1
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
@patch("meshtastic.ota.ESP32WiFiOTA")
@patch("meshtastic.__main__.meshtastic.util.our_exit")
def test_main_ota_update_retries(mock_our_exit, mock_ota_class, capsys):
"""Test --ota-update retries on failure"""
# Create a temporary firmware file
with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f:
f.write(b"fake firmware data")
firmware_file = f.name
try:
sys.argv = ["", "--ota-update", firmware_file, "--host", "192.168.1.100"]
mt_config.args = sys.argv
# Mock the OTA class to fail all 5 retries
mock_ota = MagicMock()
mock_ota_class.return_value = mock_ota
mock_ota.hash_bytes.return_value = b"\x00" * 32
mock_ota.hash_hex.return_value = "a" * 64
mock_ota.update.side_effect = Exception("Connection failed")
# Mock isinstance to return True
with patch("meshtastic.__main__.isinstance", return_value=True):
with patch("meshtastic.tcp_interface.TCPInterface") as mock_tcp:
mock_iface = MagicMock()
mock_iface.hostname = "192.168.1.100"
mock_iface.localNode = MagicMock(autospec=Node)
mock_tcp.return_value = mock_iface
with patch("time.sleep"):
main()
# Should have exhausted all retries and called our_exit
# Note: our_exit might be called twice - once for TCP check, once for failure
assert mock_our_exit.call_count >= 1
# Check the last call was for OTA failure
last_call_args = mock_our_exit.call_args[0][0]
assert "OTA update failed" in last_call_args
finally:
os.unlink(firmware_file)

View File

@@ -11,8 +11,12 @@ from ..protobuf import mesh_pb2, config_pb2
from .. import BROADCAST_ADDR, LOCAL_ADDR
from ..mesh_interface import MeshInterface, _timeago
from ..node import Node
from ..slog import LogSet
from ..powermon import SimPowerSupply
try:
# Depends upon the powermon group, not installed by default
from ..slog import LogSet
from ..powermon import SimPowerSupply
except ImportError:
pytest.skip("Can't import LogSet or SimPowerSupply", allow_module_level=True)
# TODO
# from ..config import Config
@@ -521,6 +525,28 @@ def test_getMyNodeInfo():
myinfo = iface.getMyNodeInfo()
assert myinfo == anode
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_getCannedMessage():
"""Test MeshInterface.getCannedMessage()"""
iface = MeshInterface(noProto=True)
node = MagicMock()
node.get_canned_message.return_value = "Hi|Bye|Yes"
iface.localNode = node
result = iface.getCannedMessage()
assert result == "Hi|Bye|Yes"
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_getRingtone():
"""Test MeshInterface.getRingtone()"""
iface = MeshInterface(noProto=True)
node = MagicMock()
node.get_ringtone.return_value = "foo,bar"
iface.localNode = node
result = iface.getRingtone()
assert result == "foo,bar"
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
@@ -539,7 +565,6 @@ def test_generatePacketId(capsys):
assert err == ""
assert pytest_wrapped_e.type == MeshInterface.MeshInterfaceError
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_fixupPosition_empty_pos():
@@ -647,15 +672,21 @@ def test_getOrCreateByNum(iface_with_nodes):
@pytest.mark.unit
def test_exit_with_exception(caplog):
"""Test __exit__()"""
iface = MeshInterface(noProto=True)
with caplog.at_level(logging.ERROR):
iface.__exit__("foo", "bar", "baz")
assert re.search(
r"An exception of type foo with value bar has occurred",
caplog.text,
re.MULTILINE,
)
assert re.search(r"Traceback: baz", caplog.text, re.MULTILINE)
try:
with MeshInterface(noProto=True):
raise ValueError("Something went wrong")
except:
assert re.search(
r"An exception of type <class \'ValueError\'> with value Something went wrong has occurred",
caplog.text,
re.MULTILINE,
)
assert re.search(
r"Traceback:\n.*in test_exit_with_exception\n {4}raise ValueError\(\"Something went wrong\"\)",
caplog.text,
re.MULTILINE
)
@pytest.mark.unit
@@ -726,3 +757,43 @@ def test_timeago_fuzz(seconds):
"""Fuzz _timeago to ensure it works with any integer"""
val = _timeago(seconds)
assert re.match(r"(now|\d+ (secs?|mins?|hours?|days?|months?|years?))", val)
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_onResponseTraceRoute_routing_error(capsys):
"""Test that onResponseTraceRoute handles ROUTING_APP error packets correctly."""
iface = MeshInterface(noProto=True)
packet = {
"decoded": {
"portnum": "ROUTING_APP",
"routing": {"errorReason": "MAX_RETRANSMIT"},
}
}
iface.onResponseTraceRoute(packet)
assert iface._acknowledgment.receivedTraceRoute is True
out, _ = capsys.readouterr()
assert "Traceroute failed: MAX_RETRANSMIT" in out
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_onResponseTraceRoute_routing_none(capsys):
"""Test that onResponseTraceRoute does not print error for ROUTING_APP with NONE errorReason."""
iface = MeshInterface(noProto=True)
packet = {
"decoded": {
"portnum": "ROUTING_APP",
"routing": {"errorReason": "NONE"},
}
}
iface.onResponseTraceRoute(packet)
assert iface._acknowledgment.receivedTraceRoute is True
out, _ = capsys.readouterr()
assert "Traceroute failed" not in out

View File

@@ -0,0 +1,22 @@
"""Meshtastic unit tests for traffic management handling in mesh_interface.py."""
import pytest
from ..mesh_interface import MeshInterface
from ..protobuf import mesh_pb2
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_handleFromRadio_with_traffic_management_module_config():
"""Test _handleFromRadio with moduleConfig.traffic_management."""
iface = MeshInterface(noProto=True)
from_radio = mesh_pb2.FromRadio()
from_radio.moduleConfig.traffic_management.enabled = True
from_radio.moduleConfig.traffic_management.rate_limit_enabled = True
iface._handleFromRadio(from_radio.SerializeToString())
assert iface.localNode.moduleConfig.traffic_management.enabled is True
assert iface.localNode.moduleConfig.traffic_management.rate_limit_enabled is True
iface.close()

View File

@@ -1,16 +1,20 @@
"""Meshtastic unit tests for node.py"""
# pylint: disable=C0302
import base64
import logging
import re
from unittest.mock import MagicMock, patch
import pytest
from hypothesis import given, strategies as st
from ..protobuf import localonly_pb2, config_pb2
from ..protobuf import admin_pb2, localonly_pb2, config_pb2, mesh_pb2, nanopb_pb2
from ..protobuf.channel_pb2 import Channel # pylint: disable=E0611
from ..node import Node
from ..serial_interface import SerialInterface
from ..mesh_interface import MeshInterface
from ..util import to_node_num
# from ..config_pb2 import Config
# from ..cannedmessages_pb2 import (CannedMessagePluginMessagePart1, CannedMessagePluginMessagePart2,
@@ -18,6 +22,11 @@ from ..mesh_interface import MeshInterface
# CannedMessagePluginMessagePart5)
# from ..util import Timeout
# Extract nanopb max_size constraints from the User protobuf descriptor
_USER_NANOPB = {
field.name: field.GetOptions().Extensions[nanopb_pb2.nanopb]
for field in mesh_pb2.User.DESCRIPTOR.fields
}
@pytest.mark.unit
def test_node(capsys):
@@ -261,6 +270,36 @@ def test_shutdown(caplog):
assert re.search(r"Telling node to shutdown", caplog.text, re.MULTILINE)
@pytest.mark.unit
def test_factoryReset_config_uses_int_field():
"""Test factoryReset(config) sets int32 protobuf field with an int value."""
iface = MagicMock(autospec=MeshInterface)
anode = Node(iface, 1234567890, noProto=True)
amesg = admin_pb2.AdminMessage()
with patch("meshtastic.node.admin_pb2.AdminMessage", return_value=amesg):
with patch.object(anode, "_sendAdmin") as mock_send_admin:
anode.factoryReset(full=False)
assert amesg.factory_reset_config == 1
mock_send_admin.assert_called_once_with(amesg, onResponse=anode.onAckNak)
@pytest.mark.unit
def test_factoryReset_full_sets_device_field():
"""Test factoryReset(full=True) sets the full-device reset protobuf field."""
iface = MagicMock(autospec=MeshInterface)
anode = Node(iface, 1234567890, noProto=True)
amesg = admin_pb2.AdminMessage()
with patch("meshtastic.node.admin_pb2.AdminMessage", return_value=amesg):
with patch.object(anode, "_sendAdmin") as mock_send_admin:
anode.factoryReset(full=True)
assert amesg.factory_reset_device == 1
mock_send_admin.assert_called_once_with(amesg, onResponse=anode.onAckNak)
@pytest.mark.unit
def test_setURL_empty_url(capsys):
"""Test reboot"""
@@ -270,7 +309,7 @@ def test_setURL_empty_url(capsys):
assert pytest_wrapped_e.type == SystemExit
assert pytest_wrapped_e.value.code == 1
out, err = capsys.readouterr()
assert re.search(r"Warning: There were no settings.", out, re.MULTILINE)
assert re.search(r"Warning: config or channels not loaded", out, re.MULTILINE)
assert err == ""
@@ -304,10 +343,252 @@ def test_setURL_valid_URL_but_no_settings(capsys):
assert pytest_wrapped_e.type == SystemExit
assert pytest_wrapped_e.value.code == 1
out, err = capsys.readouterr()
assert re.search(r"Warning: There were no settings", out, re.MULTILINE)
assert re.search(r"Warning: config or channels not loaded", out, re.MULTILINE)
assert err == ""
@pytest.mark.unit
@pytest.mark.parametrize("node_id,node_data,should_ignore,manually_verified", [
pytest.param(
"!830f522a",
{
"num": 2198819370,
"user": {
"id": "!830f522a",
"longName": "Roadrunner Ridge",
"shortName": "RKSN",
"macaddr": "AAAAAAAAAAA=",
"hwModel": "RAK4631",
"role": "ROUTER",
"publicKey": "Rx8XD96uBAiFGoFusdqwti3eBT4DLyGuG7g5Wcg9Bw==",
"isLicensed": True,
"isUnmessagable": False,
},
},
True,
True,
id="all_fields_all_flags",
),
pytest.param(
"!12345678",
{
"num": 305419896,
"user": {
"id": "!12345678",
"longName": "Test Node",
"shortName": "TN",
"macaddr": "QkVTVEVWRVI=",
"hwModel": "TBEAM",
},
},
False,
False,
id="minimal_fields_no_flags",
),
pytest.param(
305419896,
{
"num": 305419896,
"user": {
"id": "!12345678",
"longName": "Another Node",
"shortName": "AN",
"macaddr": "QkVTVEVWRVI=",
"hwModel": "HELTEC_V3",
"role": "CLIENT",
"publicKey": "AAAAAAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
"isLicensed": False,
},
},
True,
False,
id="int_node_id_should_ignore_only",
),
pytest.param(
"!deadbeef",
{
"num": 3735928559,
"user": {
"id": "!deadbeef",
"longName": "Minimal Contact",
"shortName": "MC",
"macaddr": "BQYHCAkKCw==",
"hwModel": "UNSET",
"role": "CLIENT_MUTE",
},
},
False,
True,
id="unset_hw_model_verified_only",
),
pytest.param(
"!1a2b3c4d",
{
"num": 439041101,
"user": {
"id": "!1a2b3c4d",
"longName": "Licensed Node",
"shortName": "LN",
"macaddr": "DA0ODxAREg==",
"hwModel": "NANO_G1",
"isLicensed": True,
"isUnmessagable": True,
},
},
False,
False,
id="licensed_unmessagable_no_flags",
),
])
def test_contact_url_roundtrip(node_id, node_data, should_ignore, manually_verified):
"""Verify that contact URL generation via getContactURL() and parsing via addContactURL() is fully reversible"""
iface = MagicMock(autospec=MeshInterface)
node_num = to_node_num(node_id)
iface.nodesByNum = {node_num: node_data}
iface.localNode = None
anode = Node(iface, node_num, noProto=True)
sent_admin = []
def capture_send(p, *_args, **_kwargs):
sent_admin.append(p)
with patch.object(anode, "_sendAdmin", side_effect=capture_send):
url = anode.getContactURL(node_id, should_ignore=should_ignore, manually_verified=manually_verified)
assert url.startswith("https://meshtastic.org/v/#")
anode.addContactURL(url)
assert len(sent_admin) == 1
contact = sent_admin[0].add_contact
u = node_data["user"]
assert contact.node_num == node_num
assert contact.user.id == u["id"]
assert contact.user.long_name == u["longName"]
assert contact.user.short_name == u["shortName"]
assert contact.user.macaddr == base64.b64decode(u["macaddr"])
if u.get("hwModel") and u["hwModel"] != "UNSET":
assert contact.user.hw_model == mesh_pb2.HardwareModel.Value(u["hwModel"])
if u.get("role"):
assert contact.user.role == config_pb2.Config.DeviceConfig.Role.Value(u["role"])
if u.get("publicKey"):
assert contact.user.public_key == base64.b64decode(u["publicKey"])
if u.get("isLicensed"):
assert contact.user.is_licensed is True
if u.get("isUnmessagable") is not None:
assert contact.user.is_unmessagable == u["isUnmessagable"]
assert contact.should_ignore == should_ignore
assert contact.manually_verified == manually_verified
@st.composite
def contact_url_roundtrip_params(draw):
"""Hypothesis strategy: generate a full node config and roundtrip flags"""
should_ignore = draw(st.booleans())
manually_verified = draw(st.booleans())
node_num = draw(st.integers(min_value=6, max_value=2**32 - 2))
node_id = f"!{node_num:08x}"
hw_model = draw(st.sampled_from(list(mesh_pb2.HardwareModel.keys())))
role = draw(st.one_of(
st.none(),
st.sampled_from(list(config_pb2.Config.DeviceConfig.Role.keys())),
))
long_name = draw(st.text(
min_size=1, max_size=_USER_NANOPB['long_name'].max_size
))
short_name = draw(st.text(
min_size=1, max_size=_USER_NANOPB['short_name'].max_size
))
macaddr_bytes = draw(st.binary(
min_size=_USER_NANOPB['macaddr'].max_size,
max_size=_USER_NANOPB['macaddr'].max_size,
))
macaddr_b64 = base64.b64encode(macaddr_bytes).decode("ascii")
has_public_key = draw(st.booleans())
public_key_b64 = None
if has_public_key:
pk_bytes = draw(st.binary(
min_size=_USER_NANOPB['public_key'].max_size,
max_size=_USER_NANOPB['public_key'].max_size,
))
public_key_b64 = base64.b64encode(pk_bytes).decode("ascii")
is_licensed = draw(st.booleans())
is_unmessagable = draw(st.booleans())
node_data = {
"num": node_num,
"user": {
"id": node_id,
"longName": long_name,
"shortName": short_name,
"macaddr": macaddr_b64,
"hwModel": hw_model,
"isLicensed": is_licensed,
"isUnmessagable": is_unmessagable,
},
}
if role is not None:
node_data["user"]["role"] = role
if public_key_b64 is not None:
node_data["user"]["publicKey"] = public_key_b64
return node_num, node_data, should_ignore, manually_verified
@pytest.mark.unit
@given(contact_url_roundtrip_params())
def test_contact_url_roundtrip_hypothesis(params):
"""Property: roundtrip preserves data across random field configurations"""
node_num, node_data, should_ignore, manually_verified = params
iface = MagicMock(autospec=MeshInterface)
iface.nodesByNum = {node_num: node_data}
iface.localNode = None
anode = Node(iface, node_num, noProto=True)
sent_admin = []
def capture_send(p, *_args, **_kwargs):
sent_admin.append(p)
with patch.object(anode, "_sendAdmin", side_effect=capture_send):
url = anode.getContactURL(
node_num,
should_ignore=should_ignore,
manually_verified=manually_verified,
)
anode.addContactURL(url)
assert len(sent_admin) == 1
contact = sent_admin[0].add_contact
u = node_data["user"]
assert contact.node_num == node_num
assert contact.user.id == u["id"]
assert contact.user.long_name == u["longName"]
assert contact.user.short_name == u["shortName"]
assert contact.user.macaddr == base64.b64decode(u["macaddr"])
assert contact.user.hw_model == mesh_pb2.HardwareModel.Value(u["hwModel"])
if "role" in u:
assert contact.user.role == config_pb2.Config.DeviceConfig.Role.Value(u["role"])
if "publicKey" in u:
assert contact.user.public_key == base64.b64decode(u["publicKey"])
assert contact.user.is_licensed == u["isLicensed"]
assert contact.user.is_unmessagable == u["isUnmessagable"]
assert contact.should_ignore == should_ignore
assert contact.manually_verified == manually_verified
# TODO
# @pytest.mark.unit
# def test_showChannels(capsys):
@@ -794,6 +1075,30 @@ def test_writeConfig_with_no_radioConfig(capsys):
assert err == ""
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_writeConfig_traffic_management():
"""Test writeConfig with traffic_management module config."""
iface = MagicMock(autospec=SerialInterface)
anode = Node(iface, 123, noProto=True)
anode.moduleConfig.traffic_management.enabled = True
anode.moduleConfig.traffic_management.rate_limit_enabled = True
sent_admin = []
def capture_send(p, *args, **kwargs): # pylint: disable=W0613
sent_admin.append(p)
with patch.object(anode, "_sendAdmin", side_effect=capture_send):
anode.writeConfig("traffic_management")
assert len(sent_admin) == 1
assert sent_admin[0].HasField("set_module_config")
assert sent_admin[0].set_module_config.HasField("traffic_management")
assert sent_admin[0].set_module_config.traffic_management.enabled is True
assert sent_admin[0].set_module_config.traffic_management.rate_limit_enabled is True
# TODO
# @pytest.mark.unit
# def test_writeConfig(caplog):
@@ -1254,8 +1559,7 @@ def test_requestChannels_non_localNode_starting_index(caplog):
# },
# 'id': 1692918436,
# 'hopLimit': 3,
# 'priority':
# 'RELIABLE',
# 'priority': 'RELIABLE',
# 'raw': 'fake',
# 'fromId': '!9388f81c',
# 'toId': '!9388f81c'
@@ -1426,6 +1730,166 @@ def test_requestChannels_non_localNode_starting_index(caplog):
# assert err == ''
@pytest.mark.unit
@pytest.mark.parametrize("favorite", ["!1dec0ded", 502009325])
def test_set_favorite(favorite):
"""Test setFavorite"""
iface = MagicMock(autospec=SerialInterface)
node = Node(iface, 12345678)
amesg = admin_pb2.AdminMessage()
with patch("meshtastic.admin_pb2.AdminMessage", return_value=amesg):
node.setFavorite(favorite)
assert amesg.set_favorite_node == 502009325
iface.sendData.assert_called_once()
@pytest.mark.unit
@pytest.mark.parametrize("favorite", ["!1dec0ded", 502009325])
def test_remove_favorite(favorite):
"""Test setFavorite"""
iface = MagicMock(autospec=SerialInterface)
node = Node(iface, 12345678)
amesg = admin_pb2.AdminMessage()
with patch("meshtastic.admin_pb2.AdminMessage", return_value=amesg):
node.removeFavorite(favorite)
assert amesg.remove_favorite_node == 502009325
iface.sendData.assert_called_once()
@pytest.mark.unit
@pytest.mark.parametrize("ignored", ["!1dec0ded", 502009325])
def test_set_ignored(ignored):
"""Test setFavorite"""
iface = MagicMock(autospec=SerialInterface)
node = Node(iface, 12345678)
amesg = admin_pb2.AdminMessage()
with patch("meshtastic.admin_pb2.AdminMessage", return_value=amesg):
node.setIgnored(ignored)
assert amesg.set_ignored_node == 502009325
iface.sendData.assert_called_once()
@pytest.mark.unit
@pytest.mark.parametrize("ignored", ["!1dec0ded", 502009325])
def test_remove_ignored(ignored):
"""Test setFavorite"""
iface = MagicMock(autospec=SerialInterface)
node = Node(iface, 12345678)
amesg = admin_pb2.AdminMessage()
with patch("meshtastic.admin_pb2.AdminMessage", return_value=amesg):
node.removeIgnored(ignored)
assert amesg.remove_ignored_node == 502009325
iface.sendData.assert_called_once()
@pytest.mark.unit
def test_setOwner_whitespace_only_long_name(capsys):
"""Test setOwner with whitespace-only long name"""
iface = MagicMock(autospec=MeshInterface)
anode = Node(iface, 123, noProto=True)
with pytest.raises(SystemExit) as excinfo:
anode.setOwner(long_name=" ")
out, _ = capsys.readouterr()
assert "ERROR: Long Name cannot be empty or contain only whitespace characters" in out
assert excinfo.value.code == 1
@pytest.mark.unit
def test_setOwner_empty_long_name(capsys):
"""Test setOwner with empty long name"""
iface = MagicMock(autospec=MeshInterface)
anode = Node(iface, 123, noProto=True)
with pytest.raises(SystemExit) as excinfo:
anode.setOwner(long_name="")
out, _ = capsys.readouterr()
assert "ERROR: Long Name cannot be empty or contain only whitespace characters" in out
assert excinfo.value.code == 1
@pytest.mark.unit
def test_setOwner_whitespace_only_short_name(capsys):
"""Test setOwner with whitespace-only short name"""
iface = MagicMock(autospec=MeshInterface)
anode = Node(iface, 123, noProto=True)
with pytest.raises(SystemExit) as excinfo:
anode.setOwner(short_name=" ")
out, _ = capsys.readouterr()
assert "ERROR: Short Name cannot be empty or contain only whitespace characters" in out
assert excinfo.value.code == 1
@pytest.mark.unit
def test_setOwner_empty_short_name(capsys):
"""Test setOwner with empty short name"""
iface = MagicMock(autospec=MeshInterface)
anode = Node(iface, 123, noProto=True)
with pytest.raises(SystemExit) as excinfo:
anode.setOwner(short_name="")
out, _ = capsys.readouterr()
assert "ERROR: Short Name cannot be empty or contain only whitespace characters" in out
assert excinfo.value.code == 1
@pytest.mark.unit
def test_setOwner_valid_names(caplog):
"""Test setOwner with valid names"""
iface = MagicMock(autospec=MeshInterface)
anode = Node(iface, 123, noProto=True)
with caplog.at_level(logging.DEBUG):
anode.setOwner(long_name="ValidName", short_name="VN")
# Should not raise any exceptions
# Note: When noProto=True, _sendAdmin is not called as the method returns early
assert re.search(r'p.set_owner.long_name:ValidName:', caplog.text, re.MULTILINE)
assert re.search(r'p.set_owner.short_name:VN:', caplog.text, re.MULTILINE)
@pytest.mark.unit
def test_start_ota_local_node():
"""Test startOTA on local node"""
iface = MagicMock(autospec=MeshInterface)
anode = Node(iface, 1234567890, noProto=True)
# Set up as local node
iface.localNode = anode
amesg = admin_pb2.AdminMessage()
with patch("meshtastic.admin_pb2.AdminMessage", return_value=amesg):
with patch.object(anode, "_sendAdmin") as mock_send_admin:
test_hash = b"\x01\x02\x03" * 8 # 24 bytes hash
anode.startOTA(ota_mode=admin_pb2.OTAMode.OTA_WIFI, ota_file_hash=test_hash)
# Verify the OTA request was set correctly
assert amesg.ota_request.reboot_ota_mode == admin_pb2.OTAMode.OTA_WIFI
assert amesg.ota_request.ota_hash == test_hash
mock_send_admin.assert_called_once_with(amesg)
@pytest.mark.unit
def test_start_ota_remote_node_raises_error():
"""Test startOTA on remote node raises ValueError"""
iface = MagicMock(autospec=MeshInterface)
local_node = Node(iface, 1234567890, noProto=True)
remote_node = Node(iface, 9876543210, noProto=True)
iface.localNode = local_node
test_hash = b"\x01\x02\x03" * 8
with pytest.raises(ValueError, match="startOTA only possible in local node"):
remote_node.startOTA(
ota_mode=admin_pb2.OTAMode.OTA_WIFI, ota_file_hash=test_hash
)
# TODO
# @pytest.mark.unitslow
# def test_waitForConfig():

View File

@@ -0,0 +1,455 @@
"""Meshtastic unit tests for ota.py"""
import hashlib
import logging
import os
import socket
import tempfile
from unittest.mock import MagicMock, patch
import pytest
from meshtastic.ota import (
_file_sha256,
ESP32WiFiOTA,
OTAError,
)
@pytest.mark.unit
def test_file_sha256():
"""Test _file_sha256 calculates correct hash"""
# Create a temporary file with known content
with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f:
test_data = b"Hello, World!"
f.write(test_data)
temp_file = f.name
try:
result = _file_sha256(temp_file)
expected_hash = hashlib.sha256(test_data).hexdigest()
assert result.hexdigest() == expected_hash
finally:
os.unlink(temp_file)
@pytest.mark.unit
def test_file_sha256_large_file():
"""Test _file_sha256 handles files larger than chunk size"""
# Create a temporary file with more than 4096 bytes
with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f:
test_data = b"A" * 8192 # More than 4096 bytes
f.write(test_data)
temp_file = f.name
try:
result = _file_sha256(temp_file)
expected_hash = hashlib.sha256(test_data).hexdigest()
assert result.hexdigest() == expected_hash
finally:
os.unlink(temp_file)
@pytest.mark.unit
def test_esp32_wifi_ota_init_file_not_found():
"""Test ESP32WiFiOTA raises FileNotFoundError for non-existent file"""
with pytest.raises(FileNotFoundError, match="does not exist"):
ESP32WiFiOTA("/nonexistent/firmware.bin", "192.168.1.1")
@pytest.mark.unit
def test_esp32_wifi_ota_init_success():
"""Test ESP32WiFiOTA initializes correctly with valid file"""
with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f:
f.write(b"fake firmware data")
temp_file = f.name
try:
ota = ESP32WiFiOTA(temp_file, "192.168.1.1", 3232)
assert ota._filename == temp_file
assert ota._hostname == "192.168.1.1"
assert ota._port == 3232
assert ota._socket is None
# Verify hash is calculated
assert ota._file_hash is not None
assert len(ota.hash_hex()) == 64 # SHA256 hex is 64 chars
finally:
os.unlink(temp_file)
@pytest.mark.unit
def test_esp32_wifi_ota_init_default_port():
"""Test ESP32WiFiOTA uses default port 3232"""
with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f:
f.write(b"fake firmware data")
temp_file = f.name
try:
ota = ESP32WiFiOTA(temp_file, "192.168.1.1")
assert ota._port == 3232
finally:
os.unlink(temp_file)
@pytest.mark.unit
def test_esp32_wifi_ota_hash_bytes():
"""Test hash_bytes returns correct bytes"""
with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f:
test_data = b"firmware data"
f.write(test_data)
temp_file = f.name
try:
ota = ESP32WiFiOTA(temp_file, "192.168.1.1")
hash_bytes = ota.hash_bytes()
expected_bytes = hashlib.sha256(test_data).digest()
assert hash_bytes == expected_bytes
assert len(hash_bytes) == 32 # SHA256 is 32 bytes
finally:
os.unlink(temp_file)
@pytest.mark.unit
def test_esp32_wifi_ota_hash_hex():
"""Test hash_hex returns correct hex string"""
with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f:
test_data = b"firmware data"
f.write(test_data)
temp_file = f.name
try:
ota = ESP32WiFiOTA(temp_file, "192.168.1.1")
hash_hex = ota.hash_hex()
expected_hex = hashlib.sha256(test_data).hexdigest()
assert hash_hex == expected_hex
assert len(hash_hex) == 64 # SHA256 hex is 64 chars
finally:
os.unlink(temp_file)
@pytest.mark.unit
def test_esp32_wifi_ota_read_line_not_connected():
"""Test _read_line raises ConnectionError when not connected"""
with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f:
f.write(b"firmware")
temp_file = f.name
try:
ota = ESP32WiFiOTA(temp_file, "192.168.1.1")
with pytest.raises(ConnectionError, match="Socket not connected"):
ota._read_line()
finally:
os.unlink(temp_file)
@pytest.mark.unit
def test_esp32_wifi_ota_read_line_connection_closed():
"""Test _read_line raises ConnectionError when connection closed"""
with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f:
f.write(b"firmware")
temp_file = f.name
try:
ota = ESP32WiFiOTA(temp_file, "192.168.1.1")
mock_socket = MagicMock()
# Simulate connection closed
mock_socket.recv.return_value = b""
ota._socket = mock_socket
with pytest.raises(ConnectionError, match="Connection closed"):
ota._read_line()
finally:
os.unlink(temp_file)
@pytest.mark.unit
def test_esp32_wifi_ota_read_line_success():
"""Test _read_line successfully reads a line"""
with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f:
f.write(b"firmware")
temp_file = f.name
try:
ota = ESP32WiFiOTA(temp_file, "192.168.1.1")
mock_socket = MagicMock()
# Simulate receiving "OK\n"
mock_socket.recv.side_effect = [b"O", b"K", b"\n"]
ota._socket = mock_socket
result = ota._read_line()
assert result == "OK"
finally:
os.unlink(temp_file)
@pytest.mark.unit
@patch("meshtastic.ota.socket.socket")
def test_esp32_wifi_ota_update_success(mock_socket_class):
"""Test update() with successful OTA"""
with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f:
test_data = b"A" * 1024 # 1KB of data
f.write(test_data)
temp_file = f.name
try:
# Setup mock socket
mock_socket = MagicMock()
mock_socket_class.return_value = mock_socket
ota = ESP32WiFiOTA(temp_file, "192.168.1.1")
# Mock _read_line to return appropriate responses
# First call: ERASING, Second call: OK (ready), Third call: OK (complete)
with patch.object(ota, "_read_line") as mock_read_line:
mock_read_line.side_effect = [
"ERASING", # Device is erasing flash
"OK", # Device ready for firmware
"OK", # Device finished successfully
]
ota.update()
# Verify socket was created and connected
mock_socket_class.assert_called_once_with(
socket.AF_INET, socket.SOCK_STREAM
)
mock_socket.settimeout.assert_called_once_with(15)
mock_socket.connect.assert_called_once_with(("192.168.1.1", 3232))
# Verify start command was sent
start_cmd = f"OTA {len(test_data)} {ota.hash_hex()}\n".encode("utf-8")
mock_socket.sendall.assert_any_call(start_cmd)
# Verify firmware was sent (at least one chunk)
assert mock_socket.sendall.call_count >= 2
# Verify socket was closed
mock_socket.close.assert_called_once()
finally:
os.unlink(temp_file)
@pytest.mark.unit
@patch("meshtastic.ota.socket.socket")
def test_esp32_wifi_ota_update_with_progress_callback(mock_socket_class):
"""Test update() with progress callback"""
with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f:
test_data = b"A" * 1024 # 1KB of data
f.write(test_data)
temp_file = f.name
try:
# Setup mock socket
mock_socket = MagicMock()
mock_socket_class.return_value = mock_socket
ota = ESP32WiFiOTA(temp_file, "192.168.1.1")
# Track progress callback calls
progress_calls = []
def progress_callback(sent, total):
progress_calls.append((sent, total))
# Mock _read_line
with patch.object(ota, "_read_line") as mock_read_line:
mock_read_line.side_effect = [
"OK", # Device ready
"OK", # Device finished
]
ota.update(progress_callback=progress_callback)
# Verify progress callback was called
assert len(progress_calls) > 0
# First call should show some progress
assert progress_calls[0][0] > 0
# Total should be the firmware size
assert progress_calls[0][1] == len(test_data)
# Last call should show all bytes sent
assert progress_calls[-1][0] == len(test_data)
finally:
os.unlink(temp_file)
@pytest.mark.unit
@patch("meshtastic.ota.socket.socket")
def test_esp32_wifi_ota_update_device_error_on_start(mock_socket_class):
"""Test update() raises OTAError when device reports error during start"""
with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f:
f.write(b"firmware")
temp_file = f.name
try:
mock_socket = MagicMock()
mock_socket_class.return_value = mock_socket
ota = ESP32WiFiOTA(temp_file, "192.168.1.1")
with patch.object(ota, "_read_line") as mock_read_line:
mock_read_line.return_value = "ERR BAD_HASH"
with pytest.raises(OTAError, match="Device reported error"):
ota.update()
finally:
os.unlink(temp_file)
@pytest.mark.unit
@patch("meshtastic.ota.socket.socket")
def test_esp32_wifi_ota_update_device_error_on_finish(mock_socket_class):
"""Test update() raises OTAError when device reports error after firmware sent"""
with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f:
f.write(b"firmware")
temp_file = f.name
try:
mock_socket = MagicMock()
mock_socket_class.return_value = mock_socket
ota = ESP32WiFiOTA(temp_file, "192.168.1.1")
with patch.object(ota, "_read_line") as mock_read_line:
mock_read_line.side_effect = [
"OK", # Device ready
"ERR FLASH_ERR", # Error after firmware sent
]
with pytest.raises(OTAError, match="OTA update failed"):
ota.update()
finally:
os.unlink(temp_file)
@pytest.mark.unit
@patch("meshtastic.ota.socket.socket")
def test_esp32_wifi_ota_update_socket_cleanup_on_error(mock_socket_class):
"""Test that socket is properly cleaned up on error"""
with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f:
f.write(b"firmware")
temp_file = f.name
try:
mock_socket = MagicMock()
mock_socket_class.return_value = mock_socket
ota = ESP32WiFiOTA(temp_file, "192.168.1.1")
# Simulate connection error
mock_socket.connect.side_effect = ConnectionRefusedError("Connection refused")
with pytest.raises(ConnectionRefusedError):
ota.update()
# Verify socket was closed even on error
mock_socket.close.assert_called_once()
assert ota._socket is None
finally:
os.unlink(temp_file)
@pytest.mark.unit
@patch("meshtastic.ota.socket.socket")
def test_esp32_wifi_ota_update_large_firmware(mock_socket_class):
"""Test update() correctly chunks large firmware files"""
with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f:
# Create a file larger than chunk_size (1024)
test_data = b"B" * 3000
f.write(test_data)
temp_file = f.name
try:
mock_socket = MagicMock()
mock_socket_class.return_value = mock_socket
ota = ESP32WiFiOTA(temp_file, "192.168.1.1")
with patch.object(ota, "_read_line") as mock_read_line:
mock_read_line.side_effect = [
"OK", # Device ready
"OK", # Device finished
]
ota.update()
# Verify that all data was sent in chunks
# 3000 bytes should be sent in ~3 chunks of 1024 bytes
sendall_calls = [
call
for call in mock_socket.sendall.call_args_list
if call[0][0]
!= f"OTA {len(test_data)} {ota.hash_hex()}\n".encode("utf-8")
]
# Calculate total data sent (excluding the start command)
total_sent = sum(len(call[0][0]) for call in sendall_calls)
assert total_sent == len(test_data)
finally:
os.unlink(temp_file)
@pytest.mark.unit
@patch("meshtastic.ota.socket.socket")
def test_esp32_wifi_ota_update_unexpected_response_warning(mock_socket_class, caplog):
"""Test update() logs warning on unexpected response during startup"""
with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f:
f.write(b"firmware")
temp_file = f.name
try:
mock_socket = MagicMock()
mock_socket_class.return_value = mock_socket
ota = ESP32WiFiOTA(temp_file, "192.168.1.1")
with patch.object(ota, "_read_line") as mock_read_line:
mock_read_line.side_effect = [
"UNKNOWN", # Unexpected response
"OK", # Then proceed
"OK", # Device finished
]
with caplog.at_level(logging.WARNING):
ota.update()
# Check that warning was logged for unexpected response
assert "Unexpected response" in caplog.text
finally:
os.unlink(temp_file)
@pytest.mark.unit
@patch("meshtastic.ota.socket.socket")
def test_esp32_wifi_ota_update_unexpected_final_response(mock_socket_class, caplog):
"""Test update() logs warning on unexpected final response after firmware upload"""
with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f:
f.write(b"firmware")
temp_file = f.name
try:
mock_socket = MagicMock()
mock_socket_class.return_value = mock_socket
ota = ESP32WiFiOTA(temp_file, "192.168.1.1")
with patch.object(ota, "_read_line") as mock_read_line:
mock_read_line.side_effect = [
"OK", # Device ready for firmware
"UNKNOWN", # Unexpected final response (not OK, not ERR, not ACK)
"OK", # Then succeed
]
with caplog.at_level(logging.WARNING):
ota.update()
# Check that warning was logged for unexpected final response
assert "Unexpected final response" in caplog.text
finally:
os.unlink(temp_file)

View File

@@ -2,6 +2,7 @@
# pylint: disable=R0917
import re
import sys
from unittest.mock import mock_open, patch
import pytest
@@ -9,16 +10,14 @@ import pytest
from ..serial_interface import SerialInterface
from ..protobuf import config_pb2
@pytest.mark.unit
@patch("time.sleep")
@patch("termios.tcsetattr")
@patch("termios.tcgetattr")
@patch("meshtastic.serial_interface.SerialInterface._set_hupcl_with_termios")
@patch("builtins.open", new_callable=mock_open, read_data="data")
@patch("serial.Serial")
@patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"])
def test_SerialInterface_single_port(
mocked_findPorts, mocked_serial, mocked_open, mock_get, mock_set, mock_sleep, capsys
mocked_findPorts, mocked_serial, mocked_open, mock_hupcl, mock_sleep, capsys
):
"""Test that we can instantiate a SerialInterface with a single port"""
iface = SerialInterface(noProto=True)
@@ -28,9 +27,12 @@ def test_SerialInterface_single_port(
iface.close()
mocked_findPorts.assert_called()
mocked_serial.assert_called()
mocked_open.assert_called()
mock_get.assert_called()
mock_set.assert_called()
# doesn't get called in SerialInterface on windows
if sys.platform != "win32":
mocked_open.assert_called()
mock_hupcl.assert_called()
mock_sleep.assert_called()
out, err = capsys.readouterr()
assert re.search(r"Nodes in mesh", out, re.MULTILINE)

View File

@@ -0,0 +1,221 @@
"""Meshtastic unit tests for showNodes favorite column feature"""
from unittest.mock import MagicMock
import pytest
from ..mesh_interface import MeshInterface
@pytest.fixture
def _iface_with_favorite_nodes():
"""Fixture to setup nodes with favorite flags."""
nodesById = {
"!9388f81c": {
"num": 2475227164,
"user": {
"id": "!9388f81c",
"longName": "Favorite Node",
"shortName": "FAV1",
"macaddr": "RBeTiPgc",
"hwModel": "TBEAM",
},
"position": {},
"lastHeard": 1640204888,
"isFavorite": True,
},
"!12345678": {
"num": 305419896,
"user": {
"id": "!12345678",
"longName": "Regular Node",
"shortName": "REG1",
"macaddr": "ABCDEFGH",
"hwModel": "TLORA_V2",
},
"position": {},
"lastHeard": 1640204999,
"isFavorite": False,
},
"!abcdef00": {
"num": 2882400000,
"user": {
"id": "!abcdef00",
"longName": "Legacy Node",
"shortName": "LEG1",
"macaddr": "XYZABC00",
"hwModel": "HELTEC_V3",
},
"position": {},
"lastHeard": 1640205000,
# Note: No isFavorite field - testing backward compatibility
},
}
nodesByNum = {
2475227164: {
"num": 2475227164,
"user": {
"id": "!9388f81c",
"longName": "Favorite Node",
"shortName": "FAV1",
"macaddr": "RBeTiPgc",
"hwModel": "TBEAM",
},
"position": {"time": 1640206266},
"lastHeard": 1640206266,
"isFavorite": True,
},
305419896: {
"num": 305419896,
"user": {
"id": "!12345678",
"longName": "Regular Node",
"shortName": "REG1",
"macaddr": "ABCDEFGH",
"hwModel": "TLORA_V2",
},
"position": {"time": 1640206200},
"lastHeard": 1640206200,
"isFavorite": False,
},
2882400000: {
"num": 2882400000,
"user": {
"id": "!abcdef00",
"longName": "Legacy Node",
"shortName": "LEG1",
"macaddr": "XYZABC00",
"hwModel": "HELTEC_V3",
},
"position": {"time": 1640206100},
"lastHeard": 1640206100,
# Note: No isFavorite field - testing backward compatibility
},
}
iface = MeshInterface(noProto=True)
iface.nodes = nodesById
iface.nodesByNum = nodesByNum
myInfo = MagicMock()
iface.myInfo = myInfo
iface.myInfo.my_node_num = 2475227164
return iface
@pytest.mark.unit
def test_showNodes_favorite_column_header(capsys, _iface_with_favorite_nodes):
"""Test that 'Fav' column header appears in showNodes output"""
iface = _iface_with_favorite_nodes
iface.showNodes()
out, err = capsys.readouterr()
assert "Fav" in out
assert err == ""
@pytest.mark.unit
def test_showNodes_favorite_asterisk_display(capsys, _iface_with_favorite_nodes):
"""Test that favorite nodes show asterisk and non-favorites show empty"""
iface = _iface_with_favorite_nodes
iface.showNodes()
out, err = capsys.readouterr()
# Check that the output contains the "Fav" column
assert "Fav" in out
# Find lines containing our nodes
lines = out.split('\n')
favorite_line = None
regular_line = None
legacy_line = None
for line in lines:
if "Favorite Node" in line or "FAV1" in line:
favorite_line = line
if "Regular Node" in line or "REG1" in line:
regular_line = line
if "Legacy Node" in line or "LEG1" in line:
legacy_line = line
# Verify all nodes are present in the output
assert favorite_line is not None, "Favorite node should be in output"
assert regular_line is not None, "Regular node should be in output"
assert legacy_line is not None, "Legacy node should be in output"
# Verify the favorite node has an asterisk in its row
assert "*" in favorite_line, "Favorite node should have an asterisk"
# Verify the regular (non-favorite) node does NOT have an asterisk
assert regular_line.count("*") == 0, "Non-favorite node should not have an asterisk"
# Verify the legacy node (without isFavorite field) does NOT have an asterisk
assert legacy_line.count("*") == 0, "Legacy node without isFavorite field should not have an asterisk"
assert err == ""
@pytest.mark.unit
def test_showNodes_favorite_field_formatting():
"""Test the formatting logic for isFavorite field"""
# Test favorite node
raw_value = True
formatted_value = "*" if raw_value else ""
assert formatted_value == "*"
# Test non-favorite node
raw_value = False
formatted_value = "*" if raw_value else ""
assert formatted_value == ""
# Test None/missing value
raw_value = None
formatted_value = "*" if raw_value else ""
assert formatted_value == ""
@pytest.mark.unit
def test_showNodes_with_custom_fields_including_favorite(capsys, _iface_with_favorite_nodes):
"""Test that isFavorite can be specified in custom showFields"""
iface = _iface_with_favorite_nodes
custom_fields = ["user.longName", "isFavorite"]
iface.showNodes(showFields=custom_fields)
out, err = capsys.readouterr()
# Should still show the Fav column when explicitly requested
assert "Fav" in out
assert err == ""
@pytest.mark.unit
def test_showNodes_default_fields_includes_favorite(_iface_with_favorite_nodes):
"""Test that isFavorite is included in default fields"""
iface = _iface_with_favorite_nodes
# Call showNodes which uses default fields
result = iface.showNodes()
# The result should contain the formatted table as a string
assert "Fav" in result
@pytest.mark.unit
def test_showNodes_backward_compatibility_missing_field(capsys, _iface_with_favorite_nodes):
"""Test that nodes without isFavorite field are handled gracefully"""
iface = _iface_with_favorite_nodes
iface.showNodes()
out, err = capsys.readouterr()
# Find the legacy node line
lines = out.split('\n')
legacy_line = None
for line in lines:
if "Legacy Node" in line or "LEG1" in line:
legacy_line = line
break
# Verify the legacy node appears in output
assert legacy_line is not None, "Legacy node without isFavorite field should appear in output"
# Verify it doesn't have an asterisk (should be treated as non-favorite)
assert legacy_line.count("*") == 0, "Legacy node should not have asterisk (treated as non-favorite)"
assert err == ""

View File

@@ -15,7 +15,115 @@ def test_StreamInterface():
"""Test that we cannot instantiate a StreamInterface based on noProto"""
with pytest.raises(Exception) as pytest_wrapped_e:
StreamInterface()
assert pytest_wrapped_e.type == Exception
assert pytest_wrapped_e.type == RuntimeError
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_StreamInterface_close_safe_when_thread_never_started():
"""close() must not raise RuntimeError when called before connect() has started the reader.
Hits the cleanup path used by __init__ when the handshake raises before the
reader thread is started.
"""
iface = StreamInterface(noProto=True, connectNow=False)
iface.stream = MagicMock()
# _rxThread was created in __init__ but never .start()'d. close() should
# detect that and skip join() instead of raising RuntimeError.
iface.close()
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_StreamInterface_close_when_thread_never_started_closes_stream():
"""If no reader thread was started, close() should still close the stream."""
iface = StreamInterface(noProto=True, connectNow=False)
stream = MagicMock()
iface.stream = stream
iface.close()
stream.close.assert_called_once()
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_StreamInterface_init_cleans_up_when_connect_raises():
"""If connect() raises during __init__, close() runs and the original exception propagates."""
cleanup_calls = []
class FailingConnectStream(StreamInterface):
"""Subclass whose connect() raises, to exercise the __init__ cleanup path."""
def __init__(self):
self.stream = MagicMock() # bypass StreamInterface abstract check
super().__init__(noProto=False, connectNow=True)
def connect(self):
raise RuntimeError("simulated handshake failure")
def close(self):
cleanup_calls.append("close")
super().close()
with pytest.raises(RuntimeError, match="simulated handshake failure"):
FailingConnectStream()
assert cleanup_calls == ["close"], "close() should be invoked exactly once on handshake failure"
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_StreamInterface_init_cleans_up_when_waitForConfig_raises():
"""If waitForConfig() raises after a successful connect(), close() runs and exception propagates."""
cleanup_calls = []
class FailingWaitStream(StreamInterface):
"""Subclass whose waitForConfig() raises, to exercise the second leg of cleanup."""
def __init__(self):
self.stream = MagicMock()
super().__init__(noProto=False, connectNow=True)
def connect(self):
# No-op connect — we are simulating handshake-stage failure, not connect-stage.
pass
def waitForConfig(self):
raise TimeoutError("simulated config-handshake timeout")
def close(self):
cleanup_calls.append("close")
super().close()
with pytest.raises(TimeoutError, match="simulated config-handshake timeout"):
FailingWaitStream()
assert cleanup_calls == ["close"], "close() should be invoked exactly once on handshake timeout"
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_StreamInterface_init_cleanup_does_not_shadow_original_exception():
"""If close() itself raises during __init__ cleanup, the original exception still propagates.
The cleanup uses contextlib.suppress(Exception) so that a secondary failure
in close() doesn't replace the real reason for the failed handshake.
"""
class CleanupRaisesStream(StreamInterface):
"""Helper stream that raises during close() to verify exception precedence."""
def __init__(self):
self.stream = MagicMock()
super().__init__(noProto=False, connectNow=True)
def connect(self):
raise RuntimeError("original handshake failure")
def close(self):
raise RuntimeError("secondary close failure — should be suppressed")
with pytest.raises(RuntimeError, match="original handshake failure"):
CleanupRaisesStream()
# Note: This takes a bit, so moving from unit to slow

Some files were not shown because too many files have changed in this diff Show More