Compare commits

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

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

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

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

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

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

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

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

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

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

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

Fix: wrap connect() + waitForConfig() in try/except; call self.close() on
any exception before re-raising. Also guard close() against RuntimeError
from joining an unstarted reader thread (happens when close() runs from
a failed __init__ before connect() could spawn it).

Discovered while debugging a real-world Meshtastic firmware crash where
a passive logger's retrying TCPInterface() calls against a node with
250-entry NodeDB produced a reconnect storm — every retry triggered a
full config+NodeDB dump on the node, compounding heap pressure, which
then exposed null-deref bugs in Router::perhapsDecode / MeshService
(firmware side fixed in meshtastic/firmware#10226 and #10229). The
client-side leak is independent of those firmware bugs and worth fixing
on its own.
2026-04-25 15:02:38 -05:00
Ben Meadors c5fc51ea37 Update factory reset to use integer for config reset 2026-04-17 05:03:56 -05:00
Ian McEwen cec79a7c1f poetry lock 2026-04-10 11:42:58 -07:00
Ian McEwen 6805fee667 loosen packaging version requirement 2026-04-10 11:41:29 -07:00
Ian McEwen 3cb9ce2a56 protobufs: v2.7.21 2026-04-10 11:40:56 -07:00
Aron Tkachuk b05a6a6017 Added textchat.py and replymessage.py example scripts from TODO 2026-04-05 18:23:02 -05:00
Benjamin BabeshkinandClaude Opus 4.6 8a842ed82b Fix local node position overwrite by low-precision echoes
When other nodes relay our position via map reports, they send it at
reduced precision (e.g., 13 bits). _onPositionReceive() was blindly
overwriting our locally-stored high-precision GPS position (32 bits)
with these degraded echoes.

The fix only protects the local node's position — since we have the
GPS internally, any lower-precision update is always an echo from the
mesh, never fresh data. Remote node positions are still updated
normally, as any position they broadcast reflects their current state.

Fixes #910

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

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

LocalConfig.security was introduced in https://github.com/meshtastic/protobufs/pull/553
2026-02-27 23:27:52 -05:00
Clive Blackledge d5eaecea07 Add traffic management unit tests 2026-02-25 01:47:53 -08:00
Clive Blackledge 545c3ab192 Add traffic management module to the config. 2026-02-25 01:47:53 -08:00
github-actions fee2b6ccbd Update protobufs 2026-02-14 16:48:14 +00:00
Ben Meadors d0ccb1a597 Update protobufs GH action 2026-02-14 10:47:14 -06:00
Sergio Conde 5721859314 fix: cleanup imports in tests 2026-02-14 15:05:11 +01:00
Stephen Thorne 07172f88f3 Give TCPInterface reconnect logic on write errors
* Moving to socket.sendall() is safer, as sendall will send the entire
   buffer, while send() would return the number of bytes sent and
   require being called multiple times if the buffer was full.
 * On exceptions: reconnect to the server.
 * On reconnection: make sure using a lock that there isn't a race
   between the readers and the writers triggering a reconnect.
2026-02-05 22:46:40 +01:00
dim5x 3ea1994cf5 Refactor the Meshtastic TCP pub/sub example to ensure proper resource cleanup and clearer exception handling. 2026-02-05 13:42:37 +03:00
Sergio Conde 4de19f50d9 fix: add tests 2026-02-01 21:25:49 +01:00
Sergio Conde 4d8430d360 fix: throw propper exceptions and cleanup code 2026-02-01 11:39:09 +01:00
Ben MeadorsandCopilot bf580c36ae Update meshtastic/__main__.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-31 18:15:25 -06:00
Sergio Conde 1eb13c9953 feat: Add ESP32 WiFi Unified OTA update support 2026-01-31 16:44:03 +01:00
Aleksei SviridkinandClaude 1fc2407c5e fix(cli): add timeout error handling for serial connections
Handle MeshInterface.MeshInterfaceError when device is rebooting
or connection times out, with user-friendly error message.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-01-26 11:07:16 +03:00
Aleksei SviridkinandClaude 25ffd25fda fix(ble): handle BLEError with user-friendly messages
Replace raw tracebacks with helpful error messages that explain:
- What went wrong
- Possible causes
- How to fix it

Covers all BLEError cases:
- Device not found (BLE disabled, sleep mode, out of range)
- Multiple devices found (need to specify which one)
- Write errors (pairing PIN, Linux bluetooth group)
- Read errors (device disconnected)

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-01-26 11:07:13 +03:00
Ian McEwen cdf893e618 Merge pull request #893 from lexfrei/fix/ble-race-condition
fix(ble): resolve BLE connection hangs on macOS without --debug flag
2026-01-25 15:24:06 -07:00
Aleksei SviridkinandClaude 9b9df9e585 fix(ble): resolve BLE hangs on macOS without --debug flag
This fixes two issues that caused BLE connections to hang on macOS
when not using the --debug flag:

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-01-25 03:06:58 +03:00
github-actions b26d80f186 bump version to 2.7.7 2026-01-24 16:34:15 +00:00
Ian McEwen da559f0e37 protobufs: v2.7.18 2026-01-24 09:31:04 -07:00
Ian McEwen a73f432f42 Merge pull request #888 from dandrzejewski/node-favorites
Show favorite nodes in --nodes
2026-01-09 13:09:31 -07:00
David Andrzejewski c3c5ce64dd Copilot had a few suggestions on code review, implemented them. 2026-01-08 19:08:18 -05:00
David Andrzejewski 683dd23d63 Fix a few pylint things. 2026-01-08 18:53:55 -05:00
David Andrzejewski 57d43b84e4 Merge branch 'master' into node-favorites 2026-01-08 18:40:43 -05:00
David Andrzejewski 4f6d183ed1 Show favorite nodes in --nodes 2026-01-08 18:13:03 -05:00
github-actions 9a72e36ca6 bump version to 2.7.6 2026-01-05 02:25:14 +00:00
Ian McEwen 4d54ee7431 protobufs: v2.7.17 2026-01-04 19:21:54 -07:00
Rob c8b1b8ea6f Filter --reply based on specified channel index
Ensures that automatic replies are sent back on the same channel index the message was received on. Previously, all replies defaulted to the primary channel (0), even if the incoming message arrived on a secondary channel. Additionally it ensures incoming messages match the specified channel index.

E.g:  meshtastic --ch-index 1 --reply .

Modified the `onReceive` handler to extract the `channel` index from received packets. This ensures `interface.sendText` targets the originating channel rather than always defaulting to the primary channel. Added a filter to ensure that only the specified channel index is being replied to.
2025-12-26 15:17:46 -05:00
Lluís Batlle i Rossell d9057c0aaf Fix traceroute timeout for case of 0-hops
It was not waiting any time.
2025-11-29 15:40:33 +01:00
Olliver Schinagl 77db30125a Container: Add initial container for meshtastic-cli
Just a quick set of files to enable the build of (tagged) containers.
Both alpine and debian containers are available (~200MiB/~1.2GiB)
allowing us to use meshtastic cli with a quick docker run, instead of
having to build/install stuff locally.

Signed-off-by: Olliver Schinagl <oliver@schinagl.nl>
2025-11-21 16:16:04 +01:00
Ian McEwen 5cc0dae394 protobufs: v2.7.15 2025-11-20 14:30:39 -07:00
Ian McEwen 1a50377d34 Merge pull request #865 from ianmcorvidae/device-metadata-details
Add a bunch more detail to --device-metadata output, from fields we weren't formerly using in the output.
2025-11-20 14:29:20 -07:00
Ian McEwen 2a44be9269 Add a bunch more detail to --device-metadata output, from fields we weren't formerly using in the output. 2025-11-20 11:42:57 -07:00
Travis-L-R 1214d5010b Linting adjustment for change to StreamInterface non-abstract use check 2025-11-13 07:38:25 +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
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
henri acff793f28 Updates and bug fixes meshtastic_serial_message_reader.py 2025-02-20 08:44:32 +13:00
henri 53b58500f7 Added example script : meshtastic_serial_message_reader.py 2025-02-19 17:48:48 +13:00
100 changed files with 16291 additions and 1857 deletions

No files matched your search

+16
View File
@@ -0,0 +1,16 @@
language: en-US
reviews:
profile: chill
high_level_summary: true
poem: false
auto_review:
enabled: true
drafts: false
ignore_usernames:
- renovate
- renovate[bot]
abort_on_close: true
path_filters:
- "!/protobufs/**"
- "!/meshtastic/protobuf/**"
+11
View File
@@ -0,0 +1,11 @@
*
!Containerfile*
!Dockerfile
!README.md
!LICENSE.md
!bin/container-entrypoint.sh
!examples/
!extra/
!meshtastic/
!poetry.lock
!pyproject.toml
+206
View File
@@ -0,0 +1,206 @@
# Copilot Instructions for Meshtastic Python
## Project Overview
This is the Meshtastic Python library and CLI - a Python API for interacting with Meshtastic mesh radio devices. It supports communication via Serial, TCP, and BLE interfaces.
## Technology Stack
- **Language**: Python 3.9 - 3.14
- **Package Manager**: Poetry
- **Testing**: pytest with hypothesis for property-based testing
- **Linting**: pylint
- **Type Checking**: mypy (working toward strict mode)
- **Documentation**: pdoc3
- **License**: GPL-3.0
## Project Structure
```
meshtastic/ # Main library package
├── __init__.py # Core interface classes and pub/sub topics
├── __main__.py # CLI entry point
├── mesh_interface.py # Base interface class for all connection types
├── serial_interface.py
├── tcp_interface.py
├── ble_interface.py
├── node.py # Node representation and configuration
├── protobuf/ # Generated Protocol Buffer files (*_pb2.py, *_pb2.pyi)
├── tests/ # Unit and integration tests
├── powermon/ # Power monitoring tools
└── analysis/ # Data analysis tools
examples/ # Usage examples
protobufs/ # Protocol Buffer source definitions
```
## Coding Standards
### Style Guidelines
- Follow PEP 8 style conventions
- Use type hints for function parameters and return values
- Document public functions and classes with docstrings
- Prefer explicit imports over wildcard imports
- Use `logging` module instead of print statements for debug output
### Type Annotations
- Add type hints to all new code
- Use `Optional[T]` for nullable types
- Use `Dict`, `List`, `Tuple` from `typing` module for Python 3.9 compatibility
- Protobuf types are in `meshtastic.protobuf.*_pb2` modules
### Naming Conventions
- Classes: `PascalCase` (e.g., `MeshInterface`, `SerialInterface`)
- Functions/methods: `camelCase` for public API (e.g., `sendText`, `sendData`)
- Internal functions: `snake_case` with leading underscore (e.g., `_send_packet`)
- Constants: `UPPER_SNAKE_CASE` (e.g., `BROADCAST_ADDR`, `LOCAL_ADDR`)
### Error Handling
- Use custom exception classes when appropriate (e.g., `MeshInterface.MeshInterfaceError`)
- Provide meaningful error messages
- Use `our_exit()` from `meshtastic.util` for CLI exits with error codes
## Testing
### Test Organization
Tests are in `meshtastic/tests/` and use pytest markers:
- `@pytest.mark.unit` - Fast unit tests (default)
- `@pytest.mark.unitslow` - Slower unit tests
- `@pytest.mark.int` - Integration tests
- `@pytest.mark.smoke1` - Single device smoke tests
- `@pytest.mark.smoke2` - Two device smoke tests
- `@pytest.mark.smokevirt` - Virtual device smoke tests
- `@pytest.mark.examples` - Example validation tests
### Running Tests
```bash
# Run unit tests only (default)
make test
# or
pytest -m unit
# Run all tests
pytest
# Run with coverage
make cov
```
### Writing Tests
- Use `pytest` fixtures from `conftest.py`
- Use `hypothesis` for property-based testing where appropriate
- Mock external dependencies (serial ports, network connections)
- Test file naming: `test_<module_name>.py`
## Pub/Sub Events
The library uses pypubsub for event handling. Key topics:
- `meshtastic.connection.established` - Connection successful
- `meshtastic.connection.lost` - Connection lost
- `meshtastic.receive.text(packet)` - Text message received
- `meshtastic.receive.position(packet)` - Position update received
- `meshtastic.receive.data.portnum(packet)` - Data packet by port number
- `meshtastic.node.updated(node)` - Node database changed
- `meshtastic.log.line(line)` - Raw log line from device
## Protocol Buffers
- Protobuf definitions are in `protobufs/meshtastic/`
- Generated Python files are in `meshtastic/protobuf/`
- Never edit `*_pb2.py` or `*_pb2.pyi` files directly
- Regenerate with: `make protobufs` or `./bin/regen-protobufs.sh`
## Common Patterns
### Creating an Interface
```python
import meshtastic.serial_interface
# Auto-detect device
iface = meshtastic.serial_interface.SerialInterface()
# Specific device
iface = meshtastic.serial_interface.SerialInterface(devPath="/dev/ttyUSB0")
# Always close when done
iface.close()
# Or use context manager
with meshtastic.serial_interface.SerialInterface() as iface:
iface.sendText("Hello mesh")
```
### Sending Messages
```python
# Text message (broadcast)
iface.sendText("Hello")
# Text message to specific node
iface.sendText("Hello", destinationId="!abcd1234")
# Binary data
iface.sendData(data, portNum=portnums_pb2.PRIVATE_APP)
```
### Subscribing to Events
```python
from pubsub import pub
def on_receive(packet, interface):
print(f"Received: {packet}")
pub.subscribe(on_receive, "meshtastic.receive")
```
## Development Workflow
1. Install dependencies: `poetry install --all-extras --with dev`
2. Make changes
3. Run linting: `poetry run pylint meshtastic examples/`
4. Run type checking: `poetry run mypy meshtastic/`
5. Run tests: `poetry run pytest -m unit`
6. Update documentation if needed
## CLI Development
The CLI is in `meshtastic/__main__.py`. When adding new CLI commands:
- Use argparse for argument parsing
- Support `--dest` for specifying target node
- Provide `--help` documentation
- Handle errors gracefully with meaningful messages
## Dependencies
### Required
- `pyserial` - Serial port communication
- `protobuf` - Protocol Buffers
- `pypubsub` - Pub/sub messaging
- `bleak` - BLE communication
- `tabulate` - Table formatting
- `pyyaml` - YAML config support
- `requests` - HTTP requests
### Optional (extras)
- `cli` extra: `pyqrcode`, `print-color`, `dotmap`, `argcomplete`
- `tunnel` extra: `pytap2`
- `analysis` extra: `dash`, `pandas`
## Important Notes
- Always test with actual Meshtastic hardware when possible
- Be mindful of radio regulations in your region
- The nodedb (`interface.nodes`) is read-only
- Packet IDs are random 32-bit integers
- Default timeout is 300 seconds for operations
+54 -17
View File
@@ -1,4 +1,7 @@
name: CI
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
branches:
@@ -8,7 +11,29 @@ on:
- master
jobs:
validate:
runs-on: ubuntu-latest
strategy:
matrix:
python-version:
- "3.12"
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Install Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install meshtastic from local
run: |
python -m pip install --upgrade pip
pip3 install poetry
poetry install --all-extras --with dev,powermon
poetry run meshtastic --version
build:
needs: validate
runs-on: ubuntu-latest
strategy:
matrix:
@@ -17,10 +42,14 @@ jobs:
- "3.10"
- "3.11"
- "3.12"
- "3.13"
- "3.14"
steps:
- uses: actions/checkout@v4
- name: Install Python 3
- name: Install Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Uninstall meshtastic
run: |
pip3 uninstall -y meshtastic
@@ -32,15 +61,8 @@ jobs:
run: |
poetry install --all-extras --with dev,powermon
poetry run meshtastic --version
- name: Run pylint
run: poetry run pylint meshtastic examples/ --ignore-patterns ".*_pb2.pyi?$"
- name: Check types with mypy
run: poetry run mypy meshtastic/
- name: Run tests with pytest
run: poetry run pytest --cov=meshtastic
- name: Generate coverage report
run: |
poetry run pytest --cov=meshtastic --cov-report=xml
- name: Run lint, type check, and tests in parallel
run: make -j3 --output-sync=target ci
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
@@ -51,22 +73,37 @@ jobs:
name: codecov-umbrella
fail_ci_if_error: true
verbose: true
validate:
simradio_testing:
needs: [validate, build]
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version:
- "3.9"
- "3.10"
- "3.11"
- "3.12"
channel:
- beta
- alpha
- daily
continue-on-error: ${{ matrix.channel == 'daily' }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Install Python 3
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install meshtastic from local
run: |
python -m pip install --upgrade pip
pip3 install poetry
poetry install
poetry install --all-extras --with dev
poetry run meshtastic --version
- name: Install meshtasticd (${{ matrix.channel }}) from PPA
run: |
sudo add-apt-repository -y ppa:meshtastic/${{ matrix.channel }}
sudo apt-get update
sudo apt-get install -y meshtasticd
- name: Run firmware smoke tests
run: poetry run pytest -m "smokevirt or smokemesh" -v
timeout-minutes: 15
+79
View File
@@ -0,0 +1,79 @@
name: Create and publish Container image
on:
push:
branches:
- master
tags:
- '*.*.*'
pull_request:
branches:
- master
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-push-image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
strategy:
matrix:
include:
- container: Containerfile.debian
autotag: false
suffix: -debian
- container: Containerfile.alpine
autotag: ${{ github.ref == 'refs/heads/master' && 'true' || 'auto' }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Container registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=edge
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
flavor: |
latest=${{ matrix.autotag }}
suffix=${{ matrix.suffix }}
- name: Build and push
uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64
context: .
file: ${{ matrix.container }}
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
+36
View File
@@ -0,0 +1,36 @@
name: Daily SimRadio Tests
on:
schedule:
- cron: 0 6 * * *
workflow_dispatch:
permissions:
contents: read
jobs:
simradio_testing:
runs-on: ubuntu-latest
if: github.repository == 'meshtastic/python'
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Install Python 3
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install meshtastic from local
run: |
python -m pip install --upgrade pip
pip3 install poetry
poetry install --all-extras --with dev
poetry run meshtastic --version
- name: Install meshtasticd (daily) from PPA
run: |
sudo add-apt-repository -y ppa:meshtastic/daily
sudo apt-get update
sudo apt-get install -y meshtasticd
- name: Run firmware smoke tests
run: poetry run pytest -m "smokevirt or smokemesh" -v
timeout-minutes: 15
+74 -9
View File
@@ -1,5 +1,18 @@
name: "Update protobufs"
on: workflow_dispatch
on:
workflow_dispatch:
inputs:
version:
description: "Protobufs version tag to sync to (e.g., v2.7.26). Defaults to the latest protobufs release."
required: false
default: ""
type: string
repository_dispatch:
types: [protobufs-release]
permissions:
contents: write
jobs:
update-protobufs:
@@ -9,33 +22,85 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: true
persist-credentials: false
- name: Update Submodule
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install Poetry
run: |
git pull --recurse-submodules
git submodule update --remote --recursive
python -m pip install --upgrade pip
python -m pip install poetry
- name: Determine target protobufs version
id: version
env:
INPUT_VERSION: ${{ github.event.inputs.version }}
PAYLOAD_VERSION: ${{ github.event.client_payload.version }}
run: |
cd protobufs
git fetch --tags
if [ -n "${INPUT_VERSION}" ]; then
target="${INPUT_VERSION}"
elif [ -n "${PAYLOAD_VERSION}" ]; then
target="${PAYLOAD_VERSION}"
else
target=$(git tag --list 'v*' --sort=-version:refname | head -n1)
if [ -z "${target}" ]; then
echo "Error: no release tags found in meshtastic/protobufs"
exit 1
fi
fi
if ! git show-ref --verify --quiet "refs/tags/${target}"; then
echo "Error: '${target}' is not a valid tag in meshtastic/protobufs"
exit 1
fi
printf 'version=%s\n' "${target}" >> "$GITHUB_OUTPUT"
echo "Target protobufs version: ${target}"
- name: Update protobuf submodule to release
env:
TARGET_VERSION: ${{ steps.version.outputs.version }}
run: |
git submodule sync --recursive
git submodule update --init --recursive
cd protobufs
git checkout "${TARGET_VERSION}"
cd ..
- name: Download nanopb
run: |
wget https://jpa.kapsi.fi/nanopb/download/nanopb-0.4.8-linux-x86.tar.gz
curl -L -o nanopb-0.4.8-linux-x86.tar.gz https://jpa.kapsi.fi/nanopb/download/nanopb-0.4.8-linux-x86.tar.gz
tar xvzf nanopb-0.4.8-linux-x86.tar.gz
mv nanopb-0.4.8-linux-x86 nanopb-0.4.8
- name: Install poetry (needed by regen-protobufs.sh)
- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
pip3 install poetry
poetry install --with dev
- name: Re-generate protocol buffers
run: |
./bin/regen-protobufs.sh
- name: Commit update
env:
TARGET_VERSION: ${{ steps.version.outputs.version }}
run: |
git config --global user.name 'github-actions'
git config --global user.email 'bot@noreply.github.com'
git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}
git add protobufs
git add meshtastic/protobuf
git commit -m "Update protobuf submodule" && git push || echo "No changes to commit"
if [[ -n "$(git status --porcelain)" ]]; then
git commit -m "protobufs: ${TARGET_VERSION}"
git push
else
echo "No changes to commit"
fi
+1
View File
@@ -0,0 +1 @@
Containerfile.alpine
+40
View File
@@ -0,0 +1,40 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# Copyright (C) 2025 Olliver Schinagl <oliver@schinagl.nl>
ARG TARGET_VERSION="3.11-alpine"
ARG TARGET_ARCH="library"
FROM docker.io/${TARGET_ARCH}/python:${TARGET_VERSION} AS builder
WORKDIR /tmp/build
COPY pyproject.toml poetry.lock /tmp/build/
RUN apk add --no-cache --virtual .build-deps build-base libffi-dev && \
pip install --no-cache-dir 'poetry==2.4.1' && \
poetry config virtualenvs.create false && \
poetry install --without dev --extras cli --extras tunnel --no-interaction --no-ansi --no-root
COPY . /tmp/build/
RUN poetry build --format wheel --no-interaction
FROM docker.io/${TARGET_ARCH}/python:${TARGET_VERSION}
RUN apk add --no-cache libffi && \
addgroup -S meshtastic && \
adduser -S -G meshtastic -h /home/meshtastic meshtastic
COPY --from=builder /tmp/build/dist/*.whl /tmp/
RUN wheel=$(echo /tmp/meshtastic-*.whl) && pip install --no-cache-dir "${wheel}[cli,tunnel]" && \
rm -f /tmp/meshtastic-*.whl
COPY "./bin/container-entrypoint.sh" "/init"
RUN chmod 0755 /init
WORKDIR /home/meshtastic
USER meshtastic
ENTRYPOINT [ "/init" ]
+37
View File
@@ -0,0 +1,37 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# Copyright (C) 2025 Olliver Schinagl <oliver@schinagl.nl>
ARG TARGET_VERSION="3.11"
ARG TARGET_ARCH="library"
FROM docker.io/${TARGET_ARCH}/python:${TARGET_VERSION} AS builder
WORKDIR /tmp/build
COPY pyproject.toml poetry.lock /tmp/build/
RUN pip install --no-cache-dir 'poetry==2.4.1' && \
poetry config virtualenvs.create false && \
poetry install --without dev --extras cli --extras tunnel --no-interaction --no-ansi --no-root
COPY . /tmp/build/
RUN poetry build --format wheel --no-interaction
FROM docker.io/${TARGET_ARCH}/python:${TARGET_VERSION}
RUN useradd --system --create-home --home-dir /home/meshtastic meshtastic
COPY --from=builder /tmp/build/dist/*.whl /tmp/
RUN wheel=$(echo /tmp/meshtastic-*.whl) && pip install --no-cache-dir "${wheel}[cli,tunnel]" && \
rm -f /tmp/meshtastic-*.whl
COPY "./bin/container-entrypoint.sh" "/init"
RUN chmod 0755 /init
WORKDIR /home/meshtastic
USER meshtastic
ENTRYPOINT [ "/init" ]
Symlink
+1
View File
@@ -0,0 +1 @@
Containerfile
+14
View File
@@ -42,5 +42,19 @@ cov:
examples: FORCE
pytest -mexamples
# CI targets (run via poetry, executed in parallel by the CI workflow)
.PHONY: ci-pylint ci-mypy ci-test ci
ci-pylint:
poetry run pylint meshtastic examples/ --ignore-patterns ".*_pb2.pyi?$$"
ci-mypy:
poetry run mypy meshtastic/
ci-test:
poetry run pytest --cov=meshtastic --cov-report=xml
ci: ci-pylint ci-mypy ci-test
# Makefile hack to get the examples to always run
FORCE: ;
+18
View File
@@ -27,6 +27,24 @@ This small library (and example application) provides an easy API for sending an
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.
## Container usage
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
This library and CLI has gone without a consistent maintainer for a while, and there's many improvements that could be made. We're all volunteers here and help is extremely appreciated, whether in implementing your own needs or helping maintain the library and CLI in general.
+24
View File
@@ -0,0 +1,24 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-3.0-or-later
#
# Copyright (C) 2025 Olliver Schinagl <oliver@schinagl.nl>
#
# A beginning user should be able to docker run image bash (or sh) without
# needing to learn about --entrypoint
# https://github.com/docker-library/official-images#consistency
set -eu
bin='meshtastic'
# run command if it is not starting with a "-" and is an executable in PATH
if [ "${#}" -le 0 ] || \
[ "${1#-}" != "${1}" ] || \
[ -d "${1}" ] || \
! command -v "${1}" > '/dev/null' 2>&1; then
entrypoint='true'
fi
exec ${entrypoint:+${bin:?}} "${@}"
exit 0
+344
View File
@@ -0,0 +1,344 @@
#!/usr/bin/env python3
"""Inject nanopb .options constraints as inline field options into a .proto file.
The nanopb .options file format is specific to the nanopb C generator and is
ignored by standard protoc (including --python_out). By injecting the options
directly into the proto file's field declarations, protoc will embed them in
the serialized descriptor, making them accessible in Python via:
from meshtastic.protobuf import mesh_pb2, nanopb_pb2
field = mesh_pb2.DESCRIPTOR.message_types_by_name['User'].fields_by_name['long_name']
opts = field.GetOptions().Extensions[nanopb_pb2.nanopb]
print(opts.max_size) # 40
Usage:
inject_nanopb_options.py <options_file> <proto_file>
The proto_file is modified in-place. Intended to operate on temporary copies
generated by regen-protobufs.sh, not the source .proto files.
"""
import re
import sys
from pathlib import Path
from typing import Any, Dict, List, Tuple
# IntSize enum values from nanopb.proto
INT_SIZE_ENUM = {8: "IS_8", 16: "IS_16", 32: "IS_32", 64: "IS_64"}
# FieldType enum names from nanopb.proto. Only FT_IGNORE carries meaning for a
# Python client (the firmware omits the field entirely); the rest describe C
# storage class, but are passed through so the descriptor mirrors the .options.
FIELD_TYPE_ENUM = frozenset(
{"FT_DEFAULT", "FT_CALLBACK", "FT_POINTER", "FT_STATIC", "FT_IGNORE", "FT_INLINE"}
)
# Options that are valid proto FieldOptions and useful outside of C code generation.
# We skip C-only options (anonymous_oneof, no_unions, skip_message, packed_struct,
# packed_enum, mangle_names, callback_datatype, callback_function, descriptorsize,
# type_override) since they either don't apply to proto fields or are C-specific.
FIELD_OPTIONS = frozenset(
{
"max_size",
"max_length",
"max_count",
"int_size",
"type",
"fixed_length",
"fixed_count",
"long_names",
"proto3",
"default_has",
"sort_by_tag",
"msgid",
}
)
def parse_value(s: str) -> Any:
"""Convert an option value string to an appropriate Python type."""
if re.fullmatch(r"-?[0-9]+", s):
return int(s)
if s.lower() == "true":
return True
if s.lower() == "false":
return False
return s
def parse_options_file(
path: Path,
) -> Tuple[Dict[Tuple[str, ...], Dict[str, Any]], Dict[str, Dict[str, Any]]]:
"""Parse a nanopb .options file.
Returns:
specific: maps (message_path..., field_name) -> {option: value}
e.g. ('User', 'long_name') or ('Route', 'Link', 'uid')
wildcard: maps field_name -> {option: value}
applies to any field with this name in the same proto file
"""
specific: Dict[Tuple[str, ...], Dict[str, Any]] = {}
wildcard: Dict[str, Dict[str, Any]] = {}
with open(path, encoding="utf-8") as f:
for line in f:
# Strip inline comments
comment_pos = line.find("#")
if comment_pos >= 0:
line = line[:comment_pos]
line = line.strip().lstrip("*").strip()
if not line:
continue
tokens = line.split()
if len(tokens) < 2:
continue
pattern = tokens[0]
opts: Dict[str, Any] = {}
for tok in tokens[1:]:
if ":" in tok:
k, v = tok.split(":", 1)
if k in FIELD_OPTIONS:
opts[k] = parse_value(v)
if not opts:
continue
if "." in pattern:
# e.g. "User.long_name" -> key=('User', 'long_name')
# or "Route.Link.uid" -> key=('Route', 'Link', 'uid')
parts = tuple(pattern.split("."))
if parts in specific:
specific[parts].update(opts)
else:
specific[parts] = opts
else:
# wildcard: applies to any field with this name
if pattern in wildcard:
wildcard[pattern].update(opts)
else:
wildcard[pattern] = opts
return specific, wildcard
def format_nanopb_opts(opts: Dict[str, Any]) -> str:
"""Format a dict of nanopb options as a proto field options annotation string."""
parts = []
for k, v in opts.items():
if k == "int_size":
enum_val = INT_SIZE_ENUM.get(v, f"IS_{v}")
parts.append(f"(nanopb).int_size = {enum_val}")
elif k == "type":
if v not in FIELD_TYPE_ENUM:
raise ValueError(f"unknown nanopb field type {v!r}")
parts.append(f"(nanopb).type = {v}")
elif isinstance(v, bool):
parts.append(f"(nanopb).{k} = {'true' if v else 'false'}")
else:
parts.append(f"(nanopb).{k} = {v}")
return ", ".join(parts)
def message_path_matches(
context_stack: List[Tuple[str, str]], msg_path: Tuple[str, ...]
) -> bool:
"""Check if the current message context ends with msg_path.
context_stack entries are ('message'|'oneof'|'enum', name).
msg_path is the tuple of message names from the options pattern,
e.g. ('User',) or ('Route', 'Link').
"""
msg_names = [name for kind, name in context_stack if kind == "message"]
n = len(msg_path)
return len(msg_names) >= n and tuple(msg_names[-n:]) == msg_path
def inject_into_proto(
content: str,
specific: Dict[Tuple[str, ...], Dict[str, Any]],
wildcard: Dict[str, Dict[str, Any]],
nanopb_import_path: str,
) -> str:
"""Inject nanopb field options into a proto file's text content.
Adds an import for nanopb.proto if not already present.
Returns the modified content.
"""
if not specific and not wildcard:
return content
lines = content.split("\n")
# Check if nanopb is already imported (after sed fixup, it will be
# 'meshtastic/protobuf/nanopb.proto')
nanopb_already_imported = any(
"nanopb.proto" in line
for line in lines
if line.strip().startswith("import")
)
# Track the index of the last import line so we can insert after it
last_import_idx = max(
(
i
for i, line in enumerate(lines)
if line.strip().startswith("import ") and line.strip().endswith(";")
),
default=-1,
)
# State
context_stack: List[Tuple[str, str]] = [] # ('message'|'oneof'|'enum', name)
result: List[str] = []
import_added = nanopb_already_imported
# Patterns for proto structural elements
message_re = re.compile(r"^(\s*)message\s+(\w+)\s*\{")
oneof_re = re.compile(r"^(\s*)oneof\s+(\w+)\s*\{")
enum_re = re.compile(r"^(\s*)enum\s+(\w+)\s*\{")
close_re = re.compile(r"^\s*\}")
# Pattern for field declarations:
# indent [optional|repeated] type name = number [options] ;
# We exclude map<> fields (different syntax, nanopb handles them differently)
# and enum value lines (no type keyword before the name).
field_re = re.compile(
r"^(\s*)" # (1) indent
r"(optional\s+|repeated\s+)?" # (2) optional qualifier
r"([\w.]+)\s+" # (3) field type (possibly qualified like google.protobuf.Any)
r"(\w+)\s*" # (4) field name
r"=\s*(\d+)" # (5) field number
r"(?:\s*\[([^\]]*)\])?" # (6) existing options, without brackets
r"\s*;" # trailing semicolon
)
for i, line in enumerate(lines):
# Insert nanopb import right after the last existing import line.
# Only do this when there IS an existing import (last_import_idx >= 0);
# if there are no imports we fall through to the syntax-line fallback below.
if not import_added and last_import_idx >= 0 and i == last_import_idx + 1:
result.append(f'import "{nanopb_import_path}";')
import_added = True
# --- Track message/oneof/enum nesting ---
m = message_re.match(line)
if m:
context_stack.append(("message", m.group(2)))
result.append(line)
continue
m = oneof_re.match(line)
if m:
context_stack.append(("oneof", m.group(2)))
result.append(line)
continue
m = enum_re.match(line)
if m:
context_stack.append(("enum", m.group(2)))
result.append(line)
continue
if close_re.match(line) and context_stack:
context_stack.pop()
result.append(line)
continue
# Skip field injection inside enum bodies (enum values look like fields
# but should not have nanopb options added)
in_enum = bool(context_stack) and context_stack[-1][0] == "enum"
# --- Try to match and modify a field declaration ---
m = field_re.match(line)
if m and not in_enum:
indent = m.group(1)
qualifier = m.group(2) or ""
ftype = m.group(3)
fname = m.group(4)
fnum = m.group(5)
existing_opts = m.group(6) or ""
# Collect applicable nanopb options (wildcard < specific)
extra: Dict[str, Any] = {}
# 1. Wildcard: any field with this name in this proto file
if fname in wildcard:
extra.update(wildcard[fname])
# 2. Specific: check all keys whose last element is fname and whose
# preceding path matches the current message context
for key, opts in specific.items():
if key[-1] == fname:
msg_path = key[:-1]
if message_path_matches(context_stack, msg_path):
extra.update(opts)
break
if extra:
nanopb_str = format_nanopb_opts(extra)
if existing_opts.strip():
opts_block = f"[{existing_opts}, {nanopb_str}]"
else:
opts_block = f"[{nanopb_str}]"
qual = qualifier.rstrip()
sep = " " if qual else ""
line = f"{indent}{qual}{sep}{ftype} {fname} = {fnum} {opts_block};"
result.append(line)
# Edge case: if there were no import lines, add nanopb import after syntax line
if not import_added:
for i, line in enumerate(result):
if line.strip().startswith("syntax") and line.strip().endswith(";"):
result.insert(i + 1, f'import "{nanopb_import_path}";')
break
return "\n".join(result)
def main() -> int:
"""Parse an .options file and inject its constraints into a .proto file in-place."""
if len(sys.argv) != 3:
print(
f"Usage: {sys.argv[0]} <options_file> <proto_file>",
file=sys.stderr,
)
return 1
opts_path = Path(sys.argv[1])
proto_path = Path(sys.argv[2])
if not opts_path.exists():
print(f"Options file not found: {opts_path}", file=sys.stderr)
return 1
if not proto_path.exists():
print(f"Proto file not found: {proto_path}", file=sys.stderr)
return 1
specific, wildcard = parse_options_file(opts_path)
total = len(specific) + len(wildcard)
if total == 0:
print(f" [{opts_path.name}] No injectable options found, skipping.")
return 0
content = proto_path.read_text(encoding="utf-8")
# After regen-protobufs.sh's sed fixup, the nanopb import path is:
nanopb_import_path = "meshtastic/protobuf/nanopb.proto"
modified = inject_into_proto(content, specific, wildcard, nanopb_import_path)
proto_path.write_text(modified, encoding="utf-8")
print(
f" [{opts_path.name}] Injected {len(specific)} specific + "
f"{len(wildcard)} wildcard option(s) into {proto_path.name}"
)
return 0
if __name__ == "__main__":
sys.exit(main())
+14
View File
@@ -28,6 +28,7 @@ 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
@@ -45,6 +46,19 @@ $SEDCMD 's/^import "meshtastic\//import "meshtastic\/protobuf\//' "${INDIR}/"*.p
$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
+70
View File
@@ -0,0 +1,70 @@
# Contributing Example Scripts
Use this guide when adding or updating scripts in `examples/`.
## Must-have checklist before opening a PR
1. Script teaches one clear thing (its primary learning goal).
2. File name matches that goal.
3. Top docstring states purpose, transport scope, behavior, and expected output.
4. Script has safe shutdown (`with ...` or `finally`) and graceful `KeyboardInterrupt` handling.
5. Argument handling is clear (`argparse` preferred).
6. Errors are explicit (no bare `except:`).
7. The script is not a near-duplicate of an existing example.
## Choose the right teaching goal
Each example should have one primary lesson. Keep it focused.
- Good: "Send one text message over serial."
- Good: "Print inbound text messages."
- Avoid: discovery + chat + config mutation all in one script unless that combined flow is the lesson.
## Transport scope must be explicit
State exactly what transports are supported and why.
- Serial-only when that keeps the example simplest.
- Multi-transport (Serial/TCP/BLE) only when transport selection is part of the lesson.
- If TCP/BLE are supported, expose explicit flags (`--host`, `--ble`) and document defaults.
## Behavior and output should be predictable
Readers should know if the script sends, receives, mutates config, or combines those.
- Receive examples: subscribe to the narrowest pubsub topic that matches the lesson.
- Send examples: clarify destination behavior (broadcast default vs explicit destination).
- Mutation examples: clearly document side effects.
Output should make success easy to confirm:
- Print concise, stable status/event lines.
- Avoid noisy debug output unless the script is specifically diagnostic-focused.
## Cleanup and error handling
- Use context managers where practical; otherwise close interfaces in `finally`.
- Handle `KeyboardInterrupt` cleanly.
- Exit non-zero for invalid args, connection/setup failures, or command failures.
## Naming guidance
Use descriptive names tied to the teaching goal.
- Prefer names like `tcp_connection_info_once.py` over `pub_sub_example.py`.
- Prefer names like `tcp_pubsub_send_and_receive.py` over `pub_sub_example2.py`.
- Avoid generic names such as `example2.py`.
Keep existing filenames only when compatibility or discoverability outweighs clarity.
## New script vs extending an existing one
Create a new script when:
- The learning goal is genuinely distinct.
- Combining behaviors would make either example harder to understand.
Extend an existing script when:
- The change deepens the same lesson.
- The resulting script remains focused and readable.
+35 -14
View File
@@ -1,21 +1,42 @@
"""Simple program to demo how to use meshtastic library.
To run: python examples/get_hw.py
"""Print the local node hardware model.
Purpose: show the narrowest read-only local hardware lookup.
Transport scope: Serial only.
Behavior: reads local node metadata and prints hwModel.
Expected output: one hardware model line, if available.
Cleanup/error handling: exits with code 3 for bad args and closes interface on exit.
"""
import argparse
import sys
import meshtastic
import meshtastic.serial_interface
# simple arg check
if len(sys.argv) != 1:
print(f"usage: {sys.argv[0]}")
print("Print the hardware model for the local node.")
sys.exit(3)
iface = meshtastic.serial_interface.SerialInterface()
if iface.nodes:
for n in iface.nodes.values():
if n["num"] == iface.myInfo.my_node_num:
print(n["user"]["hwModel"])
iface.close()
def main() -> int:
"""Print the hardware model for the local node."""
if len(sys.argv) != 1:
print(f"usage: {sys.argv[0]}")
print("Print the hardware model for the local node.")
return 3
parser = argparse.ArgumentParser(description="Print local Meshtastic hardware model")
parser.parse_args()
try:
with meshtastic.serial_interface.SerialInterface() as iface:
if iface.nodes:
for node in iface.nodes.values():
if node["num"] == iface.myInfo.my_node_num:
print(node["user"]["hwModel"])
break
except KeyboardInterrupt:
return 0
except Exception as exc:
print(f"Error: Could not read hardware model: {exc}")
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
+31 -12
View File
@@ -1,19 +1,38 @@
"""Simple program to demo how to use meshtastic library.
To run: python examples/hello_world_serial.py
"""Send one text message over serial.
Purpose: minimal send-only example.
Transport scope: Serial only.
Behavior: sends one message and exits.
Expected output: no output on success.
Cleanup/error handling: exits with code 3 for bad args, closes interface on exit.
"""
import argparse
import sys
import meshtastic
import meshtastic.serial_interface
# simple arg check
if len(sys.argv) < 2:
print(f"usage: {sys.argv[0]} message")
sys.exit(3)
# By default will try to find a meshtastic device,
# otherwise provide a device path like /dev/ttyUSB0
iface = meshtastic.serial_interface.SerialInterface()
iface.sendText(sys.argv[1])
iface.close()
def main() -> int:
"""Parse arguments and send one text message."""
if len(sys.argv) < 2:
print(f"usage: {sys.argv[0]} message")
return 3
parser = argparse.ArgumentParser(description="Send one Meshtastic text message over serial")
parser.add_argument("message", help="Message text to broadcast")
args = parser.parse_args()
try:
with meshtastic.serial_interface.SerialInterface() as iface:
iface.sendText(args.message)
except KeyboardInterrupt:
return 0
except Exception as exc:
print(f"Error: Could not send message: {exc}")
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
+39 -13
View File
@@ -1,20 +1,46 @@
"""Simple program to demo how to use meshtastic library.
To run: python examples/info.py
"""Show a concise local node summary over serial.
Purpose: read local node identity and metadata in one place.
Transport scope: Serial only.
Behavior: reads node database, prints local node ID/name/hardware model.
Expected output: 1-3 summary lines describing the local node.
Cleanup/error handling: closes interface on exit and prints clear errors on failure.
"""
import meshtastic
import meshtastic.serial_interface
iface = meshtastic.serial_interface.SerialInterface()
# call showInfo() just to ensure values are populated
# info = iface.showInfo()
def main() -> int:
"""Print local node summary fields."""
try:
with meshtastic.serial_interface.SerialInterface() as iface:
local_num = iface.myInfo.my_node_num
local_node = None
if iface.nodes:
for node in iface.nodes.values():
if node["num"] == local_num:
local_node = node
break
if not local_node:
print(f"Local node not found in node database (node num: {local_num}).")
return 1
user = local_node.get("user", {})
print(f"Node number: {local_num}")
print(f"Node ID: {local_node.get('id', 'unknown')}")
print(
"Name: "
f"{user.get('longName', 'unknown')} ({user.get('shortName', 'unknown')})"
)
print(f"Hardware model: {user.get('hwModel', 'unknown')}")
except KeyboardInterrupt:
return 0
except Exception as exc:
print(f"Error: Could not read local node summary: {exc}")
return 1
return 0
if iface.nodes:
for n in iface.nodes.values():
if n["num"] == iface.myInfo.my_node_num:
print(n["user"]["hwModel"])
break
iface.close()
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,64 @@
#!/usr/bin/env python3
"""Passively monitor incoming text messages over serial.
Purpose: receive-only monitor for text messages.
Transport scope: Serial only.
Behavior: subscribes to text receive events and prints timestamp/channel/sender/text.
Expected output: one line per received text message.
Cleanup/error handling: graceful Ctrl+C exit and explicit connection errors.
"""
import argparse
import time
from datetime import datetime
from typing import Any, Optional
from pubsub import pub
import meshtastic.serial_interface
_TZ_NAME = time.tzname[time.localtime().tm_isdst > 0]
def on_receive(packet: dict[str, Any], interface: Any) -> None: # pylint: disable=unused-argument
"""Print a compact line for each received text packet."""
decoded = packet.get("decoded", {})
if decoded.get("portnum") != "TEXT_MESSAGE_APP":
return
message = decoded.get("text")
if not message:
return
channel_num = packet.get("channel", 0)
sender_id = packet.get("fromId", "unknown")
message_time = datetime.now().strftime(f"%a %b %d %Y %H:%M:%S {_TZ_NAME}")
print(f"{message_time} : {channel_num} : {sender_id} : {message}")
def main() -> int:
"""Connect over serial and print inbound text messages."""
parser = argparse.ArgumentParser(description="Read incoming Meshtastic text over serial")
parser.add_argument("--port", default=None, help="Serial port path (default: auto-detect)")
args = parser.parse_args()
pub.subscribe(on_receive, "meshtastic.receive")
iface: Optional[meshtastic.serial_interface.SerialInterface] = None
try:
iface = meshtastic.serial_interface.SerialInterface(devPath=args.port)
print("Connected. Listening for text messages. Press Ctrl+C to exit.")
while True:
time.sleep(1)
except KeyboardInterrupt:
return 0
except Exception as exc:
print(f"Error: Could not monitor serial messages: {exc}")
return 1
finally:
if iface:
iface.close()
return 0
if __name__ == "__main__":
raise SystemExit(main())
-30
View File
@@ -1,30 +0,0 @@
"""Simple program to demo how to use meshtastic library.
To run: python examples/pub_sub_example.py
"""
import sys
from pubsub import pub
import meshtastic
import meshtastic.tcp_interface
# simple arg check
if len(sys.argv) < 2:
print(f"usage: {sys.argv[0]} host")
sys.exit(1)
def onConnection(interface, topic=pub.AUTO_TOPIC): # pylint: disable=unused-argument
"""This is called when we (re)connect to the radio."""
print(interface.myInfo)
interface.close()
pub.subscribe(onConnection, "meshtastic.connection.established")
try:
iface = meshtastic.tcp_interface.TCPInterface(sys.argv[1])
except:
print(f"Error: Could not connect to {sys.argv[1]}")
sys.exit(1)
-39
View File
@@ -1,39 +0,0 @@
"""Simple program to demo how to use meshtastic library.
To run: python examples/pub_sub_example2.py
"""
import sys
import time
from pubsub import pub
import meshtastic
import meshtastic.tcp_interface
# simple arg check
if len(sys.argv) < 2:
print(f"usage: {sys.argv[0]} host")
sys.exit(1)
def onReceive(packet, interface): # pylint: disable=unused-argument
"""called when a packet arrives"""
print(f"Received: {packet}")
def onConnection(interface, topic=pub.AUTO_TOPIC): # pylint: disable=unused-argument
"""called when we (re)connect to the radio"""
# defaults to broadcast, specify a destination ID if you wish
interface.sendText("hello mesh")
pub.subscribe(onReceive, "meshtastic.receive")
pub.subscribe(onConnection, "meshtastic.connection.established")
try:
iface = meshtastic.tcp_interface.TCPInterface(hostname=sys.argv[1])
while True:
time.sleep(1000)
iface.close()
except Exception as ex:
print(f"Error: Could not connect to {sys.argv[1]} {ex}")
sys.exit(1)
+73
View File
@@ -0,0 +1,73 @@
"""Auto-reply to received text messages.
Purpose: demonstrate receive callback + generated reply flow.
Transport scope: Serial default, optional TCP/BLE.
Behavior: listens for text, prints message metadata, sends one reply per text message.
Expected output: "Connected..." plus message/reply lines while running.
Cleanup/error handling: clear connect failures and graceful Ctrl+C close.
"""
import argparse
import time
from typing import Any, Optional, Union
from pubsub import pub
import meshtastic.serial_interface
import meshtastic.tcp_interface
import meshtastic.ble_interface
from meshtastic.mesh_interface import MeshInterface
def onReceive(packet: dict, interface: MeshInterface) -> None:
"""Reply to every received packet with some info"""
text: Optional[str] = packet.get("decoded", {}).get("text")
if text:
rx_snr: Any = packet.get("rxSnr", "unknown")
hop_limit: Any = packet.get("hopLimit", "unknown")
print(f"message: {text}")
reply: str = f"got msg '{text}' with rxSnr: {rx_snr} and hopLimit: {hop_limit}"
print("Sending reply: ", reply)
interface.sendText(reply)
def onConnection(interface: MeshInterface, topic: Any = pub.AUTO_TOPIC) -> None: # pylint: disable=unused-argument
"""called when we (re)connect to the radio"""
print("Connected. Will auto-reply to all messages while running.")
parser = argparse.ArgumentParser(description="Meshtastic Auto-Reply Feature Demo")
group = parser.add_mutually_exclusive_group()
group.add_argument("--host", help="Connect via TCP to this hostname or IP")
group.add_argument("--ble", help="Connect via BLE to this MAC address")
args = parser.parse_args()
pub.subscribe(onReceive, "meshtastic.receive")
pub.subscribe(onConnection, "meshtastic.connection.established")
iface: Optional[Union[
meshtastic.tcp_interface.TCPInterface,
meshtastic.ble_interface.BLEInterface,
meshtastic.serial_interface.SerialInterface
]] = None
# defaults to serial, use --host for TCP or --ble for Bluetooth
try:
if args.host:
# note: timeout only applies after connection, not during the initial connect attempt
# TCPInterface.myConnect() calls socket.create_connection() without a timeout
iface = meshtastic.tcp_interface.TCPInterface(hostname=args.host, timeout=10)
elif args.ble:
iface = meshtastic.ble_interface.BLEInterface(address=args.ble, timeout=10)
else:
iface = meshtastic.serial_interface.SerialInterface(timeout=10)
except KeyboardInterrupt as exc:
raise SystemExit(0) from exc
except Exception as e:
print(f"Error: Could not connect. {e}")
raise SystemExit(1) from e
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
pass
finally:
if iface:
iface.close()
+40 -16
View File
@@ -1,7 +1,13 @@
"""Program to scan for hardware
To run: python examples/scan_for_devices.py
"""Scan host serial hardware for supported Meshtastic devices.
Purpose: host-side discovery without opening a radio session.
Transport scope: none (OS/device scanning only).
Behavior: scans vendor IDs, lists matched devices, and candidate active ports.
Expected output: vendor ID list, zero-or-more detected devices, and port list.
Cleanup/error handling: exits with code 3 for bad args and code 1 on scan errors.
"""
import argparse
import sys
from meshtastic.util import (
@@ -10,20 +16,38 @@ from meshtastic.util import (
get_unique_vendor_ids,
)
# simple arg check
if len(sys.argv) != 1:
print(f"usage: {sys.argv[0]}")
print("Detect which device we might have.")
sys.exit(3)
vids = get_unique_vendor_ids()
print(f"Searching for all devices with these vendor ids {vids}")
def main() -> int:
"""Run device detection and print candidate ports."""
if len(sys.argv) != 1:
print(f"usage: {sys.argv[0]}")
print("Detect which device we might have.")
return 3
sds = detect_supported_devices()
if len(sds) > 0:
print("Detected possible devices:")
for d in sds:
print(f" name:{d.name}{d.version} firmware:{d.for_firmware}")
parser = argparse.ArgumentParser(description="Scan host for supported Meshtastic devices")
parser.parse_args()
ports = active_ports_on_supported_devices(sds)
print(f"ports:{ports}")
try:
vids = get_unique_vendor_ids()
print(f"Searching for all devices with these vendor ids {vids}")
supported_devices = detect_supported_devices()
if supported_devices:
print("Detected possible devices:")
for device in supported_devices:
print(
f" name:{device.name}{device.version} firmware:{device.for_firmware}"
)
else:
print("Detected possible devices: none")
ports = active_ports_on_supported_devices(supported_devices)
print(f"ports:{ports}")
except Exception as exc:
print(f"Error: device scan failed: {exc}")
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
+33 -14
View File
@@ -1,21 +1,40 @@
"""Simple program to demo how to use meshtastic library.
To run: python examples/set_owner.py Bobby 333
"""Set local owner long/short name over serial.
Purpose: demonstrate a local config mutation workflow.
Transport scope: Serial only.
Behavior: updates owner long name and optional short name.
Expected output: prints the owner values being applied.
Cleanup/error handling: exits with code 3 for bad args and closes interface on exit.
"""
import argparse
import sys
import meshtastic
import meshtastic.serial_interface
# simple arg check
if len(sys.argv) < 2:
print(f"usage: {sys.argv[0]} long_name [short_name]")
sys.exit(3)
iface = meshtastic.serial_interface.SerialInterface()
long_name = sys.argv[1]
short_name = None
if len(sys.argv) > 2:
short_name = sys.argv[2]
iface.localNode.setOwner(long_name, short_name)
iface.close()
def main() -> int:
"""Parse args and set owner fields."""
if len(sys.argv) < 2:
print(f"usage: {sys.argv[0]} long_name [short_name]")
return 3
parser = argparse.ArgumentParser(description="Set Meshtastic local owner information")
parser.add_argument("long_name", help="Owner long name")
parser.add_argument("short_name", nargs="?", default=None, help="Owner short name")
args = parser.parse_args()
print(f"Setting owner long_name={args.long_name}, short_name={args.short_name}")
try:
with meshtastic.serial_interface.SerialInterface() as iface:
iface.localNode.setOwner(args.long_name, args.short_name)
except KeyboardInterrupt:
return 0
except Exception as exc:
print(f"Error: Could not set owner: {exc}")
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
+20 -2
View File
@@ -1,6 +1,24 @@
"""Simple program to show serial ports.
"""List serial ports currently visible to Meshtastic helpers.
Purpose: fastest host-side serial port enumeration.
Transport scope: none (host serial listing only).
Behavior: prints result of `findPorts()`.
Expected output: list-like representation of available candidate ports.
Cleanup/error handling: exits with code 1 on unexpected scan error.
"""
from meshtastic.util import findPorts
print(findPorts())
def main() -> int:
"""Print discovered serial ports."""
try:
print(findPorts())
except Exception as exc:
print(f"Error: Could not list ports: {exc}")
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
+41
View File
@@ -0,0 +1,41 @@
"""Connect over TCP, print connection info once, then exit.
Purpose: demonstrate pubsub connection lifecycle callback.
Transport scope: TCP only.
Behavior: subscribe to `meshtastic.connection.established`, print `myInfo`, then close.
Expected output: one object/line showing local radio info after connect.
Cleanup/error handling: explicit connect failure message and clean close on callback.
"""
import argparse
from pubsub import pub
import meshtastic.tcp_interface
def on_connection(interface, topic=pub.AUTO_TOPIC): # pylint: disable=unused-argument
"""Print local radio info when connected, then close."""
print(interface.myInfo)
interface.close()
def main() -> int:
"""Parse args, connect, and wait for established callback."""
parser = argparse.ArgumentParser(description="Print radio info on TCP connect and exit")
parser.add_argument("host", help="TCP hostname or IP of the Meshtastic node")
args = parser.parse_args()
pub.subscribe(on_connection, "meshtastic.connection.established")
try:
meshtastic.tcp_interface.TCPInterface(args.host)
except KeyboardInterrupt:
return 0
except Exception as exc:
print(f"Error: Could not connect to {args.host}: {exc}")
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
+43 -10
View File
@@ -1,14 +1,47 @@
"""Demonstration of how to look up a radio's location via its LAN connection.
Before running, connect your machine to the same WiFi network as the radio.
"""
"""Look up local node position over TCP.
Purpose: demonstrate read-only position lookup via LAN/TCP.
Transport scope: TCP only.
Behavior: connects, reads local node position, prints it, then exits.
Expected output: position dict for local node.
Cleanup/error handling: explicit connect/read failures and clean close.
"""
# pylint: disable=duplicate-code
import argparse
import meshtastic
import meshtastic.tcp_interface
radio_hostname = "meshtastic.local" # Can also be an IP
iface = meshtastic.tcp_interface.TCPInterface(radio_hostname)
my_node_num = iface.myInfo.my_node_num
pos = iface.nodesByNum[my_node_num]["position"]
print(pos)
iface.close()
def main() -> int:
"""Connect over TCP and print local node position."""
parser = argparse.ArgumentParser(description="Print local node position over TCP")
parser.add_argument(
"--host",
default="meshtastic.local",
help="TCP hostname or IP (default: meshtastic.local)",
)
args = parser.parse_args()
iface = None
try:
iface = meshtastic.tcp_interface.TCPInterface(args.host)
my_node_num = iface.myInfo.my_node_num
pos = iface.nodesByNum[my_node_num].get("position")
if pos is None:
print(f"No position available for local node {my_node_num}")
return 1
print(pos)
except KeyboardInterrupt:
return 0
except Exception as exc:
print(f"Error: Could not read position from {args.host}: {exc}")
return 1
finally:
if iface:
iface.close()
return 0
if __name__ == "__main__":
raise SystemExit(main())
+54
View File
@@ -0,0 +1,54 @@
"""Send once on connect and print received packets over TCP.
Purpose: demonstrate pubsub send-on-connect plus receive callback flow.
Transport scope: TCP only.
Behavior: sends "hello mesh" at connect, prints packets while running.
Expected output: "Connected..." plus "Received: ..." lines for inbound packets.
Cleanup/error handling: graceful Ctrl+C exit and clean interface close.
"""
import argparse
import time
from pubsub import pub
from meshtastic.tcp_interface import TCPInterface
def on_receive(packet, interface): # pylint: disable=unused-argument
"""Print each inbound packet."""
print(f"Received: {packet}")
def on_connection(interface, topic=pub.AUTO_TOPIC): # pylint: disable=unused-argument
"""Send a broadcast text when connected."""
print("Connected. Sending one broadcast message.")
interface.sendText("hello mesh")
def main() -> int:
"""Parse args, connect via TCP, and run callbacks."""
parser = argparse.ArgumentParser(description="TCP pubsub send-and-receive example")
parser.add_argument("host", help="TCP hostname or IP of the Meshtastic node")
args = parser.parse_args()
pub.subscribe(on_receive, "meshtastic.receive")
pub.subscribe(on_connection, "meshtastic.connection.established")
iface = None
try:
iface = TCPInterface(hostname=args.host)
while True:
time.sleep(1)
except KeyboardInterrupt:
return 0
except Exception as exc:
print(f"Error: Could not connect to {args.host}: {exc}")
return 1
finally:
if iface:
iface.close()
return 0
if __name__ == "__main__":
raise SystemExit(main())
+73
View File
@@ -0,0 +1,73 @@
"""Interactive text chat demo.
Purpose: demonstrate bidirectional text chat loop.
Transport scope: Serial default, optional TCP/BLE.
Behavior: prints incoming messages and sends each typed line as text.
Expected output: incoming sender/text lines and sent messages reaching peers.
Cleanup/error handling: explicit connect errors and graceful Ctrl+C / EOF close.
"""
import argparse
from typing import Any, Optional, Union
from pubsub import pub
import meshtastic.serial_interface
import meshtastic.tcp_interface
import meshtastic.ble_interface
from meshtastic.mesh_interface import MeshInterface
def onReceive(packet: dict, interface: MeshInterface) -> None: # pylint: disable=unused-argument
"""called when a packet arrives"""
text: Optional[str] = packet.get("decoded", {}).get("text")
if text:
sender: str = packet.get("fromId", "unknown")
print(f"{sender}: {text}")
def onConnection(interface: MeshInterface, topic: Any = pub.AUTO_TOPIC) -> None: # pylint: disable=unused-argument
"""called when we (re)connect to the radio"""
print("Connected. Type a message and press Enter to send. Ctrl+C to exit.")
parser = argparse.ArgumentParser(description="Meshtastic text chat demo")
group = parser.add_mutually_exclusive_group()
group.add_argument("--host", help="Connect via TCP to this hostname or IP")
group.add_argument("--ble", help="Connect via BLE to this MAC address or device name")
args = parser.parse_args()
pub.subscribe(onReceive, "meshtastic.receive")
pub.subscribe(onConnection, "meshtastic.connection.established")
iface: Optional[Union[
meshtastic.tcp_interface.TCPInterface,
meshtastic.ble_interface.BLEInterface,
meshtastic.serial_interface.SerialInterface
]] = None
# defaults to serial, use --host for TCP or --ble for Bluetooth
try:
if args.host:
# note: timeout only applies after connection, not during the initial connect attempt
# TCPInterface.myConnect() calls socket.create_connection() without a timeout
iface = meshtastic.tcp_interface.TCPInterface(hostname=args.host, timeout=10)
elif args.ble:
iface = meshtastic.ble_interface.BLEInterface(address=args.ble, timeout=10)
else:
iface = meshtastic.serial_interface.SerialInterface(timeout=10)
except KeyboardInterrupt as exc:
raise SystemExit(0) from exc
except Exception as e:
print(f"Error: Could not connect. {e}")
raise SystemExit(1) from e
assert iface is not None
try:
while True:
line = input()
if line:
iface.sendText(line)
except KeyboardInterrupt:
pass
except EOFError:
pass
finally:
if iface:
iface.close()
+30 -27
View File
@@ -1,7 +1,10 @@
"""Program to create and delete waypoint
To run:
python3 examples/waypoint.py --port /dev/ttyUSB0 create 45 test the_desc_2 '2024-12-18T23:05:23' 48.74 7.35
python3 examples/waypoint.py delete 45
"""Create or delete a waypoint.
Purpose: demonstrate waypoint mutation API (create/delete).
Transport scope: Serial only.
Behavior: sends waypoint create/delete request and prints API response.
Expected output: request response object printed to stdout.
Cleanup/error handling: explicit argument parsing and clean interface close.
"""
import argparse
@@ -12,26 +15,28 @@ import meshtastic
import meshtastic.serial_interface
parser = argparse.ArgumentParser(
prog='waypoint',
description='Create and delete Meshtastic waypoint')
parser.add_argument('--port', default=None)
parser.add_argument('--debug', default=False, action='store_true')
prog="waypoint", description="Create and delete Meshtastic waypoint"
)
parser.add_argument("--port", default=None)
parser.add_argument("--debug", default=False, action="store_true")
subparsers = parser.add_subparsers(dest='cmd')
parser_delete = subparsers.add_parser('delete', help='Delete a waypoint')
parser_delete.add_argument('id', help="id of the waypoint")
subparsers = parser.add_subparsers(dest="cmd", required=True)
parser_delete = subparsers.add_parser("delete", help="Delete a waypoint")
parser_delete.add_argument("id", type=int, help="ID of the waypoint")
parser_create = subparsers.add_parser('create', help='Create a new waypoint')
parser_create.add_argument('id', help="id of the waypoint")
parser_create.add_argument('name', help="name of the waypoint")
parser_create.add_argument('description', help="description of the waypoint")
parser_create.add_argument('icon', help="icon of the waypoint")
parser_create.add_argument('expire', help="expiration date of the waypoint as interpreted by datetime.fromisoformat")
parser_create.add_argument('latitude', help="latitude of the waypoint")
parser_create.add_argument('longitude', help="longitude of the waypoint")
parser_create = subparsers.add_parser("create", help="Create a new waypoint")
parser_create.add_argument("id", type=int, help="ID of the waypoint")
parser_create.add_argument("name", help="Name of the waypoint")
parser_create.add_argument("description", help="Description of the waypoint")
parser_create.add_argument("icon", help="Icon of the waypoint")
parser_create.add_argument(
"expire",
help="Expiration time as ISO timestamp accepted by datetime.fromisoformat",
)
parser_create.add_argument("latitude", type=float, help="Latitude of the waypoint")
parser_create.add_argument("longitude", type=float, help="Longitude of the waypoint")
args = parser.parse_args()
print(args)
# By default will try to find a meshtastic device,
# otherwise provide a device path like /dev/ttyUSB0
@@ -40,18 +45,16 @@ if args.debug:
else:
d = None
with meshtastic.serial_interface.SerialInterface(args.port, debugOut=d) as iface:
if args.cmd == 'create':
if args.cmd == "create":
p = iface.sendWaypoint(
waypoint_id=int(args.id),
waypoint_id=args.id,
name=args.name,
description=args.description,
icon=args.icon,
expire=int(datetime.datetime.fromisoformat(args.expire).timestamp()),
latitude=float(args.latitude),
longitude=float(args.longitude),
latitude=args.latitude,
longitude=args.longitude,
)
else:
p = iface.deleteWaypoint(int(args.id))
p = iface.deleteWaypoint(args.id)
print(p)
# iface.close()
+24 -2
View File
@@ -179,8 +179,30 @@ def _onPositionReceive(iface, asDict):
logger.debug(f"p:{p}")
p = iface._fixupPosition(p)
logger.debug(f"after fixup p:{p}")
# update node DB as needed
iface._getOrCreateByNum(asDict["from"])["position"] = 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):
+549 -138
View File
@@ -5,9 +5,11 @@
# later we can have a separate changelist to refactor main.py into smaller files
# pylint: disable=R0917,C0302
from typing import List, Optional, Union
from typing import Any, Callable, List, Optional, Union
from types import ModuleType
from decimal import Decimal
import argparse
argcomplete: Union[None, ModuleType] = None
@@ -37,6 +39,7 @@ try:
except ImportError as e:
have_test = False
import meshtastic.ota
import meshtastic.util
import meshtastic.serial_interface
import meshtastic.tcp_interface
@@ -60,11 +63,19 @@ except ImportError as e:
have_powermon = False
powermon_exception = e
meter = None
from meshtastic.protobuf import channel_pb2, config_pb2, portnums_pb2, mesh_pb2
from meshtastic.protobuf import admin_pb2, channel_pb2, clientonly_pb2, config_pb2, portnums_pb2, mesh_pb2
from meshtastic.version import get_active_version
logger = logging.getLogger(__name__)
# Map dotted preference paths to the protobuf enum that defines their flags.
# These fields are stored as uint32 bitmasks in the protobuf but have an
# associated enum that names the individual flags.
BITFIELD_ENUMS = {
"network.enabled_protocols": config_pb2.Config.NetworkConfig.ProtocolFlags,
"position.position_flags": config_pb2.Config.PositionConfig.PositionFlags,
}
def onReceive(packet, interface) -> None:
"""Callback invoked when a packet arrives"""
args = mt_config.args
@@ -85,12 +96,17 @@ def onReceive(packet, interface) -> None:
if d is not None and args and args.reply:
msg = d.get("text")
if msg:
rxSnr = packet["rxSnr"]
hopLimit = packet["hopLimit"]
print(f"message: {msg}")
reply = f"got msg '{msg}' with rxSnr: {rxSnr} and hopLimit: {hopLimit}"
print("Sending reply: ", reply)
interface.sendText(reply)
rxChannel = packet.get("channel", 0)
targetChannel = int(args.ch_index or 0)
if rxChannel == targetChannel:
rxSnr = packet["rxSnr"]
hopLimit = packet["hopLimit"]
print(f"message: {msg}")
reply = f"got msg '{msg}' with rxSnr: {rxSnr} and hopLimit: {hopLimit}"
print(f"Received channel {rxChannel}. Sending reply: {reply}")
interface.sendText(reply,channelIndex=rxChannel)
else:
print(f"Ignored message on channel {rxChannel} (waiting for channel {targetChannel})")
except Exception as ex:
print(f"Warning: Error processing received packet: {ex}.")
@@ -158,11 +174,11 @@ def getPref(node, comp_name) -> bool:
config_values = getattr(config, config_type.name)
if not wholeField:
pref_value = getattr(config_values, pref.name)
repeated = pref.label == pref.LABEL_REPEATED
repeated = _is_repeated_field(pref)
_printSetting(config_type, uni_name, pref_value, repeated)
else:
for field in config_values.ListFields():
repeated = field[0].label == field[0].LABEL_REPEATED
repeated = _is_repeated_field(field[0])
_printSetting(config_type, field[0].name, field[1], repeated)
else:
# Always show whole field for remote node
@@ -232,13 +248,28 @@ def setPref(config, comp_name, raw_val) -> bool:
print("Warning: network.wifi_psk must be 8 or more characters.")
return False
# Handle uint32 bitfields that have an associated enum of flag names.
bitfield_enum = None
if config_type.message_type is not None:
bitfield_path = f"{config_type.name}.{pref.name}"
bitfield_enum = BITFIELD_ENUMS.get(bitfield_path)
if bitfield_enum and isinstance(val, str):
# At this point fromStr() could not parse val as int/float/bool/bytes,
# so treat it as a comma-separated list of bitfield flag names.
flag_names = [name.strip() for name in val.split(",") if name.strip()]
try:
val = meshtastic.util.flags_from_list(bitfield_enum, flag_names)
except ValueError as e:
print(f"ERROR: {e}")
return False
enumType = pref.enum_type
# pylint: disable=C0123
if enumType and type(val) == str:
# We've failed so far to convert this string into an enum, try to find it by reflection
e = enumType.values_by_name.get(val)
if e:
val = e.number
ev = enumType.values_by_name.get(val)
if ev:
val = ev.number
else:
print(
f"{name[0]}.{uni_name} does not have an enum called {val}, so you can not set it."
@@ -253,7 +284,7 @@ def setPref(config, comp_name, raw_val) -> bool:
return False
# repeating fields need to be handled with append, not setattr
if pref.label != pref.LABEL_REPEATED:
if not _is_repeated_field(pref):
try:
if config_type.message_type is not None:
config_values = getattr(config_part, config_type.name)
@@ -452,6 +483,41 @@ def onConnected(interface):
waitForAckNak = True
interface.getNode(args.dest, False, **getNode_kwargs).rebootOTA()
if args.ota_update:
closeNow = True
waitForAckNak = True
if not isinstance(interface, meshtastic.tcp_interface.TCPInterface):
meshtastic.util.our_exit(
"Error: OTA update currently requires a TCP connection to the node (use --host)."
)
ota = meshtastic.ota.ESP32WiFiOTA(args.ota_update, interface.hostname)
print(f"Triggering OTA update on {interface.hostname}...")
interface.getNode(args.dest, False, **getNode_kwargs).startOTA(
ota_mode=admin_pb2.OTAMode.OTA_WIFI,
ota_file_hash=ota.hash_bytes()
)
print("Waiting for device to reboot into OTA mode...")
time.sleep(5)
retries = 5
while retries > 0:
try:
ota.update()
break
except Exception as e:
retries -= 1
if retries == 0:
meshtastic.util.our_exit(f"\nOTA update failed: {e}")
time.sleep(2)
print("\nOTA update completed successfully!")
if args.enter_dfu:
closeNow = True
waitForAckNak = True
@@ -511,6 +577,13 @@ def onConnected(interface):
waitForAckNak = True
interface.getNode(args.dest, False, **getNode_kwargs).resetNodeDb()
if args.add_contact:
closeNow = True
waitForAckNak = True
interface.getNode(args.dest, False, **getNode_kwargs).addContactURL(
args.add_contact
)
if args.sendtext:
closeNow = True
channelIndex = mt_config.channel_index or 0
@@ -671,115 +744,105 @@ def onConnected(interface):
printConfig(node.moduleConfig)
if args.configure:
with open(args.configure[0], encoding="utf8") as file:
configuration = yaml.safe_load(file)
closeNow = True
if args.dest != BROADCAST_ADDR:
print("Configuring remote nodes is not supported.")
return
interface.getNode(args.dest, False, **getNode_kwargs).beginSettingsTransaction()
filename = args.configure[0]
fmt = getattr(args, "export_format", "auto")
if "owner" in configuration:
# Validate owner name before setting
owner_name = str(configuration["owner"]).strip()
if not owner_name:
meshtastic.util.our_exit("ERROR: Long Name cannot be empty or contain only whitespace characters")
print(f"Setting device owner to {configuration['owner']}")
waitForAckNak = True
interface.getNode(args.dest, False, **getNode_kwargs).setOwner(configuration["owner"])
time.sleep(0.5)
def _seed_config(section: str, is_module: bool) -> Optional[Any]:
"""Return the current local-node config section, if present."""
config_obj = (
interface.localNode.moduleConfig
if is_module
else interface.localNode.localConfig
)
return getattr(config_obj, section) if config_obj.HasField(section) else None
if "owner_short" in configuration:
# Validate owner short name before setting
owner_short_name = str(configuration["owner_short"]).strip()
if not owner_short_name:
meshtastic.util.our_exit("ERROR: Short Name cannot be empty or contain only whitespace characters")
print(
f"Setting device owner short to {configuration['owner_short']}"
)
waitForAckNak = True
interface.getNode(args.dest, False, **getNode_kwargs).setOwner(
long_name=None, short_name=configuration["owner_short"]
)
time.sleep(0.5)
profile = _read_profile(filename, fmt, seed_fn=_seed_config)
if "ownerShort" in configuration:
# Validate owner short name before setting
owner_short_name = str(configuration["ownerShort"]).strip()
if not owner_short_name:
meshtastic.util.our_exit("ERROR: Short Name cannot be empty or contain only whitespace characters")
print(
f"Setting device owner short to {configuration['ownerShort']}"
)
waitForAckNak = True
interface.getNode(args.dest, False, **getNode_kwargs).setOwner(
long_name=None, short_name=configuration["ownerShort"]
)
time.sleep(0.5)
closeNow = True
interface.getNode(args.dest, False, **getNode_kwargs).beginSettingsTransaction()
if "channel_url" in configuration:
print("Setting channel url to", configuration["channel_url"])
interface.getNode(args.dest, **getNode_kwargs).setURL(configuration["channel_url"])
time.sleep(0.5)
# Owner: combine long_name and short_name into a single setOwner call.
# NOTE: is_licensed and is_unmessagable are not yet in DeviceProfile;
# ref: https://github.com/meshtastic/protobufs/pull/971
long_name = str(profile.long_name).strip() if profile.long_name else None
short_name = str(profile.short_name).strip() if profile.short_name else None
if "channelUrl" in configuration:
print("Setting channel url to", configuration["channelUrl"])
interface.getNode(args.dest, **getNode_kwargs).setURL(configuration["channelUrl"])
time.sleep(0.5)
if long_name is not None and not long_name:
meshtastic.util.our_exit("ERROR: Long Name cannot be empty or contain only whitespace characters")
if short_name is not None and not short_name:
meshtastic.util.our_exit("ERROR: Short Name cannot be empty or contain only whitespace characters")
if "canned_messages" in configuration:
print("Setting canned message messages to", configuration["canned_messages"])
interface.getNode(args.dest, **getNode_kwargs).set_canned_message(configuration["canned_messages"])
time.sleep(0.5)
if long_name or short_name:
if long_name and short_name:
print(f"Setting device owner to {long_name} and short name to {short_name}")
elif long_name:
print(f"Setting device owner to {long_name}")
else:
print(f"Setting device owner short to {short_name}")
waitForAckNak = True
interface.getNode(args.dest, False, **getNode_kwargs).setOwner(
long_name=long_name, short_name=short_name
)
time.sleep(0.5)
if "ringtone" in configuration:
print("Setting ringtone to", configuration["ringtone"])
interface.getNode(args.dest, **getNode_kwargs).set_ringtone(configuration["ringtone"])
time.sleep(0.5)
if profile.channel_url:
print(f"Setting channel url to {profile.channel_url}")
interface.getNode(args.dest, **getNode_kwargs).setURL(profile.channel_url)
time.sleep(0.5)
if "location" in configuration:
alt = 0
lat = 0.0
lon = 0.0
localConfig = interface.localNode.localConfig
if profile.canned_messages:
print(f"Setting canned message messages to {profile.canned_messages}")
interface.getNode(args.dest, **getNode_kwargs).set_canned_message(profile.canned_messages)
time.sleep(0.5)
if "alt" in configuration["location"]:
alt = int(configuration["location"]["alt"] or 0)
print(f"Fixing altitude at {alt} meters")
if "lat" in configuration["location"]:
lat = float(configuration["location"]["lat"] or 0)
print(f"Fixing latitude at {lat} degrees")
if "lon" in configuration["location"]:
lon = float(configuration["location"]["lon"] or 0)
print(f"Fixing longitude at {lon} degrees")
if profile.ringtone:
print(f"Setting ringtone to {profile.ringtone}")
interface.getNode(args.dest, **getNode_kwargs).set_ringtone(profile.ringtone)
time.sleep(0.5)
if profile.HasField("fixed_position"):
# Only send the admin message when the position config
# explicitly opts into fixed_position. The admin message
# unconditionally enables the flag on the device, so we
# require an explicit opt-in from the profile's config.
if (
profile.HasField("config")
and profile.config.HasField("position")
and profile.config.position.fixed_position
):
pos = profile.fixed_position
lat = float(pos.latitude_i * Decimal("1e-7")) if pos.latitude_i else 0.0
lon = float(pos.longitude_i * Decimal("1e-7")) if pos.longitude_i else 0.0
alt = pos.altitude if pos.altitude else 0
print(f"Fixing altitude at {alt} meters")
print(f"Fixing latitude at {lat} degrees")
print(f"Fixing longitude at {lon} degrees")
print("Setting device position")
interface.localNode.setFixedPosition(lat, lon, alt)
interface.getNode(args.dest, False, **getNode_kwargs).setFixedPosition(lat, lon, alt)
time.sleep(0.5)
if "config" in configuration:
localConfig = interface.getNode(args.dest, **getNode_kwargs).localConfig
for section in configuration["config"]:
traverseConfig(
section, configuration["config"][section], localConfig
)
interface.getNode(args.dest, **getNode_kwargs).writeConfig(
meshtastic.util.camel_to_snake(section)
)
if profile.HasField("config"):
localConfig = interface.getNode(args.dest, **getNode_kwargs).localConfig
for field in profile.config.DESCRIPTOR.fields:
if field.message_type is not None and profile.config.HasField(field.name):
getattr(localConfig, field.name).CopyFrom(getattr(profile.config, field.name))
interface.getNode(args.dest, **getNode_kwargs).writeConfig(field.name)
time.sleep(0.5)
if "module_config" in configuration:
moduleConfig = interface.getNode(args.dest, **getNode_kwargs).moduleConfig
for section in configuration["module_config"]:
traverseConfig(
section,
configuration["module_config"][section],
moduleConfig,
)
interface.getNode(args.dest, **getNode_kwargs).writeConfig(
meshtastic.util.camel_to_snake(section)
)
if profile.HasField("module_config"):
moduleConfig = interface.getNode(args.dest, **getNode_kwargs).moduleConfig
for field in profile.module_config.DESCRIPTOR.fields:
if field.message_type is not None and profile.module_config.HasField(field.name):
getattr(moduleConfig, field.name).CopyFrom(getattr(profile.module_config, field.name))
interface.getNode(args.dest, **getNode_kwargs).writeConfig(field.name)
time.sleep(0.5)
interface.getNode(args.dest, False, **getNode_kwargs).commitSettingsTransaction()
print("Writing modified configuration to device")
interface.getNode(args.dest, False, **getNode_kwargs).commitSettingsTransaction()
print("Writing modified configuration to device")
if args.export_config:
if args.dest != BROADCAST_ADDR:
@@ -787,18 +850,40 @@ def onConnected(interface):
return
closeNow = True
config_txt = export_config(interface)
if args.export_config == "-":
# Output to stdout (preserves legacy use of `> file.yaml`)
print(config_txt)
is_binary = False
fmt = getattr(args, "export_format", "auto")
if fmt in ("binary", "protobuf"):
is_binary = True
elif fmt == "yaml":
is_binary = False
else:
try:
with open(args.export_config, "w", encoding="utf-8") as f:
f.write(config_txt)
print(f"Exported configuration to {args.export_config}")
except Exception as e:
meshtastic.util.our_exit(f"ERROR: Failed to write config file: {e}")
is_binary = args.export_config.endswith(".cfg")
if is_binary:
config_bytes = export_profile(interface)
if args.export_config == "-":
sys.stdout.buffer.write(config_bytes)
else:
try:
with open(args.export_config, "wb") as f:
f.write(config_bytes)
print(f"Exported profile to {args.export_config}")
except Exception as e:
meshtastic.util.our_exit(f"ERROR: Failed to write profile file: {e}")
else:
config_txt = export_config(interface)
if args.export_config == "-":
# Output to stdout (preserves legacy use of `> file.yaml`)
print(config_txt)
else:
try:
with open(args.export_config, "w", encoding="utf-8") as f:
f.write(config_txt)
print(f"Exported configuration to {args.export_config}")
except Exception as e:
meshtastic.util.our_exit(f"ERROR: Failed to write config file: {e}")
if args.ch_set_url:
closeNow = True
@@ -884,9 +969,15 @@ def onConnected(interface):
if args.ch_longslow:
setSimpleConfig(config_pb2.Config.LoRaConfig.ModemPreset.LONG_SLOW)
if args.ch_longmod:
setSimpleConfig(config_pb2.Config.LoRaConfig.ModemPreset.LONG_MODERATE)
if args.ch_longfast:
setSimpleConfig(config_pb2.Config.LoRaConfig.ModemPreset.LONG_FAST)
if args.ch_longturbo:
setSimpleConfig(config_pb2.Config.LoRaConfig.ModemPreset.LONG_TURBO)
if args.ch_medslow:
setSimpleConfig(config_pb2.Config.LoRaConfig.ModemPreset.MEDIUM_SLOW)
@@ -899,6 +990,9 @@ def onConnected(interface):
if args.ch_shortfast:
setSimpleConfig(config_pb2.Config.LoRaConfig.ModemPreset.SHORT_FAST)
if args.ch_shortturbo:
setSimpleConfig(config_pb2.Config.LoRaConfig.ModemPreset.SHORT_TURBO)
if args.ch_set or args.ch_enable or args.ch_disable:
closeNow = True
@@ -1033,6 +1127,20 @@ def onConnected(interface):
else:
print("Install pyqrcode to view a QR code printed to terminal.")
if args.contact_qr:
closeNow = True
url = interface.getNode(args.dest, True, **getNode_kwargs).getContactURL(
args.contact_qr,
should_ignore=args.contact_ignore,
manually_verified=args.contact_verified,
)
print(f"Contact URL: {url}")
if pyqrcode is not None:
qr = pyqrcode.create(url)
print(qr.terminal())
else:
print("Install pyqrcode to view a QR code printed to terminal.")
log_set: Optional = None # type: ignore[annotation-unchecked]
# we need to keep a reference to the logset so it doesn't get GCed early
@@ -1131,6 +1239,14 @@ def subscribe() -> None:
# pub.subscribe(onNode, "meshtastic.node")
def _is_repeated_field(field_desc) -> bool:
"""Return True if the protobuf field is repeated.
Protobuf 6.31.0 and later use an is_repeated property, while older versions compare against the label field.
"""
if hasattr(field_desc, "is_repeated"):
return bool(field_desc.is_repeated)
return field_desc.label == field_desc.LABEL_REPEATED
def set_missing_flags_false(config_dict: dict, true_defaults: set[tuple[str, str]]) -> None:
"""Ensure that missing default=True keys are present in the config_dict and set to False."""
for path in true_defaults:
@@ -1170,7 +1286,7 @@ def export_config(interface) -> str:
lat = None
lon = None
alt = None
if pos:
if pos and interface.localNode.localConfig.position.fixed_position:
lat = pos.get("latitude")
lon = pos.get("longitude")
alt = pos.get("altitude")
@@ -1214,7 +1330,7 @@ def export_config(interface) -> str:
for i in range(len(prefs[pref]['adminKey'])):
prefs[pref]['adminKey'][i] = 'base64:' + prefs[pref]['adminKey'][i]
if mt_config.camel_case:
configObj["config"] = config #Identical command here and 2 lines below?
configObj["config"] = prefs
else:
configObj["config"] = config
@@ -1239,6 +1355,162 @@ def export_config(interface) -> str:
config_txt += yaml.dump(configObj)
return config_txt
def _set_if_populated(profile, field_name, value):
if value is None:
return
val = str(value).strip()
if val:
setattr(profile, field_name, val)
def _profile_from_yaml(
configuration: dict,
seed_fn: Optional[Callable[[str, bool], Optional[Any]]] = None,
) -> clientonly_pb2.DeviceProfile:
"""Convert a YAML config dict to a DeviceProfile protobuf for uniform import.
If seed_fn is provided, it is called for each config/module_config section
before applying YAML values. It should return the current protobuf message
for that section (or None) so that unmentioned fields are preserved.
"""
profile = clientonly_pb2.DeviceProfile()
if "owner" in configuration:
_set_if_populated(profile, "long_name", configuration["owner"])
if "owner_short" in configuration:
_set_if_populated(profile, "short_name", configuration["owner_short"])
elif "ownerShort" in configuration:
_set_if_populated(profile, "short_name", configuration["ownerShort"])
if "channel_url" in configuration:
_set_if_populated(profile, "channel_url", configuration["channel_url"])
elif "channelUrl" in configuration:
_set_if_populated(profile, "channel_url", configuration["channelUrl"])
if "canned_messages" in configuration:
_set_if_populated(profile, "canned_messages", configuration["canned_messages"])
if "ringtone" in configuration:
_set_if_populated(profile, "ringtone", configuration["ringtone"])
loc = configuration.get("location")
if loc:
lat = float(loc.get("lat", 0) or 0)
lon = float(loc.get("lon", 0) or 0)
alt = int(loc.get("alt", 0) or 0)
if lat or lon or alt:
profile.fixed_position.latitude_i = int(Decimal(str(lat)) * Decimal("1e7"))
profile.fixed_position.longitude_i = int(Decimal(str(lon)) * Decimal("1e7"))
profile.fixed_position.altitude = alt
if "config" in configuration:
for section in configuration["config"]:
section_snake = meshtastic.util.camel_to_snake(section)
if seed_fn is not None:
seeded = seed_fn(section_snake, False)
if seeded is not None:
getattr(profile.config, section_snake).CopyFrom(seeded)
traverseConfig(section, configuration["config"][section], profile.config)
if "module_config" in configuration:
for section in configuration["module_config"]:
section_snake = meshtastic.util.camel_to_snake(section)
if seed_fn is not None:
seeded = seed_fn(section_snake, True)
if seeded is not None:
getattr(profile.module_config, section_snake).CopyFrom(seeded)
traverseConfig(section, configuration["module_config"][section], profile.module_config)
return profile
def _read_profile(
filename: str,
fmt: str,
seed_fn: Optional[Callable[[str, bool], Optional[Any]]] = None,
) -> clientonly_pb2.DeviceProfile:
"""Read a config file and return a DeviceProfile, autodetecting format by content."""
with open(filename, "rb") as f:
raw = f.read()
if fmt in ("binary", "protobuf"):
try:
return _parse_profile_bytes(raw)
except Exception as e:
meshtastic.util.our_exit(
f"ERROR: {filename} is not a valid DeviceProfile (.cfg) file: {e}"
)
if fmt == "yaml":
try:
configuration = yaml.safe_load(raw.decode("utf8"))
except (UnicodeDecodeError, yaml.YAMLError) as e:
meshtastic.util.our_exit(
f"ERROR: {filename} is not a valid YAML config (expected UTF-8 YAML): {e}"
)
if not isinstance(configuration, dict):
meshtastic.util.our_exit(
f"ERROR: {filename} is not a valid YAML config (expected a mapping)."
)
return _profile_from_yaml(configuration, seed_fn=seed_fn)
# Auto: try YAML first (text files), fall back to protobuf (binary files).
try:
configuration = yaml.safe_load(raw.decode("utf8"))
if isinstance(configuration, dict):
return _profile_from_yaml(configuration, seed_fn=seed_fn)
except (UnicodeDecodeError, yaml.YAMLError):
pass
try:
return _parse_profile_bytes(raw)
except Exception as e:
meshtastic.util.our_exit(
f"ERROR: {filename} is not a valid YAML config or DeviceProfile (.cfg) file: {e}"
)
def _parse_profile_bytes(raw: bytes) -> clientonly_pb2.DeviceProfile:
"""Parse raw bytes as a DeviceProfile protobuf, raising on failure."""
profile = clientonly_pb2.DeviceProfile()
profile.ParseFromString(raw)
return profile
def export_profile(interface) -> bytes:
"""used in --export-config for binary .cfg files"""
profile = clientonly_pb2.DeviceProfile()
owner = interface.getLongName()
owner_short = interface.getShortName()
channel_url = interface.localNode.getURL()
myinfo = interface.getMyNodeInfo()
canned_messages = interface.getCannedMessage()
ringtone = interface.getRingtone()
if owner:
profile.long_name = owner
if owner_short:
profile.short_name = owner_short
if channel_url:
profile.channel_url = channel_url
if canned_messages:
profile.canned_messages = canned_messages
if ringtone:
profile.ringtone = ringtone
profile.config.CopyFrom(interface.localNode.localConfig)
profile.module_config.CopyFrom(interface.localNode.moduleConfig)
if interface.localNode.localConfig.position.fixed_position:
pos = myinfo.get("position")
if pos:
lat = pos.get("latitude")
lon = pos.get("longitude")
alt = pos.get("altitude")
if lat or lon or alt:
if lat:
profile.fixed_position.latitude_i = int(Decimal(str(lat)) * Decimal("1e7"))
if lon:
profile.fixed_position.longitude_i = int(Decimal(str(lon)) * Decimal("1e7"))
if alt:
profile.fixed_position.altitude = int(alt)
return profile.SerializeToString()
def create_power_meter():
"""Setup the power meter."""
@@ -1313,6 +1585,17 @@ def common():
if not stripped_ham_name:
meshtastic.util.our_exit("ERROR: Ham radio callsign cannot be empty or contain only whitespace characters")
# Early validation for OTA firmware file before attempting device connection
if hasattr(args, 'ota_update') and args.ota_update is not None:
if not os.path.isfile(args.ota_update):
meshtastic.util.our_exit(f"Error: OTA firmware file not found: {args.ota_update}", 1)
# OTA (WiFi/BLE) only needs the local node to send the admin request and then
# streams the firmware directly; it never reads the node DB. Skip fetching it so a
# large node DB dump can't stall/close the connection before the OTA request lands.
if getattr(args, "ota_update", None) or getattr(args, "reboot_ota", False):
args.no_nodes = True
if have_powermon:
create_power_meter()
@@ -1365,13 +1648,59 @@ def common():
print(f"Found: name='{x.name}' address='{x.address}'")
meshtastic.util.our_exit("BLE scan finished", 0)
elif args.ble:
client = BLEInterface(
args.ble if args.ble != "any" else None,
debugOut=logfile,
noProto=args.noproto,
noNodes=args.no_nodes,
timeout=args.timeout,
)
try:
client = BLEInterface(
args.ble if args.ble != "any" else None,
debugOut=logfile,
noProto=args.noproto,
noNodes=args.no_nodes,
timeout=args.timeout,
)
except BLEInterface.BLEError as e:
if e.kind == BLEInterface.BLEError.DEVICE_NOT_FOUND:
meshtastic.util.our_exit(
"BLE device not found.\n\n"
"Possible causes:\n"
" - Bluetooth is disabled on the Meshtastic device\n"
" - Device is in deep sleep mode\n"
" - Device is out of range\n\n"
"Try:\n"
" - Press the reset button on your device\n"
" - Run 'meshtastic --ble-scan' to see available devices",
1,
)
elif e.kind == BLEInterface.BLEError.MULTIPLE_DEVICES:
meshtastic.util.our_exit(
"Multiple Meshtastic BLE devices found.\n\n"
"Please specify which device to connect to:\n"
" - Run 'meshtastic --ble-scan' to list devices\n"
" - Use 'meshtastic --ble <name_or_address>' to connect",
1,
)
elif e.kind == BLEInterface.BLEError.WRITE_ERROR:
meshtastic.util.our_exit(
"Failed to write to BLE device.\n\n"
"Possible causes:\n"
" - Device requires pairing PIN (check device screen)\n"
" - On Linux: user not in 'bluetooth' group\n"
" - Connection was interrupted\n\n"
"Try:\n"
" - Restart Bluetooth on your computer\n"
" - Reset the Meshtastic device",
1,
)
elif e.kind == BLEInterface.BLEError.READ_ERROR:
meshtastic.util.our_exit(
"Failed to read from BLE device.\n\n"
"The device may have disconnected unexpectedly.\n\n"
"Try:\n"
" - Move closer to the device\n"
" - Reset the Meshtastic device\n"
" - Restart Bluetooth on your computer",
1,
)
else:
meshtastic.util.our_exit(f"BLE error: {e}", 1)
elif args.host:
try:
if ":" in args.host:
@@ -1427,6 +1756,23 @@ def common():
message += " Please close any applications or webpages that may be using the device and try again.\n"
message += f"\nOriginal error: {ex}"
meshtastic.util.our_exit(message)
except MeshInterface.MeshInterfaceError as ex:
msg = str(ex)
if "Timed out" in msg:
meshtastic.util.our_exit(
"Connection timed out.\n\n"
"Possible causes:\n"
" - Device is rebooting\n"
" - Device firmware is updating\n"
" - Serial connection was interrupted\n\n"
"Try:\n"
" - Wait a few seconds and try again\n"
" - Check if device is fully booted (LED patterns)\n"
" - Reconnect the USB cable",
1,
)
else:
meshtastic.util.our_exit(f"Connection error: {ex}", 1)
if client.devPath is None:
try:
client = meshtastic.tcp_interface.TCPInterface(
@@ -1483,10 +1829,12 @@ def addConnectionArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentParse
"--host",
"--tcp",
"-t",
help="Connect to a device using TCP, optionally passing hostname or IP address to use. (defaults to '%(const)s')",
help=("Connect to a device using TCP, optionally passing hostname or IP address to use. (defaults to '%(const)s'). "
"A port number may be specified as well, e.g. meshtastic.local:4404. The default port is 4403."),
nargs="?",
default=None,
const="localhost",
metavar="HOST[:PORT]",
)
group.add_argument(
@@ -1539,7 +1887,9 @@ def addImportExportArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentPar
group.add_argument(
"--configure",
help="Specify a path to a yaml(.yml) file containing the desired settings for the connected device.",
"--import-config",
dest="configure",
help="Specify a path to a configuration file to import. Autodetects format (yaml or binary protobuf).",
action="append",
)
group.add_argument(
@@ -1547,7 +1897,13 @@ def addImportExportArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentPar
nargs="?",
const="-", # default to "-" if no value provided
metavar="FILE",
help="Export device config as YAML (to stdout if no file given)"
help="Export device config (to stdout if no file given). Autodetects format by extension if possible."
)
group.add_argument(
"--export-format",
choices=["auto", "yaml", "binary", "protobuf"],
default="auto",
help="Format for export or import. 'auto' uses file extension or contents. 'binary' or 'protobuf' forces binary format. 'yaml' forces yaml."
)
return parser
@@ -1619,43 +1975,61 @@ def addConfigArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentParser:
group.add_argument(
"--ch-vlongslow",
help="Change to the very long-range and slow modem preset",
help="Change to the VERY_LONG_SLOW modem preset. Deprecated since 2.5 firmware.",
action="store_true",
)
group.add_argument(
"--ch-longslow",
help="Change to the long-range and slow modem preset",
help="Change to the LONG_SLOW modem preset. Deprecated since 2.7 firmware.",
action="store_true",
)
group.add_argument(
"--ch-longmod", "--ch-longmoderate",
help="Change to the LONG_MODERATE modem preset",
action="store_true",
)
group.add_argument(
"--ch-longfast",
help="Change to the long-range and fast modem preset",
help="Change to the LONG_FAST modem preset",
action="store_true",
)
group.add_argument(
"--ch-longturbo",
help="Change to the LONG_TURBO preset",
action="store_true",
)
group.add_argument(
"--ch-medslow",
help="Change to the med-range and slow modem preset",
help="Change to the MEDIUM_SLOW modem preset",
action="store_true",
)
group.add_argument(
"--ch-medfast",
help="Change to the med-range and fast modem preset",
help="Change to the MEDIUM_FAST modem preset",
action="store_true",
)
group.add_argument(
"--ch-shortslow",
help="Change to the short-range and slow modem preset",
help="Change to the SHORT_SLOW modem preset",
action="store_true",
)
group.add_argument(
"--ch-shortfast",
help="Change to the short-range and fast modem preset",
help="Change to the SHORT_FAST modem preset",
action="store_true",
)
group.add_argument(
"--ch-shortturbo",
help="Change to the SHORT_TURBO modem preset",
action="store_true",
)
@@ -1746,6 +2120,24 @@ def addChannelConfigArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentPa
action="store_true",
)
group.add_argument(
"--contact-qr",
help="Display a QR code for a node's contact data. "
"Use the node ID with a '!' or '0x' prefix or the node number. "
"Also shows the shareable contact URL.",
metavar="!xxxxxxxx",
)
group.add_argument(
"--contact-verified",
help="Set the IS_KEY_MANUALLY_VERIFIED bit in the generated contact URL",
action="store_true",
)
group.add_argument(
"--contact-ignore",
help="Mark this contact as blocked/ignored in the generated contact URL",
action="store_true",
)
group.add_argument(
"--ch-enable",
help="Enable the specified channel. Use --ch-add instead whenever possible.",
@@ -1798,7 +2190,8 @@ def addPositionConfigArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentP
group.add_argument(
"--pos-fields",
help="Specify fields to send when sending a position. Use no argument for a list of valid values. "
help="Deprecated: use '--set position.position_flags FLAG1,FLAG2' instead. "
"Specify fields to send when sending a position. Use no argument for a list of valid values. "
"Can pass multiple values as a space separated list like "
"this: '--pos-fields ALTITUDE HEADING SPEED'",
nargs="*",
@@ -1883,7 +2276,10 @@ def addRemoteActionArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentPar
)
group.add_argument(
"--reply", help="Reply to received messages", action="store_true"
"--reply",
help="Reply to received messages on the channel they were received. "
"If '--ch-index' is set, only messages on that channel are replied to.",
action="store_true",
)
return parser
@@ -1904,10 +2300,18 @@ def addRemoteAdminArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentPars
group.add_argument(
"--reboot-ota",
help="Tell the destination node to reboot into factory firmware (ESP32)",
help="Tell the destination node to reboot into factory firmware (ESP32, firmware version <2.7.18)",
action="store_true",
)
group.add_argument(
"--ota-update",
help="Perform an OTA update on the local node (ESP32, firmware version >=2.7.18, WiFi/TCP only for now). "
"Specify the path to the firmware file.",
metavar="FIRMWARE_FILE",
action="store",
)
group.add_argument(
"--enter-dfu",
help="Tell the destination node to enter DFU mode (NRF52)",
@@ -1972,6 +2376,13 @@ def addRemoteAdminArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentPars
action="store_true",
)
group.add_argument(
"--add-contact",
help="Add a contact (User) to the NodeDB from a shareable URL. "
"Example: https://meshtastic.org/v/#<base64>",
metavar="URL",
)
group.add_argument(
"--set-time",
help="Set the time to the provided unix epoch timestamp, or the system's current time if omitted or 0.",
+40 -6
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
@@ -32,6 +33,16 @@ class BLEInterface(MeshInterface):
class BLEError(Exception):
"""An exception class for BLE errors."""
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],
@@ -156,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]
@@ -203,7 +216,10 @@ class BLEInterface(MeshInterface):
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)
@@ -226,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)
@@ -258,11 +275,13 @@ 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:
logger.debug("No address provided - only discover method will work.")
@@ -306,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()
+34 -5
View File
@@ -253,6 +253,7 @@ class MeshInterface: # pylint: disable=R0902
"channel": "Channel",
"lastHeard": "LastHeard",
"since": "Since",
"isFavorite": "Fav",
}
@@ -300,7 +301,7 @@ class MeshInterface: # pylint: disable=R0902
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", "lastHeard", "since"]
"deviceMetrics.airUtilTx", "snr", "hopsAway", "channel", "isFavorite", "lastHeard", "since"]
else:
# Always at least include the row number.
showFields.insert(0, "N")
@@ -342,6 +343,8 @@ class MeshInterface: # pylint: disable=R0902
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":
@@ -416,6 +419,7 @@ class MeshInterface: # pylint: disable=R0902
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.
@@ -433,6 +437,7 @@ class MeshInterface: # pylint: disable=R0902
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.
@@ -446,7 +451,8 @@ class MeshInterface: # pylint: disable=R0902
wantResponse=wantResponse,
onResponse=onResponse,
channelIndex=channelIndex,
replyId=replyId
replyId=replyId,
hopLimit=hopLimit,
)
@@ -456,6 +462,7 @@ class MeshInterface: # pylint: disable=R0902
destinationId: Union[int, str] = BROADCAST_ADDR,
onResponse: Optional[Callable[[dict], Any]] = None,
channelIndex: int = 0,
hopLimit: Optional[int]=None,
):
"""Send an alert text to some other node. This is similar to a text message,
but carries a higher priority and is capable of generating special notifications
@@ -467,6 +474,7 @@ class MeshInterface: # pylint: disable=R0902
Keyword Arguments:
destinationId {nodeId or nodeNum} -- where to send this
message (default: {BROADCAST_ADDR})
hopLimit {int} -- hop limit to use
Returns the sent packet. The id field will be populated in this packet
and can be used to track future message acks/naks.
@@ -480,7 +488,8 @@ class MeshInterface: # pylint: disable=R0902
wantResponse=False,
onResponse=onResponse,
channelIndex=channelIndex,
priority=mesh_pb2.MeshPacket.Priority.ALERT
priority=mesh_pb2.MeshPacket.Priority.ALERT,
hopLimit=hopLimit,
)
def sendMqttClientProxyMessage(self, topic: str, data: bytes):
@@ -582,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)
@@ -618,6 +628,7 @@ class MeshInterface: # pylint: disable=R0902
wantResponse=wantResponse,
onResponse=onResponse,
channelIndex=channelIndex,
hopLimit=hopLimit,
)
if wantResponse:
self.waitForPosition()
@@ -670,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()
@@ -723,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()
@@ -770,6 +790,7 @@ class MeshInterface: # pylint: disable=R0902
wantResponse=wantResponse,
onResponse=onResponse,
channelIndex=channelIndex,
hopLimit=hopLimit,
)
if wantResponse:
self.waitForTelemetry()
@@ -839,6 +860,7 @@ class MeshInterface: # pylint: disable=R0902
wantAck: bool = True,
wantResponse: bool = False,
channelIndex: int = 0,
hopLimit: Optional[int]=None,
): # pylint: disable=R0913
"""
Send a waypoint packet to some other node (normally a broadcast)
@@ -879,6 +901,7 @@ class MeshInterface: # pylint: disable=R0902
wantResponse=wantResponse,
onResponse=onResponse,
channelIndex=channelIndex,
hopLimit=hopLimit,
)
if wantResponse:
self.waitForWaypoint()
@@ -891,6 +914,7 @@ class MeshInterface: # pylint: disable=R0902
wantAck: bool = True,
wantResponse: bool = False,
channelIndex: int = 0,
hopLimit: Optional[int]=None,
):
"""
Send a waypoint deletion packet to some other node (normally a broadcast)
@@ -917,6 +941,7 @@ class MeshInterface: # pylint: disable=R0902
wantResponse=wantResponse,
onResponse=onResponse,
channelIndex=channelIndex,
hopLimit=hopLimit,
)
if wantResponse:
self.waitForWaypoint()
@@ -1452,6 +1477,10 @@ 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:
logger.debug("Unexpected FromRadio payload")
+125 -9
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,
@@ -18,6 +18,7 @@ from meshtastic.util import (
message_to_json,
generate_channel_hash,
to_node_num,
flags_to_list,
)
logger = logging.getLogger(__name__)
@@ -54,6 +55,16 @@ class Node:
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)
@@ -159,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:
@@ -234,6 +244,8 @@ 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}")
@@ -275,12 +287,22 @@ class Node:
# for sending admin channels will also change
adminIndex = self.iface.localNode._getAdminChannelIndex()
# Snapshot serialized channel payloads from channelIndex onward so we
# can avoid writing slots whose protobuf content did not change after
# the shift. Use bytes (not message objects), because _fixupChannels()
# mutates message fields in-place.
old_channels = [
self.channels[i].SerializeToString()
for i in range(channelIndex, len(self.channels))
]
self.channels.pop(channelIndex)
self._fixupChannels() # expand back to 8 channels
index = channelIndex
while index < 8:
self.writeChannel(index, adminIndex=adminIndex)
for old_ch in old_channels:
if self.channels[index].SerializeToString() != old_ch:
self.writeChannel(index, adminIndex=adminIndex)
index += 1
# if we are updating the local node, we might end up
@@ -368,6 +390,46 @@ class Node:
s = s.replace("=", "").replace("+", "-").replace("/", "_")
return f"https://meshtastic.org/e/#{s}"
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 or self.channels is None:
@@ -433,6 +495,32 @@ class Node:
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"""
logger.debug(f"onResponseRequestRingtone() p:{p}")
@@ -643,7 +731,7 @@ 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
@@ -656,6 +744,22 @@ 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()
@@ -700,10 +804,10 @@ class Node:
self.ensureSessionKey()
p = admin_pb2.AdminMessage()
if full:
p.factory_reset_device = True
p.factory_reset_device = 1
logger.info(f"Telling node to factory reset (full device reset)")
else:
p.factory_reset_config = True
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
@@ -902,6 +1006,18 @@ class Node:
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()"""
+128
View File
@@ -0,0 +1,128 @@
"""Meshtastic ESP32 Unified OTA
"""
import os
import hashlib
import socket
import logging
from typing import Optional, Callable
logger = logging.getLogger(__name__)
def _file_sha256(filename: str):
"""Calculate SHA256 hash of a file."""
sha256_hash = hashlib.sha256()
with open(filename, "rb") as f:
for byte_block in iter(lambda: f.read(4096), b""):
sha256_hash.update(byte_block)
return sha256_hash
class OTAError(Exception):
"""Exception for OTA errors."""
class ESP32WiFiOTA:
"""ESP32 WiFi Unified OTA updates."""
def __init__(self, filename: str, hostname: str, port: int = 3232):
self._filename = filename
self._hostname = hostname
self._port = port
self._socket: Optional[socket.socket] = None
if not os.path.exists(self._filename):
raise FileNotFoundError(f"File {self._filename} does not exist")
self._file_hash = _file_sha256(self._filename)
def _read_line(self) -> str:
"""Read a line from the socket."""
if not self._socket:
raise ConnectionError("Socket not connected")
line = b""
while not line.endswith(b"\n"):
char = self._socket.recv(1)
if not char:
raise ConnectionError("Connection closed while waiting for response")
line += char
return line.decode("utf-8").strip()
def hash_bytes(self) -> bytes:
"""Return the hash as bytes."""
return self._file_hash.digest()
def hash_hex(self) -> str:
"""Return the hash as a hex string."""
return self._file_hash.hexdigest()
def update(self, progress_callback: Optional[Callable[[int, int], None]] = None):
"""Perform the OTA update."""
with open(self._filename, "rb") as f:
data = f.read()
size = len(data)
logger.info(f"Starting OTA update with {self._filename} ({size} bytes, hash {self.hash_hex()})")
self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._socket.settimeout(15)
try:
self._socket.connect((self._hostname, self._port))
logger.debug(f"Connected to {self._hostname}:{self._port}")
# Send start command
self._socket.sendall(f"OTA {size} {self.hash_hex()}\n".encode("utf-8"))
# Wait for OK from the device
while True:
response = self._read_line()
if response == "OK":
break
if response == "ERASING":
logger.info("Device is erasing flash...")
elif response.startswith("ERR "):
raise OTAError(f"Device reported error: {response}")
else:
logger.warning(f"Unexpected response: {response}")
# Stream firmware
sent_bytes = 0
chunk_size = 1024
while sent_bytes < size:
chunk = data[sent_bytes : sent_bytes + chunk_size]
self._socket.sendall(chunk)
sent_bytes += len(chunk)
if progress_callback:
progress_callback(sent_bytes, size)
else:
print(f"[{sent_bytes / size * 100:5.1f}%] Sent {sent_bytes} of {size} bytes...", end="\r")
if not progress_callback:
print()
# Wait for OK from device
logger.info("Firmware sent, waiting for verification...")
while True:
response = self._read_line()
if response == "OK":
logger.info("OTA update completed successfully!")
break
if response.startswith("ERR "):
raise OTAError(f"OTA update failed: {response}")
elif response != "ACK":
logger.warning(f"Unexpected final response: {response}")
finally:
if self._socket:
self._socket.close()
self._socket = None
+78 -21
View File
File diff suppressed because one or more lines are too long.
+651 -6
View File
@@ -25,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):
"""
@@ -186,6 +224,22 @@ 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
"""
MESHBEACON_CONFIG: AdminMessage._ModuleConfigType.ValueType # 16
"""
Mesh Beacon module config
"""
class ModuleConfigType(_ModuleConfigType, metaclass=_ModuleConfigTypeEnumTypeWrapper):
"""
@@ -244,6 +298,22 @@ 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
"""
MESHBEACON_CONFIG: AdminMessage.ModuleConfigType.ValueType # 16
"""
Mesh Beacon module config
"""
class _BackupLocation:
ValueType = typing.NewType("ValueType", builtins.int)
@@ -308,6 +378,34 @@ class AdminMessage(google.protobuf.message.Message):
) -> 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
GET_CHANNEL_RESPONSE_FIELD_NUMBER: builtins.int
@@ -352,6 +450,7 @@ class AdminMessage(google.protobuf.message.Message):
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
@@ -363,6 +462,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.
@@ -480,6 +582,10 @@ class AdminMessage(google.protobuf.message.Message):
"""
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
@@ -497,6 +603,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
"""
@@ -515,9 +622,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:
@@ -632,6 +740,31 @@ class AdminMessage(google.protobuf.message.Message):
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,
*,
@@ -679,6 +812,7 @@ class AdminMessage(google.protobuf.message.Message):
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 = ...,
@@ -689,14 +823,144 @@ class AdminMessage(google.protobuf.message.Message):
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["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", "nodedb_reset", b"nodedb_reset", "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", "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"]) -> 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", "nodedb_reset", b"nodedb_reset", "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", "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"]) -> 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", "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"] | 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
MAX_SESSION_SECONDS_FIELD_NUMBER: builtins.int
DISABLE_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.
"""
max_session_seconds: builtins.int
"""
Optional per-boot uptime cap on the unlocked session, in seconds.
0 = unlimited (token-only enforcement, suitable for unattended
tower / infrastructure nodes).
When non-zero, the firmware arms an uptime timer at unlock. On
each expiry, while there is still boot-count budget, the firmware
decrements the on-flash boot count in place, revokes per-
connection admin auth (clients must re-authenticate to see
content), re-engages the screen lock, and re-arms the timer
without rebooting. Mesh routing keeps running across session
boundaries; only when the boot-count budget reaches zero does
the device hard-lock and reboot.
Total exposure ceiling = ((resolved boot count) + 1) * max_session_seconds.
The +1 accounts for the initial passphrase-unlocked session
itself, since boots_remaining is the number of subsequent
session rolls (each consuming one boot from the rollback ledger).
The resolved boot count is the value the firmware writes into the
token at unlock time: the client-supplied boots_remaining when
non-zero, otherwise the firmware default (TOKEN_DEFAULT_BOOTS).
Note that boots_remaining == 0 in this message means "use firmware
default", NOT "zero boots" — a client computing the ceiling for
display should mirror that resolution rather than multiplying the
raw request value.
The cap is persisted in the token, so it survives token-based
auto-unlock across reboots. Explicit operator Lock Now still
deletes the token and forces passphrase re-entry.
Uses millis() (CPU uptime), not wall-clock time, so the cap is
immune to GPS spoofing, RTC backup-battery removal, and Faraday
cage isolation — none of those move the uptime counter. The only
way to reset the session clock is a reboot, which costs a boot
from the on-flash, HMAC-bound counter.
"""
disable: builtins.bool
"""
Disable lockdown mode. Requires a valid passphrase in the same
message (the device must prove the operator owns it before
reverting at-rest encryption). On success the firmware decrypts
every stored config / channel / nodedb file back to plaintext,
removes the wrapped DEK, unlock token, monotonic-counter, and
backoff files, and reboots out of lockdown.
This is the inverse of the provision/unlock path: it is how the
client app's "lockdown mode" toggle returns a device to normal
operation.
NOT reversed by this operation: APPROTECT. Once the debug port
lockout has been burned (on silicon where it is effective) it is
permanent — disabling lockdown decrypts your data and removes the
access gates, but the SWD/JTAG port stays locked for the life of
the device (recoverable only via a full chip erase over a debug
probe, which destroys all data). Clients should make this
irreversibility clear at the moment lockdown is first enabled.
When true the passphrase field is still required; boots_remaining,
valid_until_epoch, max_session_seconds, and lock_now are ignored.
"""
def __init__(
self,
*,
passphrase: builtins.bytes = ...,
boots_remaining: builtins.int = ...,
valid_until_epoch: builtins.int = ...,
lock_now: builtins.bool = ...,
max_session_seconds: builtins.int = ...,
disable: builtins.bool = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["boots_remaining", b"boots_remaining", "disable", b"disable", "lock_now", b"lock_now", "max_session_seconds", b"max_session_seconds", "passphrase", b"passphrase", "valid_until_epoch", b"valid_until_epoch"]) -> None: ...
global___LockdownAuth = LockdownAuth
@typing.final
class HamParameters(google.protobuf.message.Message):
"""
@@ -709,6 +973,7 @@ class HamParameters(google.protobuf.message.Message):
TX_POWER_FIELD_NUMBER: builtins.int
FREQUENCY_FIELD_NUMBER: builtins.int
SHORT_NAME_FIELD_NUMBER: builtins.int
LONG_NAME_FIELD_NUMBER: builtins.int
call_sign: builtins.str
"""
Amateur radio call sign, eg. KD2ABC
@@ -727,6 +992,11 @@ class HamParameters(google.protobuf.message.Message):
"""
Optional short name of user
"""
long_name: builtins.str
"""
Optional long name of user
Appended to callsign
"""
def __init__(
self,
*,
@@ -734,8 +1004,9 @@ class HamParameters(google.protobuf.message.Message):
tx_power: builtins.int = ...,
frequency: builtins.float = ...,
short_name: builtins.str = ...,
long_name: builtins.str = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["call_sign", b"call_sign", "frequency", b"frequency", "short_name", b"short_name", "tx_power", b"tx_power"]) -> None: ...
def ClearField(self, field_name: typing.Literal["call_sign", b"call_sign", "frequency", b"frequency", "long_name", b"long_name", "short_name", b"short_name", "tx_power", b"tx_power"]) -> None: ...
global___HamParameters = HamParameters
@@ -887,3 +1158,377 @@ class KeyVerificationAdmin(google.protobuf.message.Message):
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
DS248X_CONFIG_FIELD_NUMBER: builtins.int
SEN6X_CONFIG_FIELD_NUMBER: builtins.int
AS3935_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
"""
@property
def ds248x_config(self) -> global___DS248X_config:
"""
DS248X-800 temperature sensor configuration
"""
@property
def sen6x_config(self) -> global___SEN6X_config:
"""
SEN6X PM/RHT/VOC/NOx/CO2/HCHO Sensor configuration
"""
@property
def as3935_config(self) -> global___AS3935_config:
"""
AS3935 lightning 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 = ...,
ds248x_config: global___DS248X_config | None = ...,
sen6x_config: global___SEN6X_config | None = ...,
as3935_config: global___AS3935_config | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["as3935_config", b"as3935_config", "ds248x_config", b"ds248x_config", "scd30_config", b"scd30_config", "scd4x_config", b"scd4x_config", "sen5x_config", b"sen5x_config", "sen6x_config", b"sen6x_config", "shtxx_config", b"shtxx_config"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["as3935_config", b"as3935_config", "ds248x_config", b"ds248x_config", "scd30_config", b"scd30_config", "scd4x_config", b"scd4x_config", "sen5x_config", b"sen5x_config", "sen6x_config", b"sen6x_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
START_FAN_CLEANING_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)
"""
start_fan_cleaning: builtins.bool
"""
Trigger a fan cleaning cycle
"""
def __init__(
self,
*,
set_temperature: builtins.float | None = ...,
set_one_shot_mode: builtins.bool | None = ...,
start_fan_cleaning: 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", "_start_fan_cleaning", b"_start_fan_cleaning", "set_one_shot_mode", b"set_one_shot_mode", "set_temperature", b"set_temperature", "start_fan_cleaning", b"start_fan_cleaning"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["_set_one_shot_mode", b"_set_one_shot_mode", "_set_temperature", b"_set_temperature", "_start_fan_cleaning", b"_start_fan_cleaning", "set_one_shot_mode", b"set_one_shot_mode", "set_temperature", b"set_temperature", "start_fan_cleaning", b"start_fan_cleaning"]) -> 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: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_start_fan_cleaning", b"_start_fan_cleaning"]) -> typing.Literal["start_fan_cleaning"] | None: ...
global___SEN5X_config = SEN5X_config
@typing.final
class SEN6X_config(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
SET_TEMPERATURE_FIELD_NUMBER: builtins.int
SET_ONE_SHOT_MODE_FIELD_NUMBER: builtins.int
START_FAN_CLEANING_FIELD_NUMBER: builtins.int
SET_ASC_FIELD_NUMBER: builtins.int
SET_TARGET_CO2_CONC_FIELD_NUMBER: builtins.int
SET_ALTITUDE_FIELD_NUMBER: builtins.int
SET_AMBIENT_PRESSURE_FIELD_NUMBER: builtins.int
FACTORY_RESET_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)
"""
start_fan_cleaning: builtins.bool
"""
Trigger a fan cleaning cycle
"""
set_asc: builtins.bool
"""
Set Automatic self-calibration enabled (CO2-capable variants only: SEN63C, SEN66, SEN69C)
"""
set_target_co2_conc: builtins.int
"""
Recalibration target CO2 concentration in ppm (FRC only), CO2-capable variants only
"""
set_altitude: builtins.int
"""
Altitude of sensor in meters above sea level. 0 - 3000m (overrides ambient pressure), CO2-capable variants only
"""
set_ambient_pressure: builtins.int
"""
Sensor ambient pressure in Pa. 70000 - 120000 Pa (overrides altitude), CO2-capable variants only
"""
factory_reset: builtins.bool
"""
Perform a factory reset of the CO2 sensor's calibration, CO2-capable variants only
"""
def __init__(
self,
*,
set_temperature: builtins.float | None = ...,
set_one_shot_mode: builtins.bool | None = ...,
start_fan_cleaning: builtins.bool | None = ...,
set_asc: builtins.bool | None = ...,
set_target_co2_conc: builtins.int | None = ...,
set_altitude: builtins.int | None = ...,
set_ambient_pressure: builtins.int | None = ...,
factory_reset: 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_one_shot_mode", b"_set_one_shot_mode", "_set_target_co2_conc", b"_set_target_co2_conc", "_set_temperature", b"_set_temperature", "_start_fan_cleaning", b"_start_fan_cleaning", "factory_reset", b"factory_reset", "set_altitude", b"set_altitude", "set_ambient_pressure", b"set_ambient_pressure", "set_asc", b"set_asc", "set_one_shot_mode", b"set_one_shot_mode", "set_target_co2_conc", b"set_target_co2_conc", "set_temperature", b"set_temperature", "start_fan_cleaning", b"start_fan_cleaning"]) -> 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_one_shot_mode", b"_set_one_shot_mode", "_set_target_co2_conc", b"_set_target_co2_conc", "_set_temperature", b"_set_temperature", "_start_fan_cleaning", b"_start_fan_cleaning", "factory_reset", b"factory_reset", "set_altitude", b"set_altitude", "set_ambient_pressure", b"set_ambient_pressure", "set_asc", b"set_asc", "set_one_shot_mode", b"set_one_shot_mode", "set_target_co2_conc", b"set_target_co2_conc", "set_temperature", b"set_temperature", "start_fan_cleaning", b"start_fan_cleaning"]) -> 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_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_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["_start_fan_cleaning", b"_start_fan_cleaning"]) -> typing.Literal["start_fan_cleaning"] | None: ...
global___SEN6X_config = SEN6X_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
@typing.final
class DS248X_config(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
MAIN_TEMPERATURE_CHANNEL_FIELD_NUMBER: builtins.int
main_temperature_channel: builtins.int
"""
Main channel for temperature reporting (0-7)
"""
def __init__(
self,
*,
main_temperature_channel: builtins.int | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["_main_temperature_channel", b"_main_temperature_channel", "main_temperature_channel", b"main_temperature_channel"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["_main_temperature_channel", b"_main_temperature_channel", "main_temperature_channel", b"main_temperature_channel"]) -> None: ...
def WhichOneof(self, oneof_group: typing.Literal["_main_temperature_channel", b"_main_temperature_channel"]) -> typing.Literal["main_temperature_channel"] | None: ...
global___DS248X_config = DS248X_config
@typing.final
class AS3935_config(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
SET_TUNING_CAP_PF_FIELD_NUMBER: builtins.int
set_tuning_cap_pf: builtins.int
"""
Antenna tuning capacitance in pF, 0 to 120 in steps of 8. The antenna tank must
resonate within 3.5% of 500kHz; the correct trim is specific to the sensor board.
"""
def __init__(
self,
*,
set_tuning_cap_pf: builtins.int | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["_set_tuning_cap_pf", b"_set_tuning_cap_pf", "set_tuning_cap_pf", b"set_tuning_cap_pf"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["_set_tuning_cap_pf", b"_set_tuning_cap_pf", "set_tuning_cap_pf", b"set_tuning_cap_pf"]) -> None: ...
def WhichOneof(self, oneof_group: typing.Literal["_set_tuning_cap_pf", b"_set_tuning_cap_pf"]) -> typing.Literal["set_tuning_cap_pf"] | None: ...
global___AS3935_config = AS3935_config
+6 -3
View File
@@ -13,9 +13,10 @@ _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.LoRaConfigBc\n\x14org.meshtastic.protoB\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)
@@ -23,6 +24,8 @@ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.apponly
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\rAppOnlyProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
_globals['_CHANNELSET']._serialized_start=128
_globals['_CHANNELSET']._serialized_end=257
_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)
+230 -17
View File
File diff suppressed because one or more lines are too long.
+3378 -4
View File
File diff suppressed because it is too large. Load diff
+6 -3
View File
@@ -11,9 +11,10 @@ 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(\tBo\n\x14org.meshtastic.protoB\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)
@@ -21,6 +22,8 @@ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.cannedm
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\031CannedMessageConfigProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
_globals['_CANNEDMESSAGEMODULECONFIG']._serialized_start=65
_globals['_CANNEDMESSAGEMODULECONFIG']._serialized_end=110
_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)
+16 -9
View File
@@ -11,9 +11,10 @@ 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\">\n\x0eModuleSettings\x12\x1a\n\x12position_precision\x18\x01 \x01(\r\x12\x10\n\x08is_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\x63\n\x14org.meshtastic.protoB\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)
@@ -23,12 +24,18 @@ if _descriptor._USE_C_DESCRIPTORS == False:
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=316
_globals['_CHANNEL']._serialized_start=319
_globals['_CHANNEL']._serialized_end=498
_globals['_CHANNEL_ROLE']._serialized_start=450
_globals['_CHANNEL_ROLE']._serialized_end=498
_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)
+1 -1
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
+12 -3
View File
@@ -13,9 +13,10 @@ _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_messagesBf\n\x14org.meshtastic.protoB\x10\x43lientOnlyProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n$meshtastic/protobuf/clientonly.proto\x12\x13meshtastic.protobuf\x1a#meshtastic/protobuf/localonly.proto\x1a\x1emeshtastic/protobuf/mesh.proto\x1a meshtastic/protobuf/nanopb.proto\"\xbe\x04\n\rDeviceProfile\x12\x1d\n\tlong_name\x18\x01 \x01(\tB\x05\x92?\x02\x08\x19H\x00\x88\x01\x01\x12\x1e\n\nshort_name\x18\x02 \x01(\tB\x05\x92?\x02\x08\x05H\x01\x88\x01\x01\x12\x18\n\x0b\x63hannel_url\x18\x03 \x01(\tH\x02\x88\x01\x01\x12\x35\n\x06\x63onfig\x18\x04 \x01(\x0b\x32 .meshtastic.protobuf.LocalConfigH\x03\x88\x01\x01\x12\x42\n\rmodule_config\x18\x05 \x01(\x0b\x32&.meshtastic.protobuf.LocalModuleConfigH\x04\x88\x01\x01\x12:\n\x0e\x66ixed_position\x18\x06 \x01(\x0b\x32\x1d.meshtastic.protobuf.PositionH\x05\x88\x01\x01\x12\x1d\n\x08ringtone\x18\x07 \x01(\tB\x06\x92?\x03\x08\xe7\x01H\x06\x88\x01\x01\x12$\n\x0f\x63\x61nned_messages\x18\x08 \x01(\tB\x06\x92?\x03\x08\xc9\x01H\x07\x88\x01\x01\x12\x1c\n\x0fis_unmessagable\x18\t \x01(\x08H\x08\x88\x01\x01\x12\x18\n\x0bis_licensed\x18\n \x01(\x08H\t\x88\x01\x01\x42\x0c\n\n_long_nameB\r\n\x0b_short_nameB\x0e\n\x0c_channel_urlB\t\n\x07_configB\x10\n\x0e_module_configB\x11\n\x0f_fixed_positionB\x0b\n\t_ringtoneB\x12\n\x10_canned_messagesB\x12\n\x10_is_unmessagableB\x0e\n\x0c_is_licensedBf\n\x14org.meshtastic.protoB\x10\x43lientOnlyProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
@@ -23,6 +24,14 @@ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.cliento
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\020ClientOnlyProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
_globals['_DEVICEPROFILE']._serialized_start=131
_globals['_DEVICEPROFILE']._serialized_end=583
_DEVICEPROFILE.fields_by_name['long_name']._options = None
_DEVICEPROFILE.fields_by_name['long_name']._serialized_options = b'\222?\002\010\031'
_DEVICEPROFILE.fields_by_name['short_name']._options = None
_DEVICEPROFILE.fields_by_name['short_name']._serialized_options = b'\222?\002\010\005'
_DEVICEPROFILE.fields_by_name['ringtone']._options = None
_DEVICEPROFILE.fields_by_name['ringtone']._serialized_options = b'\222?\003\010\347\001'
_DEVICEPROFILE.fields_by_name['canned_messages']._options = None
_DEVICEPROFILE.fields_by_name['canned_messages']._serialized_options = b'\222?\003\010\311\001'
_globals['_DEVICEPROFILE']._serialized_start=165
_globals['_DEVICEPROFILE']._serialized_end=739
# @@protoc_insertion_point(module_scope)
+18 -2
View File
@@ -29,6 +29,8 @@ class DeviceProfile(google.protobuf.message.Message):
FIXED_POSITION_FIELD_NUMBER: builtins.int
RINGTONE_FIELD_NUMBER: builtins.int
CANNED_MESSAGES_FIELD_NUMBER: builtins.int
IS_UNMESSAGABLE_FIELD_NUMBER: builtins.int
IS_LICENSED_FIELD_NUMBER: builtins.int
long_name: builtins.str
"""
Long name for the node
@@ -49,6 +51,14 @@ class DeviceProfile(google.protobuf.message.Message):
"""
Predefined messages for CannedMessage
"""
is_unmessagable: builtins.bool
"""
Is the node unmessagable
"""
is_licensed: builtins.bool
"""
Is this node in licensed user mode
"""
@property
def config(self) -> meshtastic.protobuf.localonly_pb2.LocalConfig:
"""
@@ -78,9 +88,11 @@ class DeviceProfile(google.protobuf.message.Message):
fixed_position: meshtastic.protobuf.mesh_pb2.Position | None = ...,
ringtone: builtins.str | None = ...,
canned_messages: builtins.str | None = ...,
is_unmessagable: builtins.bool | None = ...,
is_licensed: builtins.bool | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["_canned_messages", b"_canned_messages", "_channel_url", b"_channel_url", "_config", b"_config", "_fixed_position", b"_fixed_position", "_long_name", b"_long_name", "_module_config", b"_module_config", "_ringtone", b"_ringtone", "_short_name", b"_short_name", "canned_messages", b"canned_messages", "channel_url", b"channel_url", "config", b"config", "fixed_position", b"fixed_position", "long_name", b"long_name", "module_config", b"module_config", "ringtone", b"ringtone", "short_name", b"short_name"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["_canned_messages", b"_canned_messages", "_channel_url", b"_channel_url", "_config", b"_config", "_fixed_position", b"_fixed_position", "_long_name", b"_long_name", "_module_config", b"_module_config", "_ringtone", b"_ringtone", "_short_name", b"_short_name", "canned_messages", b"canned_messages", "channel_url", b"channel_url", "config", b"config", "fixed_position", b"fixed_position", "long_name", b"long_name", "module_config", b"module_config", "ringtone", b"ringtone", "short_name", b"short_name"]) -> None: ...
def HasField(self, field_name: typing.Literal["_canned_messages", b"_canned_messages", "_channel_url", b"_channel_url", "_config", b"_config", "_fixed_position", b"_fixed_position", "_is_licensed", b"_is_licensed", "_is_unmessagable", b"_is_unmessagable", "_long_name", b"_long_name", "_module_config", b"_module_config", "_ringtone", b"_ringtone", "_short_name", b"_short_name", "canned_messages", b"canned_messages", "channel_url", b"channel_url", "config", b"config", "fixed_position", b"fixed_position", "is_licensed", b"is_licensed", "is_unmessagable", b"is_unmessagable", "long_name", b"long_name", "module_config", b"module_config", "ringtone", b"ringtone", "short_name", b"short_name"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["_canned_messages", b"_canned_messages", "_channel_url", b"_channel_url", "_config", b"_config", "_fixed_position", b"_fixed_position", "_is_licensed", b"_is_licensed", "_is_unmessagable", b"_is_unmessagable", "_long_name", b"_long_name", "_module_config", b"_module_config", "_ringtone", b"_ringtone", "_short_name", b"_short_name", "canned_messages", b"canned_messages", "channel_url", b"channel_url", "config", b"config", "fixed_position", b"fixed_position", "is_licensed", b"is_licensed", "is_unmessagable", b"is_unmessagable", "long_name", b"long_name", "module_config", b"module_config", "ringtone", b"ringtone", "short_name", b"short_name"]) -> None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_canned_messages", b"_canned_messages"]) -> typing.Literal["canned_messages"] | None: ...
@typing.overload
@@ -90,6 +102,10 @@ class DeviceProfile(google.protobuf.message.Message):
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_fixed_position", b"_fixed_position"]) -> typing.Literal["fixed_position"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_is_licensed", b"_is_licensed"]) -> typing.Literal["is_licensed"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_is_unmessagable", b"_is_unmessagable"]) -> typing.Literal["is_unmessagable"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_long_name", b"_long_name"]) -> typing.Literal["long_name"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_module_config", b"_module_config"]) -> typing.Literal["module_config"] | None: ...
+92 -53
View File
File diff suppressed because one or more lines are too long.
+315 -5
View File
@@ -119,7 +119,7 @@ class Config(google.protobuf.message.Message):
"""
CLIENT_BASE: Config.DeviceConfig._Role.ValueType # 12
"""
Description: Treats packets from or to favorited nodes as ROUTER, and all other packets as CLIENT.
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.
@@ -211,7 +211,7 @@ class Config(google.protobuf.message.Message):
"""
CLIENT_BASE: Config.DeviceConfig.Role.ValueType # 12
"""
Description: Treats packets from or to favorited nodes as ROUTER, and all other packets as CLIENT.
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.
@@ -1010,6 +1010,10 @@ class Config(google.protobuf.message.Message):
"""
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):
"""
@@ -1036,6 +1040,10 @@ class Config(google.protobuf.message.Message):
"""
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)
@@ -1164,6 +1172,7 @@ class Config(google.protobuf.message.Message):
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
@@ -1222,6 +1231,10 @@ class Config(google.protobuf.message.Message):
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,
*,
@@ -1238,8 +1251,9 @@ class Config(google.protobuf.message.Message):
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", "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: ...
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):
@@ -1363,6 +1377,52 @@ class Config(google.protobuf.message.Message):
"""
Brazil 902MHz
"""
ITU1_2M: Config.LoRaConfig._RegionCode.ValueType # 27
"""
ITU Region 1 Amateur Radio 2m band (144-146 MHz)
"""
ITU2_2M: Config.LoRaConfig._RegionCode.ValueType # 28
"""
ITU Region 2 Amateur Radio 2m band (144-148 MHz)
"""
EU_866: Config.LoRaConfig._RegionCode.ValueType # 29
"""
EU 866MHz band (Band no. 47b of 2006/771/EC and subsequent amendments) for Non-specific short-range devices (SRD)
"""
EU_874: Config.LoRaConfig._RegionCode.ValueType # 30
"""
EU 874MHz and 917MHz bands (Band no. 1 and 4 of 2022/172/EC and subsequent amendments) for Non-specific short-range devices (SRD)
"""
EU_917: Config.LoRaConfig._RegionCode.ValueType # 31
EU_N_868: Config.LoRaConfig._RegionCode.ValueType # 32
"""
EU 868MHz band, with narrow presets
"""
ITU3_2M: Config.LoRaConfig._RegionCode.ValueType # 33
"""
ITU Region 3 Amateur Radio 2m band (144-148 MHz)
"""
ITU1_70CM: Config.LoRaConfig._RegionCode.ValueType # 34
"""
ITU Region 1 Amateur Radio 70cm band (430-440 MHz)
"""
ITU2_70CM: Config.LoRaConfig._RegionCode.ValueType # 35
"""
ITU Region 2 Amateur Radio 70cm band (420-450 MHz)
Note: Some countries do not allocate 420-430 MHz or 440-450 MHz.
Check local law!
"""
ITU3_70CM: Config.LoRaConfig._RegionCode.ValueType # 36
"""
ITU Region 3 Amateur Radio 70cm band (430-450 MHz)
Note: Some countries do not allocate 440-450 MHz. Check local law!
"""
ITU2_125CM: Config.LoRaConfig._RegionCode.ValueType # 37
"""
ITU Region 2 Amateur Radio 1.25m '125cm' band (220-225 MHz)
Note: Some countries do not allocate 220-222 MHz (Ex: USA/Canada).
Check local law!
"""
class RegionCode(_RegionCode, metaclass=_RegionCodeEnumTypeWrapper): ...
UNSET: Config.LoRaConfig.RegionCode.ValueType # 0
@@ -1473,6 +1533,52 @@ class Config(google.protobuf.message.Message):
"""
Brazil 902MHz
"""
ITU1_2M: Config.LoRaConfig.RegionCode.ValueType # 27
"""
ITU Region 1 Amateur Radio 2m band (144-146 MHz)
"""
ITU2_2M: Config.LoRaConfig.RegionCode.ValueType # 28
"""
ITU Region 2 Amateur Radio 2m band (144-148 MHz)
"""
EU_866: Config.LoRaConfig.RegionCode.ValueType # 29
"""
EU 866MHz band (Band no. 47b of 2006/771/EC and subsequent amendments) for Non-specific short-range devices (SRD)
"""
EU_874: Config.LoRaConfig.RegionCode.ValueType # 30
"""
EU 874MHz and 917MHz bands (Band no. 1 and 4 of 2022/172/EC and subsequent amendments) for Non-specific short-range devices (SRD)
"""
EU_917: Config.LoRaConfig.RegionCode.ValueType # 31
EU_N_868: Config.LoRaConfig.RegionCode.ValueType # 32
"""
EU 868MHz band, with narrow presets
"""
ITU3_2M: Config.LoRaConfig.RegionCode.ValueType # 33
"""
ITU Region 3 Amateur Radio 2m band (144-148 MHz)
"""
ITU1_70CM: Config.LoRaConfig.RegionCode.ValueType # 34
"""
ITU Region 1 Amateur Radio 70cm band (430-440 MHz)
"""
ITU2_70CM: Config.LoRaConfig.RegionCode.ValueType # 35
"""
ITU Region 2 Amateur Radio 70cm band (420-450 MHz)
Note: Some countries do not allocate 420-430 MHz or 440-450 MHz.
Check local law!
"""
ITU3_70CM: Config.LoRaConfig.RegionCode.ValueType # 36
"""
ITU Region 3 Amateur Radio 70cm band (430-450 MHz)
Note: Some countries do not allocate 440-450 MHz. Check local law!
"""
ITU2_125CM: Config.LoRaConfig.RegionCode.ValueType # 37
"""
ITU Region 2 Amateur Radio 1.25m '125cm' band (220-225 MHz)
Note: Some countries do not allocate 220-222 MHz (Ex: USA/Canada).
Check local law!
"""
class _ModemPreset:
ValueType = typing.NewType("ValueType", builtins.int)
@@ -1487,6 +1593,7 @@ class Config(google.protobuf.message.Message):
LONG_SLOW: Config.LoRaConfig._ModemPreset.ValueType # 1
"""
Long Range - Slow
Deprecated in 2.7: Unpopular slow preset.
"""
VERY_LONG_SLOW: Config.LoRaConfig._ModemPreset.ValueType # 2
"""
@@ -1519,6 +1626,60 @@ class Config(google.protobuf.message.Message):
This is the fastest preset and the only one with 500kHz bandwidth.
It is not legal to use in all regions due to this wider bandwidth.
"""
LONG_TURBO: Config.LoRaConfig._ModemPreset.ValueType # 9
"""
Long Range - Turbo
This preset performs similarly to LongFast, but with 500Khz bandwidth.
"""
LITE_FAST: Config.LoRaConfig._ModemPreset.ValueType # 10
"""
Lite Fast
Medium range preset optimized for EU 866MHz SRD band with 125kHz bandwidth.
Comparable link budget to MEDIUM_FAST but compliant with Band no. 47b of 2006/771/EC.
"""
LITE_SLOW: Config.LoRaConfig._ModemPreset.ValueType # 11
"""
Lite Slow
Medium-to-moderate range preset optimized for EU 866MHz SRD band with 125kHz bandwidth.
Comparable link budget to LONG_FAST but compliant with Band no. 47b of 2006/771/EC.
"""
NARROW_FAST: Config.LoRaConfig._ModemPreset.ValueType # 12
"""
Narrow Fast
Medium-to-moderate range preset optimized for EU 868MHz band with 62.5kHz bandwidth.
Comparable link budget to SHORT_SLOW, but with half the data rate.
Intended to avoid interference with other devices.
"""
NARROW_SLOW: Config.LoRaConfig._ModemPreset.ValueType # 13
"""
Narrow Slow
Moderate range preset optimized for EU 868MHz band with 62.5kHz bandwidth.
Comparable link budget and data rate to LONG_FAST.
"""
TINY_FAST: Config.LoRaConfig._ModemPreset.ValueType # 14
"""
Tiny Fast
Preset optimized for compliance with Amateur Radio restrictions with 20kHz bandwidth.
Many regions limit data transmission bandwidth in lower amateur bands (2 Meter).
Note: TCXO with tight tolerances (±5 ppm or better) is *absolutely required* at these narrow bandwidths.
Only compatible with SX127x and SX126x chipsets.
Comparable link budget and data rate to LONG_FAST.
"""
TINY_SLOW: Config.LoRaConfig._ModemPreset.ValueType # 15
"""
Tiny Slow
Preset optimized for compliance with Amateur Radio restrictions with 20kHz bandwidth.
Many regions limit data transmission bandwidth in lower amateur bands (2 Meter).
Note: TCXO with tight tolerances (±5 ppm or better) is *absolutely required* at these narrow bandwidths.
Only compatible with SX127x and SX126x chipsets.
Comparable link budget and data rate to LONG_MODERATE.
"""
MEDIUM_TURBO: Config.LoRaConfig._ModemPreset.ValueType # 16
"""
Medium Range - Turbo
This preset performs similarly to MEDIUM_FAST, but with 500kHz bandwidth.
It is not legal to use in all regions due to this wider bandwidth.
"""
class ModemPreset(_ModemPreset, metaclass=_ModemPresetEnumTypeWrapper):
"""
@@ -1533,6 +1694,7 @@ class Config(google.protobuf.message.Message):
LONG_SLOW: Config.LoRaConfig.ModemPreset.ValueType # 1
"""
Long Range - Slow
Deprecated in 2.7: Unpopular slow preset.
"""
VERY_LONG_SLOW: Config.LoRaConfig.ModemPreset.ValueType # 2
"""
@@ -1565,6 +1727,93 @@ class Config(google.protobuf.message.Message):
This is the fastest preset and the only one with 500kHz bandwidth.
It is not legal to use in all regions due to this wider bandwidth.
"""
LONG_TURBO: Config.LoRaConfig.ModemPreset.ValueType # 9
"""
Long Range - Turbo
This preset performs similarly to LongFast, but with 500Khz bandwidth.
"""
LITE_FAST: Config.LoRaConfig.ModemPreset.ValueType # 10
"""
Lite Fast
Medium range preset optimized for EU 866MHz SRD band with 125kHz bandwidth.
Comparable link budget to MEDIUM_FAST but compliant with Band no. 47b of 2006/771/EC.
"""
LITE_SLOW: Config.LoRaConfig.ModemPreset.ValueType # 11
"""
Lite Slow
Medium-to-moderate range preset optimized for EU 866MHz SRD band with 125kHz bandwidth.
Comparable link budget to LONG_FAST but compliant with Band no. 47b of 2006/771/EC.
"""
NARROW_FAST: Config.LoRaConfig.ModemPreset.ValueType # 12
"""
Narrow Fast
Medium-to-moderate range preset optimized for EU 868MHz band with 62.5kHz bandwidth.
Comparable link budget to SHORT_SLOW, but with half the data rate.
Intended to avoid interference with other devices.
"""
NARROW_SLOW: Config.LoRaConfig.ModemPreset.ValueType # 13
"""
Narrow Slow
Moderate range preset optimized for EU 868MHz band with 62.5kHz bandwidth.
Comparable link budget and data rate to LONG_FAST.
"""
TINY_FAST: Config.LoRaConfig.ModemPreset.ValueType # 14
"""
Tiny Fast
Preset optimized for compliance with Amateur Radio restrictions with 20kHz bandwidth.
Many regions limit data transmission bandwidth in lower amateur bands (2 Meter).
Note: TCXO with tight tolerances (±5 ppm or better) is *absolutely required* at these narrow bandwidths.
Only compatible with SX127x and SX126x chipsets.
Comparable link budget and data rate to LONG_FAST.
"""
TINY_SLOW: Config.LoRaConfig.ModemPreset.ValueType # 15
"""
Tiny Slow
Preset optimized for compliance with Amateur Radio restrictions with 20kHz bandwidth.
Many regions limit data transmission bandwidth in lower amateur bands (2 Meter).
Note: TCXO with tight tolerances (±5 ppm or better) is *absolutely required* at these narrow bandwidths.
Only compatible with SX127x and SX126x chipsets.
Comparable link budget and data rate to LONG_MODERATE.
"""
MEDIUM_TURBO: Config.LoRaConfig.ModemPreset.ValueType # 16
"""
Medium Range - Turbo
This preset performs similarly to MEDIUM_FAST, but with 500kHz bandwidth.
It is not legal to use in all regions due to this wider bandwidth.
"""
class _FEM_LNA_Mode:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
class _FEM_LNA_ModeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[Config.LoRaConfig._FEM_LNA_Mode.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
DISABLED: Config.LoRaConfig._FEM_LNA_Mode.ValueType # 0
"""
FEM_LNA is present but disabled
"""
ENABLED: Config.LoRaConfig._FEM_LNA_Mode.ValueType # 1
"""
FEM_LNA is present and enabled
"""
NOT_PRESENT: Config.LoRaConfig._FEM_LNA_Mode.ValueType # 2
"""
FEM_LNA is not present on the device
"""
class FEM_LNA_Mode(_FEM_LNA_Mode, metaclass=_FEM_LNA_ModeEnumTypeWrapper): ...
DISABLED: Config.LoRaConfig.FEM_LNA_Mode.ValueType # 0
"""
FEM_LNA is present but disabled
"""
ENABLED: Config.LoRaConfig.FEM_LNA_Mode.ValueType # 1
"""
FEM_LNA is present and enabled
"""
NOT_PRESENT: Config.LoRaConfig.FEM_LNA_Mode.ValueType # 2
"""
FEM_LNA is not present on the device
"""
USE_PRESET_FIELD_NUMBER: builtins.int
MODEM_PRESET_FIELD_NUMBER: builtins.int
@@ -1584,6 +1833,8 @@ class Config(google.protobuf.message.Message):
IGNORE_INCOMING_FIELD_NUMBER: builtins.int
IGNORE_MQTT_FIELD_NUMBER: builtins.int
CONFIG_OK_TO_MQTT_FIELD_NUMBER: builtins.int
FEM_LNA_MODE_FIELD_NUMBER: builtins.int
SERIAL_HAL_ONLY_FIELD_NUMBER: builtins.int
use_preset: builtins.bool
"""
When enabled, the `modem_preset` fields will be adhered to, else the `bandwidth`/`spread_factor`/`coding_rate`
@@ -1681,6 +1932,14 @@ class Config(google.protobuf.message.Message):
"""
Sets the ok_to_mqtt bit on outgoing packets
"""
fem_lna_mode: global___Config.LoRaConfig.FEM_LNA_Mode.ValueType
"""
Set where LORA FEM is enabled, disabled, or not present
"""
serial_hal_only: builtins.bool
"""
Don't use radiolib to initialize the radio, instead listen for a serialHal connection
"""
@property
def ignore_incoming(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]:
"""
@@ -1710,8 +1969,10 @@ class Config(google.protobuf.message.Message):
ignore_incoming: collections.abc.Iterable[builtins.int] | None = ...,
ignore_mqtt: builtins.bool = ...,
config_ok_to_mqtt: builtins.bool = ...,
fem_lna_mode: global___Config.LoRaConfig.FEM_LNA_Mode.ValueType = ...,
serial_hal_only: builtins.bool = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["bandwidth", b"bandwidth", "channel_num", b"channel_num", "coding_rate", b"coding_rate", "config_ok_to_mqtt", b"config_ok_to_mqtt", "frequency_offset", b"frequency_offset", "hop_limit", b"hop_limit", "ignore_incoming", b"ignore_incoming", "ignore_mqtt", b"ignore_mqtt", "modem_preset", b"modem_preset", "override_duty_cycle", b"override_duty_cycle", "override_frequency", b"override_frequency", "pa_fan_disabled", b"pa_fan_disabled", "region", b"region", "spread_factor", b"spread_factor", "sx126x_rx_boosted_gain", b"sx126x_rx_boosted_gain", "tx_enabled", b"tx_enabled", "tx_power", b"tx_power", "use_preset", b"use_preset"]) -> None: ...
def ClearField(self, field_name: typing.Literal["bandwidth", b"bandwidth", "channel_num", b"channel_num", "coding_rate", b"coding_rate", "config_ok_to_mqtt", b"config_ok_to_mqtt", "fem_lna_mode", b"fem_lna_mode", "frequency_offset", b"frequency_offset", "hop_limit", b"hop_limit", "ignore_incoming", b"ignore_incoming", "ignore_mqtt", b"ignore_mqtt", "modem_preset", b"modem_preset", "override_duty_cycle", b"override_duty_cycle", "override_frequency", b"override_frequency", "pa_fan_disabled", b"pa_fan_disabled", "region", b"region", "serial_hal_only", b"serial_hal_only", "spread_factor", b"spread_factor", "sx126x_rx_boosted_gain", b"sx126x_rx_boosted_gain", "tx_enabled", b"tx_enabled", "tx_power", b"tx_power", "use_preset", b"use_preset"]) -> None: ...
@typing.final
class BluetoothConfig(google.protobuf.message.Message):
@@ -1778,6 +2039,49 @@ class Config(google.protobuf.message.Message):
class SecurityConfig(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
class _PacketSignaturePolicy:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
class _PacketSignaturePolicyEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[Config.SecurityConfig._PacketSignaturePolicy.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
PACKET_SIGNATURE_POLICY_COMPATIBLE: Config.SecurityConfig._PacketSignaturePolicy.ValueType # 0
"""
Accept unsigned packets for maximum compatibility while still rejecting malformed or invalid signatures.
This is the default to avoid legacy nodes dropping signed packets during rebroadcast.
"""
PACKET_SIGNATURE_POLICY_BALANCED: Config.SecurityConfig._PacketSignaturePolicy.ValueType # 1
"""
Prefer authenticated packets while retaining compatibility with unsigned packets from nodes not known to sign.
Rejects unsigned, signable broadcasts from nodes that have previously signed.
"""
PACKET_SIGNATURE_POLICY_STRICT: Config.SecurityConfig._PacketSignaturePolicy.ValueType # 2
"""
Accept only packets authenticated by a verified XEdDSA signature or successful PKI decryption.
Unsigned, malformed, invalid, or unverifiable packets are ignored.
"""
class PacketSignaturePolicy(_PacketSignaturePolicy, metaclass=_PacketSignaturePolicyEnumTypeWrapper):
"""
Controls how the device authenticates remotely received mesh packets.
"""
PACKET_SIGNATURE_POLICY_COMPATIBLE: Config.SecurityConfig.PacketSignaturePolicy.ValueType # 0
"""
Accept unsigned packets for maximum compatibility while still rejecting malformed or invalid signatures.
This is the default to avoid legacy nodes dropping signed packets during rebroadcast.
"""
PACKET_SIGNATURE_POLICY_BALANCED: Config.SecurityConfig.PacketSignaturePolicy.ValueType # 1
"""
Prefer authenticated packets while retaining compatibility with unsigned packets from nodes not known to sign.
Rejects unsigned, signable broadcasts from nodes that have previously signed.
"""
PACKET_SIGNATURE_POLICY_STRICT: Config.SecurityConfig.PacketSignaturePolicy.ValueType # 2
"""
Accept only packets authenticated by a verified XEdDSA signature or successful PKI decryption.
Unsigned, malformed, invalid, or unverifiable packets are ignored.
"""
PUBLIC_KEY_FIELD_NUMBER: builtins.int
PRIVATE_KEY_FIELD_NUMBER: builtins.int
ADMIN_KEY_FIELD_NUMBER: builtins.int
@@ -1785,6 +2089,7 @@ class Config(google.protobuf.message.Message):
SERIAL_ENABLED_FIELD_NUMBER: builtins.int
DEBUG_LOG_API_ENABLED_FIELD_NUMBER: builtins.int
ADMIN_CHANNEL_ENABLED_FIELD_NUMBER: builtins.int
PACKET_SIGNATURE_POLICY_FIELD_NUMBER: builtins.int
public_key: builtins.bytes
"""
The public key of the user's device.
@@ -1813,6 +2118,10 @@ class Config(google.protobuf.message.Message):
"""
Allow incoming device control over the insecure legacy admin channel.
"""
packet_signature_policy: global___Config.SecurityConfig.PacketSignaturePolicy.ValueType
"""
Determines the packet signature policy applied to remotely received mesh packets.
"""
@property
def admin_key(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.bytes]:
"""
@@ -1829,8 +2138,9 @@ class Config(google.protobuf.message.Message):
serial_enabled: builtins.bool = ...,
debug_log_api_enabled: builtins.bool = ...,
admin_channel_enabled: builtins.bool = ...,
packet_signature_policy: global___Config.SecurityConfig.PacketSignaturePolicy.ValueType = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["admin_channel_enabled", b"admin_channel_enabled", "admin_key", b"admin_key", "debug_log_api_enabled", b"debug_log_api_enabled", "is_managed", b"is_managed", "private_key", b"private_key", "public_key", b"public_key", "serial_enabled", b"serial_enabled"]) -> None: ...
def ClearField(self, field_name: typing.Literal["admin_channel_enabled", b"admin_channel_enabled", "admin_key", b"admin_key", "debug_log_api_enabled", b"debug_log_api_enabled", "is_managed", b"is_managed", "packet_signature_policy", b"packet_signature_policy", "private_key", b"private_key", "public_key", b"public_key", "serial_enabled", b"serial_enabled"]) -> None: ...
@typing.final
class SessionkeyConfig(google.protobuf.message.Message):
+16 -13
View File
@@ -11,9 +11,10 @@ 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\x66\n\x14org.meshtastic.protoB\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)
@@ -21,16 +22,18 @@ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.connect
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\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
_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)
+44 -19
View File
@@ -11,9 +11,10 @@ from google.protobuf.internal import builder as _builder
_sym_db = _symbol_database.Default()
from meshtastic.protobuf import nanopb_pb2 as meshtastic_dot_protobuf_dot_nanopb__pb2
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n#meshtastic/protobuf/device_ui.proto\x12\x13meshtastic.protobuf\"\xff\x05\n\x0e\x44\x65viceUIConfig\x12\x0f\n\x07version\x18\x01 \x01(\r\x12\x19\n\x11screen_brightness\x18\x02 \x01(\r\x12\x16\n\x0escreen_timeout\x18\x03 \x01(\r\x12\x13\n\x0bscreen_lock\x18\x04 \x01(\x08\x12\x15\n\rsettings_lock\x18\x05 \x01(\x08\x12\x10\n\x08pin_code\x18\x06 \x01(\r\x12)\n\x05theme\x18\x07 \x01(\x0e\x32\x1a.meshtastic.protobuf.Theme\x12\x15\n\ralert_enabled\x18\x08 \x01(\x08\x12\x16\n\x0e\x62\x61nner_enabled\x18\t \x01(\x08\x12\x14\n\x0cring_tone_id\x18\n \x01(\r\x12/\n\x08language\x18\x0b \x01(\x0e\x32\x1d.meshtastic.protobuf.Language\x12\x34\n\x0bnode_filter\x18\x0c \x01(\x0b\x32\x1f.meshtastic.protobuf.NodeFilter\x12:\n\x0enode_highlight\x18\r \x01(\x0b\x32\".meshtastic.protobuf.NodeHighlight\x12\x18\n\x10\x63\x61libration_data\x18\x0e \x01(\x0c\x12*\n\x08map_data\x18\x0f \x01(\x0b\x32\x18.meshtastic.protobuf.Map\x12\x36\n\x0c\x63ompass_mode\x18\x10 \x01(\x0e\x32 .meshtastic.protobuf.CompassMode\x12\x18\n\x10screen_rgb_color\x18\x11 \x01(\r\x12\x1b\n\x13is_clockface_analog\x18\x12 \x01(\x08\x12K\n\ngps_format\x18\x13 \x01(\x0e\x32\x37.meshtastic.protobuf.DeviceUIConfig.GpsCoordinateFormat\"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\"\xa7\x01\n\nNodeFilter\x12\x16\n\x0eunknown_switch\x18\x01 \x01(\x08\x12\x16\n\x0eoffline_switch\x18\x02 \x01(\x08\x12\x19\n\x11public_key_switch\x18\x03 \x01(\x08\x12\x11\n\thops_away\x18\x04 \x01(\x05\x12\x17\n\x0fposition_switch\x18\x05 \x01(\x08\x12\x11\n\tnode_name\x18\x06 \x01(\t\x12\x0f\n\x07\x63hannel\x18\x07 \x01(\x05\"~\n\rNodeHighlight\x12\x13\n\x0b\x63hat_switch\x18\x01 \x01(\x08\x12\x17\n\x0fposition_switch\x18\x02 \x01(\x08\x12\x18\n\x10telemetry_switch\x18\x03 \x01(\x08\x12\x12\n\niaq_switch\x18\x04 \x01(\x08\x12\x11\n\tnode_name\x18\x05 \x01(\t\"=\n\x08GeoPoint\x12\x0c\n\x04zoom\x18\x01 \x01(\x05\x12\x10\n\x08latitude\x18\x02 \x01(\x05\x12\x11\n\tlongitude\x18\x03 \x01(\x05\"U\n\x03Map\x12+\n\x04home\x18\x01 \x01(\x0b\x32\x1d.meshtastic.protobuf.GeoPoint\x12\r\n\x05style\x18\x02 \x01(\t\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')
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)
@@ -21,22 +22,46 @@ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.device_
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'
_globals['_COMPASSMODE']._serialized_start=1278
_globals['_COMPASSMODE']._serialized_end=1340
_globals['_THEME']._serialized_start=1342
_globals['_THEME']._serialized_end=1379
_globals['_LANGUAGE']._serialized_start=1382
_globals['_LANGUAGE']._serialized_end=1702
_globals['_DEVICEUICONFIG']._serialized_start=61
_globals['_DEVICEUICONFIG']._serialized_end=828
_globals['_DEVICEUICONFIG_GPSCOORDINATEFORMAT']._serialized_start=742
_globals['_DEVICEUICONFIG_GPSCOORDINATEFORMAT']._serialized_end=828
_globals['_NODEFILTER']._serialized_start=831
_globals['_NODEFILTER']._serialized_end=998
_globals['_NODEHIGHLIGHT']._serialized_start=1000
_globals['_NODEHIGHLIGHT']._serialized_end=1126
_globals['_GEOPOINT']._serialized_start=1128
_globals['_GEOPOINT']._serialized_end=1189
_globals['_MAP']._serialized_start=1191
_globals['_MAP']._serialized_end=1276
_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)
+39
View File
@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: meshtastic/protobuf/deviceonly_legacy.proto
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
from meshtastic.protobuf import deviceonly_pb2 as meshtastic_dot_protobuf_dot_deviceonly__pb2
from meshtastic.protobuf import telemetry_pb2 as meshtastic_dot_protobuf_dot_telemetry__pb2
from meshtastic.protobuf import nanopb_pb2 as meshtastic_dot_protobuf_dot_nanopb__pb2
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n+meshtastic/protobuf/deviceonly_legacy.proto\x12\x13meshtastic.protobuf\x1a$meshtastic/protobuf/deviceonly.proto\x1a#meshtastic/protobuf/telemetry.proto\x1a meshtastic/protobuf/nanopb.proto\"\x8c\x03\n\x13NodeInfoLite_Legacy\x12\x0b\n\x03num\x18\x01 \x01(\r\x12+\n\x04user\x18\x02 \x01(\x0b\x32\x1d.meshtastic.protobuf.UserLite\x12\x33\n\x08position\x18\x03 \x01(\x0b\x32!.meshtastic.protobuf.PositionLite\x12\x0b\n\x03snr\x18\x04 \x01(\x02\x12\x12\n\nlast_heard\x18\x05 \x01(\x07\x12:\n\x0e\x64\x65vice_metrics\x18\x06 \x01(\x0b\x32\".meshtastic.protobuf.DeviceMetrics\x12\x16\n\x07\x63hannel\x18\x07 \x01(\rB\x05\x92?\x02\x38\x08\x12\x10\n\x08via_mqtt\x18\x08 \x01(\x08\x12\x1d\n\thops_away\x18\t \x01(\rB\x05\x92?\x02\x38\x08H\x00\x88\x01\x01\x12\x13\n\x0bis_favorite\x18\n \x01(\x08\x12\x12\n\nis_ignored\x18\x0b \x01(\x08\x12\x17\n\x08next_hop\x18\x0c \x01(\rB\x05\x92?\x02\x38\x08\x12\x10\n\x08\x62itfield\x18\r \x01(\rB\x0c\n\n_hops_away\"\x92\x01\n\x13NodeDatabase_Legacy\x12\x0f\n\x07version\x18\x01 \x01(\r\x12j\n\x05nodes\x18\x02 \x03(\x0b\x32(.meshtastic.protobuf.NodeInfoLite_LegacyB1\x92?.\x92\x01+std::vector<meshtastic_NodeInfoLite_Legacy>Bt\n\x14org.meshtastic.protoB\x10\x44\x65viceOnlyLegacyZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x92?\x0b\xc2\x01\x08<vector>b\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.deviceonly_legacy_pb2', _globals)
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\020DeviceOnlyLegacyZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000\222?\013\302\001\010<vector>'
_NODEINFOLITE_LEGACY.fields_by_name['channel']._options = None
_NODEINFOLITE_LEGACY.fields_by_name['channel']._serialized_options = b'\222?\0028\010'
_NODEINFOLITE_LEGACY.fields_by_name['hops_away']._options = None
_NODEINFOLITE_LEGACY.fields_by_name['hops_away']._serialized_options = b'\222?\0028\010'
_NODEINFOLITE_LEGACY.fields_by_name['next_hop']._options = None
_NODEINFOLITE_LEGACY.fields_by_name['next_hop']._serialized_options = b'\222?\0028\010'
_NODEDATABASE_LEGACY.fields_by_name['nodes']._options = None
_NODEDATABASE_LEGACY.fields_by_name['nodes']._serialized_options = b'\222?.\222\001+std::vector<meshtastic_NodeInfoLite_Legacy>'
_globals['_NODEINFOLITE_LEGACY']._serialized_start=178
_globals['_NODEINFOLITE_LEGACY']._serialized_end=574
_globals['_NODEDATABASE_LEGACY']._serialized_start=577
_globals['_NODEDATABASE_LEGACY']._serialized_end=723
# @@protoc_insertion_point(module_scope)
+106
View File
@@ -0,0 +1,106 @@
"""
@generated by mypy-protobuf. Do not edit manually!
isort:skip_file
"""
import builtins
import collections.abc
import google.protobuf.descriptor
import google.protobuf.internal.containers
import google.protobuf.message
import meshtastic.protobuf.deviceonly_pb2
import meshtastic.protobuf.telemetry_pb2
import typing
DESCRIPTOR: google.protobuf.descriptor.FileDescriptor
@typing.final
class NodeInfoLite_Legacy(google.protobuf.message.Message):
"""
Legacy NodeInfoLite descriptor used only to decode pre-split
/prefs/nodes.proto saves during the v24 -> v25 migration boot.
This preserves the original NodeInfoLite-compatible field numbers needed
to parse old wire bytes cleanly, including user (2), position (3),
device_metrics (6), and the legacy-only compatibility fields via_mqtt (8),
is_favorite (10), and is_ignored (11). Steady-state code does not use
this struct; it is dropped after migration completes. This file should be
removed once DEVICESTATE_MIN_VER advances past 24.
"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
NUM_FIELD_NUMBER: builtins.int
USER_FIELD_NUMBER: builtins.int
POSITION_FIELD_NUMBER: builtins.int
SNR_FIELD_NUMBER: builtins.int
LAST_HEARD_FIELD_NUMBER: builtins.int
DEVICE_METRICS_FIELD_NUMBER: builtins.int
CHANNEL_FIELD_NUMBER: builtins.int
VIA_MQTT_FIELD_NUMBER: builtins.int
HOPS_AWAY_FIELD_NUMBER: builtins.int
IS_FAVORITE_FIELD_NUMBER: builtins.int
IS_IGNORED_FIELD_NUMBER: builtins.int
NEXT_HOP_FIELD_NUMBER: builtins.int
BITFIELD_FIELD_NUMBER: builtins.int
num: builtins.int
snr: builtins.float
last_heard: builtins.int
channel: builtins.int
via_mqtt: builtins.bool
hops_away: builtins.int
is_favorite: builtins.bool
is_ignored: builtins.bool
next_hop: builtins.int
bitfield: builtins.int
@property
def user(self) -> meshtastic.protobuf.deviceonly_pb2.UserLite: ...
@property
def position(self) -> meshtastic.protobuf.deviceonly_pb2.PositionLite: ...
@property
def device_metrics(self) -> meshtastic.protobuf.telemetry_pb2.DeviceMetrics: ...
def __init__(
self,
*,
num: builtins.int = ...,
user: meshtastic.protobuf.deviceonly_pb2.UserLite | None = ...,
position: meshtastic.protobuf.deviceonly_pb2.PositionLite | None = ...,
snr: builtins.float = ...,
last_heard: builtins.int = ...,
device_metrics: meshtastic.protobuf.telemetry_pb2.DeviceMetrics | None = ...,
channel: builtins.int = ...,
via_mqtt: builtins.bool = ...,
hops_away: builtins.int | None = ...,
is_favorite: builtins.bool = ...,
is_ignored: builtins.bool = ...,
next_hop: builtins.int = ...,
bitfield: builtins.int = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["_hops_away", b"_hops_away", "device_metrics", b"device_metrics", "hops_away", b"hops_away", "position", b"position", "user", b"user"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["_hops_away", b"_hops_away", "bitfield", b"bitfield", "channel", b"channel", "device_metrics", b"device_metrics", "hops_away", b"hops_away", "is_favorite", b"is_favorite", "is_ignored", b"is_ignored", "last_heard", b"last_heard", "next_hop", b"next_hop", "num", b"num", "position", b"position", "snr", b"snr", "user", b"user", "via_mqtt", b"via_mqtt"]) -> None: ...
def WhichOneof(self, oneof_group: typing.Literal["_hops_away", b"_hops_away"]) -> typing.Literal["hops_away"] | None: ...
global___NodeInfoLite_Legacy = NodeInfoLite_Legacy
@typing.final
class NodeDatabase_Legacy(google.protobuf.message.Message):
"""
Legacy NodeDatabase shape: one repeated array of fat NodeInfoLite_Legacy
with no satellite position/telemetry arrays.
"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
VERSION_FIELD_NUMBER: builtins.int
NODES_FIELD_NUMBER: builtins.int
version: builtins.int
@property
def nodes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NodeInfoLite_Legacy]: ...
def __init__(
self,
*,
version: builtins.int = ...,
nodes: collections.abc.Iterable[global___NodeInfoLite_Legacy] | None = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["nodes", b"nodes", "version", b"version"]) -> None: ...
global___NodeDatabase_Legacy = NodeDatabase_Legacy
+59 -15
View File
File diff suppressed because one or more lines are too long.
+159 -51
View File
@@ -30,6 +30,7 @@ class PositionLite(google.protobuf.message.Message):
ALTITUDE_FIELD_NUMBER: builtins.int
TIME_FIELD_NUMBER: builtins.int
LOCATION_SOURCE_FIELD_NUMBER: builtins.int
PRECISION_BITS_FIELD_NUMBER: builtins.int
latitude_i: builtins.int
"""
The new preferred location encoding, multiply by 1e-7 to get degrees
@@ -55,6 +56,10 @@ class PositionLite(google.protobuf.message.Message):
"""
TODO: REPLACE
"""
precision_bits: builtins.int
"""
Indicates the bits of precision set by the sending node
"""
def __init__(
self,
*,
@@ -63,8 +68,9 @@ class PositionLite(google.protobuf.message.Message):
altitude: builtins.int = ...,
time: builtins.int = ...,
location_source: meshtastic.protobuf.mesh_pb2.Position.LocSource.ValueType = ...,
precision_bits: builtins.int = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["altitude", b"altitude", "latitude_i", b"latitude_i", "location_source", b"location_source", "longitude_i", b"longitude_i", "time", b"time"]) -> None: ...
def ClearField(self, field_name: typing.Literal["altitude", b"altitude", "latitude_i", b"latitude_i", "location_source", b"location_source", "longitude_i", b"longitude_i", "precision_bits", b"precision_bits", "time", b"time"]) -> None: ...
global___PositionLite = PositionLite
@@ -142,26 +148,26 @@ class NodeInfoLite(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
NUM_FIELD_NUMBER: builtins.int
USER_FIELD_NUMBER: builtins.int
POSITION_FIELD_NUMBER: builtins.int
SNR_FIELD_NUMBER: builtins.int
LAST_HEARD_FIELD_NUMBER: builtins.int
DEVICE_METRICS_FIELD_NUMBER: builtins.int
CHANNEL_FIELD_NUMBER: builtins.int
VIA_MQTT_FIELD_NUMBER: builtins.int
HOPS_AWAY_FIELD_NUMBER: builtins.int
IS_FAVORITE_FIELD_NUMBER: builtins.int
IS_IGNORED_FIELD_NUMBER: builtins.int
NEXT_HOP_FIELD_NUMBER: builtins.int
BITFIELD_FIELD_NUMBER: builtins.int
LONG_NAME_FIELD_NUMBER: builtins.int
SHORT_NAME_FIELD_NUMBER: builtins.int
HW_MODEL_FIELD_NUMBER: builtins.int
ROLE_FIELD_NUMBER: builtins.int
PUBLIC_KEY_FIELD_NUMBER: builtins.int
SNR_Q4_FIELD_NUMBER: builtins.int
num: builtins.int
"""
The node number
"""
snr: builtins.float
"""
Returns the Signal-to-noise ratio (SNR) of the last received message,
as measured by the receiver. Return SNR of the last received message in dB
In-memory SNR of the last received message in dB. Not serialised directly:
always zeroed before encode; persisted as snr_q4 = 19 below.
"""
last_heard: builtins.int
"""
@@ -171,71 +177,69 @@ class NodeInfoLite(google.protobuf.message.Message):
"""
local channel index we heard that node on. Only populated if its not the default channel.
"""
via_mqtt: builtins.bool
"""
True if we witnessed the node over MQTT instead of LoRA transport
"""
hops_away: builtins.int
"""
Number of hops away from us this node is (0 if direct neighbor)
"""
is_favorite: builtins.bool
"""
True if node is in our favorites list
Persists between NodeDB internal clean ups
"""
is_ignored: builtins.bool
"""
True if node is in our ignored list
Persists between NodeDB internal clean ups
"""
next_hop: builtins.int
"""
Last byte of the node number of the node that should be used as the next hop to reach this node.
"""
bitfield: builtins.int
"""
Bitfield for storing booleans.
LSB 0 is_key_manually_verified
Bitfield for storing booleans. See NODEINFO_BITFIELD_* in src/mesh/NodeDB.h.
"""
@property
def user(self) -> global___UserLite:
"""
The user info for this node
"""
long_name: builtins.str
"""Flattened user fields (formerly UserLite). macaddr dropped (deprecated 1.2.11).
@property
def position(self) -> global___PositionLite:
"""
This position data. Note: before 1.2.14 we would also store the last time we've heard from this node in position.time, that is no longer true.
Position.time now indicates the last time we received a POSITION from that node.
"""
@property
def device_metrics(self) -> meshtastic.protobuf.telemetry_pb2.DeviceMetrics:
"""
The latest device metrics for the node.
"""
A full name for this user, i.e. "Kevin Hester".
"""
short_name: builtins.str
"""
A VERY short name, ideally two characters or an emoji.
Suitable for a tiny OLED screen.
"""
hw_model: meshtastic.protobuf.mesh_pb2.HardwareModel.ValueType
"""
Hardware model the user's device is running.
"""
role: meshtastic.protobuf.config_pb2.Config.DeviceConfig.Role.ValueType
"""
The user's role in the mesh.
"""
public_key: builtins.bytes
"""
The public key of the user's device, for PKI-based encrypted DMs.
"""
snr_q4: builtins.int
"""
Q4-encoded SNR: dB × 4, sint32 zigzag. Matches RouteDiscovery convention.
Encode: snr_q4 = (int32_t)lroundf(snr * 4.0f). Decode: snr = snr_q4 / 4.0f.
float snr is always zeroed on disk; this field carries all persisted SNR.
A stored 0 does not by itself mean "unknown" here - see NODEINFO_BITFIELD_HAS_SNR in
src/mesh/NodeDB.h for the presence bit that disambiguates a genuine 0 dB reading from
"never measured".
"""
def __init__(
self,
*,
num: builtins.int = ...,
user: global___UserLite | None = ...,
position: global___PositionLite | None = ...,
snr: builtins.float = ...,
last_heard: builtins.int = ...,
device_metrics: meshtastic.protobuf.telemetry_pb2.DeviceMetrics | None = ...,
channel: builtins.int = ...,
via_mqtt: builtins.bool = ...,
hops_away: builtins.int | None = ...,
is_favorite: builtins.bool = ...,
is_ignored: builtins.bool = ...,
next_hop: builtins.int = ...,
bitfield: builtins.int = ...,
long_name: builtins.str = ...,
short_name: builtins.str = ...,
hw_model: meshtastic.protobuf.mesh_pb2.HardwareModel.ValueType = ...,
role: meshtastic.protobuf.config_pb2.Config.DeviceConfig.Role.ValueType = ...,
public_key: builtins.bytes = ...,
snr_q4: builtins.int = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["_hops_away", b"_hops_away", "device_metrics", b"device_metrics", "hops_away", b"hops_away", "position", b"position", "user", b"user"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["_hops_away", b"_hops_away", "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 HasField(self, field_name: typing.Literal["_hops_away", b"_hops_away", "hops_away", b"hops_away"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["_hops_away", b"_hops_away", "bitfield", b"bitfield", "channel", b"channel", "hops_away", b"hops_away", "hw_model", b"hw_model", "last_heard", b"last_heard", "long_name", b"long_name", "next_hop", b"next_hop", "num", b"num", "public_key", b"public_key", "role", b"role", "short_name", b"short_name", "snr", b"snr", "snr_q4", b"snr_q4"]) -> None: ...
def WhichOneof(self, oneof_group: typing.Literal["_hops_away", b"_hops_away"]) -> typing.Literal["hops_away"] | None: ...
global___NodeInfoLite = NodeInfoLite
@@ -336,12 +340,100 @@ class DeviceState(google.protobuf.message.Message):
global___DeviceState = DeviceState
@typing.final
class NodePositionEntry(google.protobuf.message.Message):
"""Satellite per-node entries; stored alongside the slim NodeInfoLite so nodes
that never report don't pay the embedded cost.
"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
NUM_FIELD_NUMBER: builtins.int
POSITION_FIELD_NUMBER: builtins.int
num: builtins.int
@property
def position(self) -> global___PositionLite: ...
def __init__(
self,
*,
num: builtins.int = ...,
position: global___PositionLite | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["position", b"position"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["num", b"num", "position", b"position"]) -> None: ...
global___NodePositionEntry = NodePositionEntry
@typing.final
class NodeTelemetryEntry(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
NUM_FIELD_NUMBER: builtins.int
DEVICE_METRICS_FIELD_NUMBER: builtins.int
num: builtins.int
@property
def device_metrics(self) -> meshtastic.protobuf.telemetry_pb2.DeviceMetrics: ...
def __init__(
self,
*,
num: builtins.int = ...,
device_metrics: meshtastic.protobuf.telemetry_pb2.DeviceMetrics | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["device_metrics", b"device_metrics"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["device_metrics", b"device_metrics", "num", b"num"]) -> None: ...
global___NodeTelemetryEntry = NodeTelemetryEntry
@typing.final
class NodeEnvironmentEntry(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
NUM_FIELD_NUMBER: builtins.int
ENVIRONMENT_METRICS_FIELD_NUMBER: builtins.int
num: builtins.int
@property
def environment_metrics(self) -> meshtastic.protobuf.telemetry_pb2.EnvironmentMetrics: ...
def __init__(
self,
*,
num: builtins.int = ...,
environment_metrics: meshtastic.protobuf.telemetry_pb2.EnvironmentMetrics | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["environment_metrics", b"environment_metrics"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["environment_metrics", b"environment_metrics", "num", b"num"]) -> None: ...
global___NodeEnvironmentEntry = NodeEnvironmentEntry
@typing.final
class NodeStatusEntry(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
NUM_FIELD_NUMBER: builtins.int
STATUS_FIELD_NUMBER: builtins.int
num: builtins.int
@property
def status(self) -> meshtastic.protobuf.mesh_pb2.StatusMessage: ...
def __init__(
self,
*,
num: builtins.int = ...,
status: meshtastic.protobuf.mesh_pb2.StatusMessage | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["status", b"status"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["num", b"num", "status", b"status"]) -> None: ...
global___NodeStatusEntry = NodeStatusEntry
@typing.final
class NodeDatabase(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
VERSION_FIELD_NUMBER: builtins.int
NODES_FIELD_NUMBER: builtins.int
POSITIONS_FIELD_NUMBER: builtins.int
TELEMETRY_FIELD_NUMBER: builtins.int
STATUS_FIELD_NUMBER: builtins.int
ENVIRONMENT_FIELD_NUMBER: builtins.int
version: builtins.int
"""
A version integer used to invalidate old save files when we make
@@ -354,13 +446,29 @@ class NodeDatabase(google.protobuf.message.Message):
New lite version of NodeDB to decrease memory footprint
"""
@property
def positions(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NodePositionEntry]:
"""Per-NodeNum satellite arrays. Constrained platforms (e.g. STM32WL) omit
these via MESHTASTIC_EXCLUDE_*DB build flags.
"""
@property
def telemetry(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NodeTelemetryEntry]: ...
@property
def status(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NodeStatusEntry]: ...
@property
def environment(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NodeEnvironmentEntry]: ...
def __init__(
self,
*,
version: builtins.int = ...,
nodes: collections.abc.Iterable[global___NodeInfoLite] | None = ...,
positions: collections.abc.Iterable[global___NodePositionEntry] | None = ...,
telemetry: collections.abc.Iterable[global___NodeTelemetryEntry] | None = ...,
status: collections.abc.Iterable[global___NodeStatusEntry] | None = ...,
environment: collections.abc.Iterable[global___NodeEnvironmentEntry] | None = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["nodes", b"nodes", "version", b"version"]) -> None: ...
def ClearField(self, field_name: typing.Literal["environment", b"environment", "nodes", b"nodes", "positions", b"positions", "status", b"status", "telemetry", b"telemetry", "version", b"version"]) -> None: ...
global___NodeDatabase = NodeDatabase
+50 -7
View File
@@ -11,9 +11,10 @@ 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/interdevice.proto\x12\x13meshtastic.protobuf\"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\"_\n\x12InterdeviceMessage\x12\x0e\n\x04nmea\x18\x01 \x01(\tH\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')
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n%meshtastic/protobuf/interdevice.proto\x12\x13meshtastic.protobuf\x1a meshtastic/protobuf/nanopb.proto\"\xf6\x01\n\x0c\x46ileTransfer\x12\x35\n\toperation\x18\x01 \x01(\x0e\x32\".meshtastic.protobuf.FileOperation\x12\x18\n\x08\x66ilepath\x18\x02 \x01(\tB\x06\x92?\x03\x08\x80\x02\x12\x18\n\x08\x66iledata\x18\x03 \x01(\x0c\x42\x06\x92?\x03\x08\x80 \x12/\n\x06status\x18\x04 \x01(\x0e\x32\x1f.meshtastic.protobuf.FileStatus\x12\x17\n\x07message\x18\x05 \x01(\tB\x06\x92?\x03\x08\xff\x01\x12\x0e\n\x06offset\x18\x06 \x01(\x04\x12\x0e\n\x06length\x18\x07 \x01(\r\x12\x11\n\tfile_size\x18\x08 \x01(\x04\"\xb9\x01\n\x10\x44irectoryListing\x12\x19\n\tdirectory\x18\x01 \x01(\tB\x06\x92?\x03\x08\x80\x02\x12\x1b\n\tfilenames\x18\x02 \x03(\tB\x08\x92?\x05\x10\x10p\xff\x01\x12/\n\x06status\x18\x03 \x01(\x0e\x32\x1f.meshtastic.protobuf.FileStatus\x12\x17\n\x07message\x18\x04 \x01(\tB\x06\x92?\x03\x08\xff\x01\x12\x0e\n\x06offset\x18\x05 \x01(\r\x12\x13\n\x0btotal_count\x18\x06 \x01(\r\"O\n\x0eI2CTransaction\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\r\x12\x1a\n\nwrite_data\x18\x02 \x01(\x0c\x42\x06\x92?\x03\x08\x80\x02\x12\x10\n\x08read_len\x18\x03 \x01(\r\"\x92\x03\n\nSdCardInfo\x12\x0f\n\x07present\x18\x01 \x01(\x08\x12;\n\tcard_type\x18\x02 \x01(\x0e\x32(.meshtastic.protobuf.SdCardInfo.CardType\x12\x39\n\x08\x66\x61t_type\x18\x03 \x01(\x0e\x32\'.meshtastic.protobuf.SdCardInfo.FatType\x12\x11\n\tcard_size\x18\x04 \x01(\x04\x12\x12\n\nused_bytes\x18\x05 \x01(\x04\x12\x12\n\nfree_bytes\x18\x06 \x01(\x04\x12\x13\n\x0bstats_valid\x18\x07 \x01(\x08\x12\x0c\n\x04\x62usy\x18\x08 \x01(\x08\x12\x13\n\x0bunformatted\x18\t \x01(\x08\"K\n\x08\x43\x61rdType\x12\x08\n\x04NONE\x10\x00\x12\x07\n\x03MMC\x10\x01\x12\x06\n\x02SD\x10\x02\x12\x08\n\x04SDHC\x10\x03\x12\x08\n\x04SDXC\x10\x04\x12\x10\n\x0cUNKNOWN_CARD\x10\x05\";\n\x07\x46\x61tType\x12\x0f\n\x0bUNKNOWN_FAT\x10\x00\x12\t\n\x05\x46\x41T16\x10\x01\x12\t\n\x05\x46\x41T32\x10\x02\x12\t\n\x05\x45XFAT\x10\x03\"\xac\x01\n\tI2CResult\x12\x35\n\x06status\x18\x01 \x01(\x0e\x32%.meshtastic.protobuf.I2CResult.Status\x12\x19\n\tread_data\x18\x02 \x01(\x0c\x42\x06\x92?\x03\x08\x80\x02\"M\n\x06Status\x12\x0f\n\x0bUNSPECIFIED\x10\x00\x12\x06\n\x02OK\x10\x01\x12\x10\n\x0cNACK_ADDRESS\x10\x02\x12\r\n\tNACK_DATA\x10\x03\x12\t\n\x05\x45RROR\x10\x04\"\x87\x05\n\x12InterdeviceMessage\x12\n\n\x02id\x18\x0f \x01(\r\x12\x16\n\x04nmea\x18\x01 \x01(\tB\x06\x92?\x03\x08\x80\x08H\x00\x12\x15\n\x04\x62\x65\x65p\x18\x02 \x01(\rB\x05\x92?\x02\x38\x10H\x00\x12>\n\x0fi2c_transaction\x18\x03 \x01(\x0b\x32#.meshtastic.protobuf.I2CTransactionH\x00\x12\x34\n\ni2c_result\x18\x04 \x01(\x0b\x32\x1e.meshtastic.protobuf.I2CResultH\x00\x12\x12\n\x08i2c_scan\x18\x05 \x01(\x08H\x00\x12!\n\x0fi2c_scan_result\x18\x06 \x01(\x0c\x42\x06\x92?\x03\x08\x80\x01H\x00\x12:\n\rfile_transfer\x18\x07 \x01(\x0b\x32!.meshtastic.protobuf.FileTransferH\x00\x12\x42\n\x11\x64irectory_listing\x18\x08 \x01(\x0b\x32%.meshtastic.protobuf.DirectoryListingH\x00\x12\x15\n\x0bget_sd_info\x18\t \x01(\x08H\x00\x12\x32\n\x07sd_info\x18\n \x01(\x0b\x32\x1f.meshtastic.protobuf.SdCardInfoH\x00\x12\x37\n\x04ping\x18\x0b \x01(\x0e\x32\'.meshtastic.protobuf.InterdeviceVersionH\x00\x12\x37\n\x04pong\x18\x0c \x01(\x0e\x32\'.meshtastic.protobuf.InterdeviceVersionH\x00\x12\x0e\n\x04nack\x18\r \x01(\x08H\x00\x12\x34\n\nsd_command\x18\x0e \x01(\x0e\x32\x1e.meshtastic.protobuf.SdCommandH\x00\x42\x06\n\x04\x64\x61ta*Z\n\x12InterdeviceVersion\x12#\n\x1fINTERDEVICE_VERSION_UNSPECIFIED\x10\x00\x12\x1f\n\x1bINTERDEVICE_VERSION_CURRENT\x10\x02*7\n\rFileOperation\x12\x07\n\x03GET\x10\x00\x12\x08\n\x04POST\x10\x01\x12\x07\n\x03PUT\x10\x02\x12\n\n\x06\x44\x45LETE\x10\x03*\xa6\x01\n\nFileStatus\x12\x14\n\x10\x46ILE_UNSPECIFIED\x10\x00\x12\x0b\n\x07\x46ILE_OK\x10\x01\x12\r\n\tFILE_BUSY\x10\x02\x12\x10\n\x0c\x46ILE_NO_CARD\x10\x03\x12\x12\n\x0e\x46ILE_NOT_FOUND\x10\x04\x12\x18\n\x14\x46ILE_OFFSET_CONFLICT\x10\x05\x12\x11\n\rFILE_IO_ERROR\x10\x06\x12\x13\n\x0f\x46ILE_NOT_A_FILE\x10\x07*R\n\tSdCommand\x12\x1a\n\x16SD_COMMAND_UNSPECIFIED\x10\x00\x12\x0c\n\x08SD_MOUNT\x10\x01\x12\x0c\n\x08SD_EJECT\x10\x02\x12\r\n\tSD_FORMAT\x10\x03\x42g\n\x14org.meshtastic.protoB\x11InterdeviceProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
@@ -21,10 +22,52 @@ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.interde
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'
_globals['_MESSAGETYPE']._serialized_start=277
_globals['_MESSAGETYPE']._serialized_end=490
_globals['_SENSORDATA']._serialized_start=62
_globals['_SENSORDATA']._serialized_end=177
_globals['_INTERDEVICEMESSAGE']._serialized_start=179
_globals['_INTERDEVICEMESSAGE']._serialized_end=274
_FILETRANSFER.fields_by_name['filepath']._options = None
_FILETRANSFER.fields_by_name['filepath']._serialized_options = b'\222?\003\010\200\002'
_FILETRANSFER.fields_by_name['filedata']._options = None
_FILETRANSFER.fields_by_name['filedata']._serialized_options = b'\222?\003\010\200 '
_FILETRANSFER.fields_by_name['message']._options = None
_FILETRANSFER.fields_by_name['message']._serialized_options = b'\222?\003\010\377\001'
_DIRECTORYLISTING.fields_by_name['directory']._options = None
_DIRECTORYLISTING.fields_by_name['directory']._serialized_options = b'\222?\003\010\200\002'
_DIRECTORYLISTING.fields_by_name['filenames']._options = None
_DIRECTORYLISTING.fields_by_name['filenames']._serialized_options = b'\222?\005\020\020p\377\001'
_DIRECTORYLISTING.fields_by_name['message']._options = None
_DIRECTORYLISTING.fields_by_name['message']._serialized_options = b'\222?\003\010\377\001'
_I2CTRANSACTION.fields_by_name['write_data']._options = None
_I2CTRANSACTION.fields_by_name['write_data']._serialized_options = b'\222?\003\010\200\002'
_I2CRESULT.fields_by_name['read_data']._options = None
_I2CRESULT.fields_by_name['read_data']._serialized_options = b'\222?\003\010\200\002'
_INTERDEVICEMESSAGE.fields_by_name['nmea']._options = None
_INTERDEVICEMESSAGE.fields_by_name['nmea']._serialized_options = b'\222?\003\010\200\010'
_INTERDEVICEMESSAGE.fields_by_name['beep']._options = None
_INTERDEVICEMESSAGE.fields_by_name['beep']._serialized_options = b'\222?\0028\020'
_INTERDEVICEMESSAGE.fields_by_name['i2c_scan_result']._options = None
_INTERDEVICEMESSAGE.fields_by_name['i2c_scan_result']._serialized_options = b'\222?\003\010\200\001'
_globals['_INTERDEVICEVERSION']._serialized_start=1844
_globals['_INTERDEVICEVERSION']._serialized_end=1934
_globals['_FILEOPERATION']._serialized_start=1936
_globals['_FILEOPERATION']._serialized_end=1991
_globals['_FILESTATUS']._serialized_start=1994
_globals['_FILESTATUS']._serialized_end=2160
_globals['_SDCOMMAND']._serialized_start=2162
_globals['_SDCOMMAND']._serialized_end=2244
_globals['_FILETRANSFER']._serialized_start=97
_globals['_FILETRANSFER']._serialized_end=343
_globals['_DIRECTORYLISTING']._serialized_start=346
_globals['_DIRECTORYLISTING']._serialized_end=531
_globals['_I2CTRANSACTION']._serialized_start=533
_globals['_I2CTRANSACTION']._serialized_end=612
_globals['_SDCARDINFO']._serialized_start=615
_globals['_SDCARDINFO']._serialized_end=1017
_globals['_SDCARDINFO_CARDTYPE']._serialized_start=881
_globals['_SDCARDINFO_CARDTYPE']._serialized_end=956
_globals['_SDCARDINFO_FATTYPE']._serialized_start=958
_globals['_SDCARDINFO_FATTYPE']._serialized_end=1017
_globals['_I2CRESULT']._serialized_start=1020
_globals['_I2CRESULT']._serialized_end=1192
_globals['_I2CRESULT_STATUS']._serialized_start=1115
_globals['_I2CRESULT_STATUS']._serialized_end=1192
_globals['_INTERDEVICEMESSAGE']._serialized_start=1195
_globals['_INTERDEVICEMESSAGE']._serialized_end=1842
# @@protoc_insertion_point(module_scope)
+445 -56
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
@@ -17,89 +19,476 @@ else:
DESCRIPTOR: google.protobuf.descriptor.FileDescriptor
class _MessageType:
class _InterdeviceVersion:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
class _MessageTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_MessageType.ValueType], builtins.type):
class _InterdeviceVersionEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_InterdeviceVersion.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
INTERDEVICE_VERSION_UNSPECIFIED: _InterdeviceVersion.ValueType # 0
INTERDEVICE_VERSION_CURRENT: _InterdeviceVersion.ValueType # 2
"""Never use 1: ping/pong were bools before the handshake existed, and a
bool true is the same varint on the wire as the number 1, so firmware
predating the handshake would pass it.
"""
class MessageType(_MessageType, metaclass=_MessageTypeEnumTypeWrapper):
"""encapsulate up to 1k of NMEA string data"""
class InterdeviceVersion(_InterdeviceVersion, metaclass=_InterdeviceVersionEnumTypeWrapper):
"""Version of the interdevice protocol spoken on the link. Both sides send
theirs in the ping/pong handshake; a peer reporting a different one runs
firmware that does not match and is not talked to.
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
On a change that breaks the other side (renumbered fields, changed
semantics, removed messages), raise the value of CURRENT. Do not add
another entry: this enum carries a single constant, not a history.
"""
INTERDEVICE_VERSION_UNSPECIFIED: InterdeviceVersion.ValueType # 0
INTERDEVICE_VERSION_CURRENT: InterdeviceVersion.ValueType # 2
"""Never use 1: ping/pong were bools before the handshake existed, and a
bool true is the same varint on the wire as the number 1, so firmware
predating the handshake would pass it.
"""
global___InterdeviceVersion = InterdeviceVersion
class _FileOperation:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
class _FileOperationEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_FileOperation.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
GET: _FileOperation.ValueType # 0
POST: _FileOperation.ValueType # 1
PUT: _FileOperation.ValueType # 2
DELETE: _FileOperation.ValueType # 3
class FileOperation(_FileOperation, metaclass=_FileOperationEnumTypeWrapper):
"""Defines the supported file operations"""
GET: FileOperation.ValueType # 0
POST: FileOperation.ValueType # 1
PUT: FileOperation.ValueType # 2
DELETE: FileOperation.ValueType # 3
global___FileOperation = FileOperation
class _FileStatus:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
class _FileStatusEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_FileStatus.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
FILE_UNSPECIFIED: _FileStatus.ValueType # 0
FILE_OK: _FileStatus.ValueType # 1
FILE_BUSY: _FileStatus.ValueType # 2
"""Retry later: the co-processor is doing card maintenance (mount,
free space scan) and cannot serve the request right now
"""
FILE_NO_CARD: _FileStatus.ValueType # 3
FILE_NOT_FOUND: _FileStatus.ValueType # 4
FILE_OFFSET_CONFLICT: _FileStatus.ValueType # 5
"""PUT only: offset did not match the current end of the file. file_size
carries the size the file actually has, so the writer can resync (or
recognize its own chunk as already written after a lost response).
"""
FILE_IO_ERROR: _FileStatus.ValueType # 6
FILE_NOT_A_FILE: _FileStatus.ValueType # 7
"""path is a directory (GET) or not one (listing)"""
class FileStatus(_FileStatus, metaclass=_FileStatusEnumTypeWrapper):
"""Outcome of a file or directory operation. The requester must be able to
tell a transient condition from a definitive one: BUSY is worth another
try, NOT_FOUND is not.
"""
FILE_UNSPECIFIED: FileStatus.ValueType # 0
FILE_OK: FileStatus.ValueType # 1
FILE_BUSY: FileStatus.ValueType # 2
"""Retry later: the co-processor is doing card maintenance (mount,
free space scan) and cannot serve the request right now
"""
FILE_NO_CARD: FileStatus.ValueType # 3
FILE_NOT_FOUND: FileStatus.ValueType # 4
FILE_OFFSET_CONFLICT: FileStatus.ValueType # 5
"""PUT only: offset did not match the current end of the file. file_size
carries the size the file actually has, so the writer can resync (or
recognize its own chunk as already written after a lost response).
"""
FILE_IO_ERROR: FileStatus.ValueType # 6
FILE_NOT_A_FILE: FileStatus.ValueType # 7
"""path is a directory (GET) or not one (listing)"""
global___FileStatus = FileStatus
class _SdCommand:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
class _SdCommandEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_SdCommand.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
SD_COMMAND_UNSPECIFIED: _SdCommand.ValueType # 0
SD_MOUNT: _SdCommand.ValueType # 1
"""mount a card that is in the slot, also after an eject"""
SD_EJECT: _SdCommand.ValueType # 2
"""flush and release the card so it can be pulled safely"""
SD_FORMAT: _SdCommand.ValueType # 3
"""wipe the card and put a fresh FAT on it, then mount it"""
class SdCommand(_SdCommand, metaclass=_SdCommandEnumTypeWrapper):
"""What to do with the SD card of the co-processor"""
SD_COMMAND_UNSPECIFIED: SdCommand.ValueType # 0
SD_MOUNT: SdCommand.ValueType # 1
"""mount a card that is in the slot, also after an eject"""
SD_EJECT: SdCommand.ValueType # 2
"""flush and release the card so it can be pulled safely"""
SD_FORMAT: SdCommand.ValueType # 3
"""wipe the card and put a fresh FAT on it, then mount it"""
global___SdCommand = SdCommand
@typing.final
class SensorData(google.protobuf.message.Message):
class FileTransfer(google.protobuf.message.Message):
"""Message for file operations"""
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
OPERATION_FIELD_NUMBER: builtins.int
FILEPATH_FIELD_NUMBER: builtins.int
FILEDATA_FIELD_NUMBER: builtins.int
STATUS_FIELD_NUMBER: builtins.int
MESSAGE_FIELD_NUMBER: builtins.int
OFFSET_FIELD_NUMBER: builtins.int
LENGTH_FIELD_NUMBER: builtins.int
FILE_SIZE_FIELD_NUMBER: builtins.int
operation: global___FileOperation.ValueType
"""File operation (GET, POST, PUT, DELETE)"""
filepath: builtins.str
filedata: builtins.bytes
status: global___FileStatus.ValueType
"""Response: outcome of the operation"""
message: builtins.str
offset: builtins.int
"""Byte offset of this chunk within the file (ranged GET/PUT)"""
length: builtins.int
"""GET request: number of bytes to read, 0 = max chunk size. A response
carries at most the filedata max_size (see interdevice.options) per
chunk; larger requests are truncated, visible in the filedata length.
"""
file_size: builtins.int
"""GET response: total size of the file"""
def __init__(
self,
*,
type: global___MessageType.ValueType = ...,
float_value: builtins.float = ...,
uint32_value: builtins.int = ...,
operation: global___FileOperation.ValueType = ...,
filepath: builtins.str = ...,
filedata: builtins.bytes = ...,
status: global___FileStatus.ValueType = ...,
message: builtins.str = ...,
offset: builtins.int = ...,
length: builtins.int = ...,
file_size: builtins.int = ...,
) -> None: ...
def 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: ...
def ClearField(self, field_name: typing.Literal["file_size", b"file_size", "filedata", b"filedata", "filepath", b"filepath", "length", b"length", "message", b"message", "offset", b"offset", "operation", b"operation", "status", b"status"]) -> None: ...
global___SensorData = SensorData
global___FileTransfer = FileTransfer
@typing.final
class DirectoryListing(google.protobuf.message.Message):
"""Message for structured directory listing"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
DIRECTORY_FIELD_NUMBER: builtins.int
FILENAMES_FIELD_NUMBER: builtins.int
STATUS_FIELD_NUMBER: builtins.int
MESSAGE_FIELD_NUMBER: builtins.int
OFFSET_FIELD_NUMBER: builtins.int
TOTAL_COUNT_FIELD_NUMBER: builtins.int
directory: builtins.str
status: global___FileStatus.ValueType
"""Response: outcome of the operation"""
message: builtins.str
offset: builtins.int
"""Request: skip this many entries (paging)"""
total_count: builtins.int
"""Response: total number of entries in the directory"""
@property
def filenames(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]:
"""One page of entry names, full FAT LFN length. Subdirectories carry a
trailing slash. Note that a name whose directory prefix pushes the
combined path past the FileTransfer.filepath limit cannot round-trip.
Page size is the max_count in interdevice.options; page through with
offset and total_count.
"""
def __init__(
self,
*,
directory: builtins.str = ...,
filenames: collections.abc.Iterable[builtins.str] | None = ...,
status: global___FileStatus.ValueType = ...,
message: builtins.str = ...,
offset: builtins.int = ...,
total_count: builtins.int = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["directory", b"directory", "filenames", b"filenames", "message", b"message", "offset", b"offset", "status", b"status", "total_count", b"total_count"]) -> None: ...
global___DirectoryListing = DirectoryListing
@typing.final
class I2CTransaction(google.protobuf.message.Message):
"""A single I2C transaction: an optional write followed by an optional
read with repeated start, matching the TwoWire usage of sensor drivers
(beginTransmission/write.../endTransmission(false)/requestFrom)
"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
ADDRESS_FIELD_NUMBER: builtins.int
WRITE_DATA_FIELD_NUMBER: builtins.int
READ_LEN_FIELD_NUMBER: builtins.int
address: builtins.int
"""7-bit device address"""
write_data: builtins.bytes
read_len: builtins.int
"""Number of bytes to read after the write, 0 = write-only. Bounded by
the read_data max_size of I2CResult (see interdevice.options); larger
requests are truncated, visible in the returned byte count.
"""
def __init__(
self,
*,
address: builtins.int = ...,
write_data: builtins.bytes = ...,
read_len: builtins.int = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["address", b"address", "read_len", b"read_len", "write_data", b"write_data"]) -> None: ...
global___I2CTransaction = I2CTransaction
@typing.final
class SdCardInfo(google.protobuf.message.Message):
"""SD card statistics"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
class _CardType:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
class _CardTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[SdCardInfo._CardType.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
NONE: SdCardInfo._CardType.ValueType # 0
MMC: SdCardInfo._CardType.ValueType # 1
SD: SdCardInfo._CardType.ValueType # 2
SDHC: SdCardInfo._CardType.ValueType # 3
SDXC: SdCardInfo._CardType.ValueType # 4
UNKNOWN_CARD: SdCardInfo._CardType.ValueType # 5
class CardType(_CardType, metaclass=_CardTypeEnumTypeWrapper): ...
NONE: SdCardInfo.CardType.ValueType # 0
MMC: SdCardInfo.CardType.ValueType # 1
SD: SdCardInfo.CardType.ValueType # 2
SDHC: SdCardInfo.CardType.ValueType # 3
SDXC: SdCardInfo.CardType.ValueType # 4
UNKNOWN_CARD: SdCardInfo.CardType.ValueType # 5
class _FatType:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
class _FatTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[SdCardInfo._FatType.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
UNKNOWN_FAT: SdCardInfo._FatType.ValueType # 0
FAT16: SdCardInfo._FatType.ValueType # 1
FAT32: SdCardInfo._FatType.ValueType # 2
EXFAT: SdCardInfo._FatType.ValueType # 3
class FatType(_FatType, metaclass=_FatTypeEnumTypeWrapper): ...
UNKNOWN_FAT: SdCardInfo.FatType.ValueType # 0
FAT16: SdCardInfo.FatType.ValueType # 1
FAT32: SdCardInfo.FatType.ValueType # 2
EXFAT: SdCardInfo.FatType.ValueType # 3
PRESENT_FIELD_NUMBER: builtins.int
CARD_TYPE_FIELD_NUMBER: builtins.int
FAT_TYPE_FIELD_NUMBER: builtins.int
CARD_SIZE_FIELD_NUMBER: builtins.int
USED_BYTES_FIELD_NUMBER: builtins.int
FREE_BYTES_FIELD_NUMBER: builtins.int
STATS_VALID_FIELD_NUMBER: builtins.int
BUSY_FIELD_NUMBER: builtins.int
UNFORMATTED_FIELD_NUMBER: builtins.int
present: builtins.bool
"""Card initialized and usable. False while `busy` is set does not mean
there is no card: the co-processor does not know yet.
"""
card_type: global___SdCardInfo.CardType.ValueType
fat_type: global___SdCardInfo.FatType.ValueType
card_size: builtins.int
"""Filesystem size in bytes"""
used_bytes: builtins.int
"""Used bytes (may be expensive to compute on FAT32)"""
free_bytes: builtins.int
"""Free bytes"""
stats_valid: builtins.bool
"""used_bytes/free_bytes are only meaningful when true: the scan behind
them runs in the background after mount and can take a while, and a
full card is otherwise indistinguishable from a scan in progress
"""
busy: builtins.bool
"""The co-processor is mounting a card right now, so whether one is
present is not decided yet. Ask again rather than concluding the slot
is empty.
"""
unformatted: builtins.bool
"""A card answers in the slot but carries no filesystem that could be
mounted (present is false then). Formatting it makes it usable.
"""
def __init__(
self,
*,
present: builtins.bool = ...,
card_type: global___SdCardInfo.CardType.ValueType = ...,
fat_type: global___SdCardInfo.FatType.ValueType = ...,
card_size: builtins.int = ...,
used_bytes: builtins.int = ...,
free_bytes: builtins.int = ...,
stats_valid: builtins.bool = ...,
busy: builtins.bool = ...,
unformatted: builtins.bool = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["busy", b"busy", "card_size", b"card_size", "card_type", b"card_type", "fat_type", b"fat_type", "free_bytes", b"free_bytes", "present", b"present", "stats_valid", b"stats_valid", "unformatted", b"unformatted", "used_bytes", b"used_bytes"]) -> None: ...
global___SdCardInfo = SdCardInfo
@typing.final
class I2CResult(google.protobuf.message.Message):
"""Result of an I2CTransaction"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
class _Status:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
class _StatusEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[I2CResult._Status.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
UNSPECIFIED: I2CResult._Status.ValueType # 0
"""Never sent: an all-defaults (e.g. accidentally empty) message must
not decode as a successful transaction
"""
OK: I2CResult._Status.ValueType # 1
NACK_ADDRESS: I2CResult._Status.ValueType # 2
NACK_DATA: I2CResult._Status.ValueType # 3
ERROR: I2CResult._Status.ValueType # 4
class Status(_Status, metaclass=_StatusEnumTypeWrapper): ...
UNSPECIFIED: I2CResult.Status.ValueType # 0
"""Never sent: an all-defaults (e.g. accidentally empty) message must
not decode as a successful transaction
"""
OK: I2CResult.Status.ValueType # 1
NACK_ADDRESS: I2CResult.Status.ValueType # 2
NACK_DATA: I2CResult.Status.ValueType # 3
ERROR: I2CResult.Status.ValueType # 4
STATUS_FIELD_NUMBER: builtins.int
READ_DATA_FIELD_NUMBER: builtins.int
status: global___I2CResult.Status.ValueType
read_data: builtins.bytes
def __init__(
self,
*,
status: global___I2CResult.Status.ValueType = ...,
read_data: builtins.bytes = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["read_data", b"read_data", "status", b"status"]) -> None: ...
global___I2CResult = I2CResult
@typing.final
class InterdeviceMessage(google.protobuf.message.Message):
"""Main message for interdevice communication"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
ID_FIELD_NUMBER: builtins.int
NMEA_FIELD_NUMBER: builtins.int
SENSOR_FIELD_NUMBER: builtins.int
BEEP_FIELD_NUMBER: builtins.int
I2C_TRANSACTION_FIELD_NUMBER: builtins.int
I2C_RESULT_FIELD_NUMBER: builtins.int
I2C_SCAN_FIELD_NUMBER: builtins.int
I2C_SCAN_RESULT_FIELD_NUMBER: builtins.int
FILE_TRANSFER_FIELD_NUMBER: builtins.int
DIRECTORY_LISTING_FIELD_NUMBER: builtins.int
GET_SD_INFO_FIELD_NUMBER: builtins.int
SD_INFO_FIELD_NUMBER: builtins.int
PING_FIELD_NUMBER: builtins.int
PONG_FIELD_NUMBER: builtins.int
NACK_FIELD_NUMBER: builtins.int
SD_COMMAND_FIELD_NUMBER: builtins.int
id: builtins.int
"""Correlates a response with its request: responses echo the id of the
request they answer. 0 for unsolicited messages (e.g. the nmea stream).
"""
nmea: builtins.str
beep: builtins.int
i2c_scan: builtins.bool
"""Request: scan the secondary I2C bus"""
i2c_scan_result: builtins.bytes
get_sd_info: builtins.bool
"""Request: SD card statistics"""
ping: global___InterdeviceVersion.ValueType
"""Link liveness probe and version handshake. The receiver answers ping
with pong, echoing the id. Touches no peripherals, so it works with
nothing attached. Both carry the version the sender speaks; a peer
that answers with a different one speaks another protocol and must
not be used.
"""
pong: global___InterdeviceVersion.ValueType
nack: builtins.bool
"""Response: the request could not be decoded or is of an unhandled
type, so the requester fails fast instead of burning its timeout.
Echoes the id when known, 0 when the frame was undecodable. Never
sent in reaction to a nack.
"""
sd_command: global___SdCommand.ValueType
"""Request: mount the card, or release it so it can be pulled safely. The
co-processor answers with sd_info. Without an eject the card is mounted
on its own and kept mounted; after one it stays released until a mount
is asked for.
"""
@property
def sensor(self) -> global___SensorData: ...
def i2c_transaction(self) -> global___I2CTransaction: ...
@property
def i2c_result(self) -> global___I2CResult: ...
@property
def file_transfer(self) -> global___FileTransfer: ...
@property
def directory_listing(self) -> global___DirectoryListing: ...
@property
def sd_info(self) -> global___SdCardInfo:
"""Response"""
def __init__(
self,
*,
id: builtins.int = ...,
nmea: builtins.str = ...,
sensor: global___SensorData | None = ...,
beep: builtins.int = ...,
i2c_transaction: global___I2CTransaction | None = ...,
i2c_result: global___I2CResult | None = ...,
i2c_scan: builtins.bool = ...,
i2c_scan_result: builtins.bytes = ...,
file_transfer: global___FileTransfer | None = ...,
directory_listing: global___DirectoryListing | None = ...,
get_sd_info: builtins.bool = ...,
sd_info: global___SdCardInfo | None = ...,
ping: global___InterdeviceVersion.ValueType = ...,
pong: global___InterdeviceVersion.ValueType = ...,
nack: builtins.bool = ...,
sd_command: global___SdCommand.ValueType = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["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: ...
def HasField(self, field_name: typing.Literal["beep", b"beep", "data", b"data", "directory_listing", b"directory_listing", "file_transfer", b"file_transfer", "get_sd_info", b"get_sd_info", "i2c_result", b"i2c_result", "i2c_scan", b"i2c_scan", "i2c_scan_result", b"i2c_scan_result", "i2c_transaction", b"i2c_transaction", "nack", b"nack", "nmea", b"nmea", "ping", b"ping", "pong", b"pong", "sd_command", b"sd_command", "sd_info", b"sd_info"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["beep", b"beep", "data", b"data", "directory_listing", b"directory_listing", "file_transfer", b"file_transfer", "get_sd_info", b"get_sd_info", "i2c_result", b"i2c_result", "i2c_scan", b"i2c_scan", "i2c_scan_result", b"i2c_scan_result", "i2c_transaction", b"i2c_transaction", "id", b"id", "nack", b"nack", "nmea", b"nmea", "ping", b"ping", "pong", b"pong", "sd_command", b"sd_command", "sd_info", b"sd_info"]) -> None: ...
def WhichOneof(self, oneof_group: typing.Literal["data", b"data"]) -> typing.Literal["nmea", "beep", "i2c_transaction", "i2c_result", "i2c_scan", "i2c_scan_result", "file_transfer", "directory_listing", "get_sd_info", "sd_info", "ping", "pong", "nack", "sd_command"] | None: ...
global___InterdeviceMessage = InterdeviceMessage
+2 -2
View File
@@ -15,7 +15,7 @@ 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(\rBe\n\x14org.meshtastic.protoB\x0fLocalOnlyProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n#meshtastic/protobuf/localonly.proto\x12\x13meshtastic.protobuf\x1a meshtastic/protobuf/config.proto\x1a\'meshtastic/protobuf/module_config.proto\"\xfa\x03\n\x0bLocalConfig\x12\x38\n\x06\x64\x65vice\x18\x01 \x01(\x0b\x32(.meshtastic.protobuf.Config.DeviceConfig\x12<\n\x08position\x18\x02 \x01(\x0b\x32*.meshtastic.protobuf.Config.PositionConfig\x12\x36\n\x05power\x18\x03 \x01(\x0b\x32\'.meshtastic.protobuf.Config.PowerConfig\x12:\n\x07network\x18\x04 \x01(\x0b\x32).meshtastic.protobuf.Config.NetworkConfig\x12:\n\x07\x64isplay\x18\x05 \x01(\x0b\x32).meshtastic.protobuf.Config.DisplayConfig\x12\x34\n\x04lora\x18\x06 \x01(\x0b\x32&.meshtastic.protobuf.Config.LoRaConfig\x12>\n\tbluetooth\x18\x07 \x01(\x0b\x32+.meshtastic.protobuf.Config.BluetoothConfig\x12\x0f\n\x07version\x18\x08 \x01(\r\x12<\n\x08security\x18\t \x01(\x0b\x32*.meshtastic.protobuf.Config.SecurityConfig\"\x98\n\n\x11LocalModuleConfig\x12:\n\x04mqtt\x18\x01 \x01(\x0b\x32,.meshtastic.protobuf.ModuleConfig.MQTTConfig\x12>\n\x06serial\x18\x02 \x01(\x0b\x32..meshtastic.protobuf.ModuleConfig.SerialConfig\x12[\n\x15\x65xternal_notification\x18\x03 \x01(\x0b\x32<.meshtastic.protobuf.ModuleConfig.ExternalNotificationConfig\x12K\n\rstore_forward\x18\x04 \x01(\x0b\x32\x34.meshtastic.protobuf.ModuleConfig.StoreForwardConfig\x12\x45\n\nrange_test\x18\x05 \x01(\x0b\x32\x31.meshtastic.protobuf.ModuleConfig.RangeTestConfig\x12\x44\n\ttelemetry\x18\x06 \x01(\x0b\x32\x31.meshtastic.protobuf.ModuleConfig.TelemetryConfig\x12M\n\x0e\x63\x61nned_message\x18\x07 \x01(\x0b\x32\x35.meshtastic.protobuf.ModuleConfig.CannedMessageConfig\x12<\n\x05\x61udio\x18\t \x01(\x0b\x32-.meshtastic.protobuf.ModuleConfig.AudioConfig\x12O\n\x0fremote_hardware\x18\n \x01(\x0b\x32\x36.meshtastic.protobuf.ModuleConfig.RemoteHardwareConfig\x12K\n\rneighbor_info\x18\x0b \x01(\x0b\x32\x34.meshtastic.protobuf.ModuleConfig.NeighborInfoConfig\x12Q\n\x10\x61mbient_lighting\x18\x0c \x01(\x0b\x32\x37.meshtastic.protobuf.ModuleConfig.AmbientLightingConfig\x12Q\n\x10\x64\x65tection_sensor\x18\r \x01(\x0b\x32\x37.meshtastic.protobuf.ModuleConfig.DetectionSensorConfig\x12\x46\n\npaxcounter\x18\x0e \x01(\x0b\x32\x32.meshtastic.protobuf.ModuleConfig.PaxcounterConfig\x12L\n\rstatusmessage\x18\x0f \x01(\x0b\x32\x35.meshtastic.protobuf.ModuleConfig.StatusMessageConfig\x12U\n\x12traffic_management\x18\x10 \x01(\x0b\x32\x39.meshtastic.protobuf.ModuleConfig.TrafficManagementConfig\x12\x38\n\x03tak\x18\x11 \x01(\x0b\x32+.meshtastic.protobuf.ModuleConfig.TAKConfig\x12G\n\x0bmesh_beacon\x18\x12 \x01(\x0b\x32\x32.meshtastic.protobuf.ModuleConfig.MeshBeaconConfig\x12\x0f\n\x07version\x18\x08 \x01(\rBe\n\x14org.meshtastic.protoB\x0fLocalOnlyProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
@@ -26,5 +26,5 @@ if _descriptor._USE_C_DESCRIPTORS == False:
_globals['_LOCALCONFIG']._serialized_start=136
_globals['_LOCALCONFIG']._serialized_end=642
_globals['_LOCALMODULECONFIG']._serialized_start=645
_globals['_LOCALMODULECONFIG']._serialized_end=1653
_globals['_LOCALMODULECONFIG']._serialized_end=1949
# @@protoc_insertion_point(module_scope)
+34 -2
View File
@@ -119,6 +119,10 @@ class LocalModuleConfig(google.protobuf.message.Message):
AMBIENT_LIGHTING_FIELD_NUMBER: builtins.int
DETECTION_SENSOR_FIELD_NUMBER: builtins.int
PAXCOUNTER_FIELD_NUMBER: builtins.int
STATUSMESSAGE_FIELD_NUMBER: builtins.int
TRAFFIC_MANAGEMENT_FIELD_NUMBER: builtins.int
TAK_FIELD_NUMBER: builtins.int
MESH_BEACON_FIELD_NUMBER: builtins.int
VERSION_FIELD_NUMBER: builtins.int
version: builtins.int
"""
@@ -204,6 +208,30 @@ class LocalModuleConfig(google.protobuf.message.Message):
Paxcounter Config
"""
@property
def statusmessage(self) -> meshtastic.protobuf.module_config_pb2.ModuleConfig.StatusMessageConfig:
"""
StatusMessage Config
"""
@property
def traffic_management(self) -> meshtastic.protobuf.module_config_pb2.ModuleConfig.TrafficManagementConfig:
"""
The part of the config that is specific to the Traffic Management module
"""
@property
def tak(self) -> meshtastic.protobuf.module_config_pb2.ModuleConfig.TAKConfig:
"""
TAK Config
"""
@property
def mesh_beacon(self) -> meshtastic.protobuf.module_config_pb2.ModuleConfig.MeshBeaconConfig:
"""
MeshBeacon Config
"""
def __init__(
self,
*,
@@ -220,9 +248,13 @@ class LocalModuleConfig(google.protobuf.message.Message):
ambient_lighting: meshtastic.protobuf.module_config_pb2.ModuleConfig.AmbientLightingConfig | None = ...,
detection_sensor: meshtastic.protobuf.module_config_pb2.ModuleConfig.DetectionSensorConfig | None = ...,
paxcounter: meshtastic.protobuf.module_config_pb2.ModuleConfig.PaxcounterConfig | None = ...,
statusmessage: meshtastic.protobuf.module_config_pb2.ModuleConfig.StatusMessageConfig | None = ...,
traffic_management: meshtastic.protobuf.module_config_pb2.ModuleConfig.TrafficManagementConfig | None = ...,
tak: meshtastic.protobuf.module_config_pb2.ModuleConfig.TAKConfig | None = ...,
mesh_beacon: meshtastic.protobuf.module_config_pb2.ModuleConfig.MeshBeaconConfig | None = ...,
version: builtins.int = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["ambient_lighting", b"ambient_lighting", "audio", b"audio", "canned_message", b"canned_message", "detection_sensor", b"detection_sensor", "external_notification", b"external_notification", "mqtt", b"mqtt", "neighbor_info", b"neighbor_info", "paxcounter", b"paxcounter", "range_test", b"range_test", "remote_hardware", b"remote_hardware", "serial", b"serial", "store_forward", b"store_forward", "telemetry", b"telemetry"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["ambient_lighting", b"ambient_lighting", "audio", b"audio", "canned_message", b"canned_message", "detection_sensor", b"detection_sensor", "external_notification", b"external_notification", "mqtt", b"mqtt", "neighbor_info", b"neighbor_info", "paxcounter", b"paxcounter", "range_test", b"range_test", "remote_hardware", b"remote_hardware", "serial", b"serial", "store_forward", b"store_forward", "telemetry", b"telemetry", "version", b"version"]) -> None: ...
def HasField(self, field_name: typing.Literal["ambient_lighting", b"ambient_lighting", "audio", b"audio", "canned_message", b"canned_message", "detection_sensor", b"detection_sensor", "external_notification", b"external_notification", "mesh_beacon", b"mesh_beacon", "mqtt", b"mqtt", "neighbor_info", b"neighbor_info", "paxcounter", b"paxcounter", "range_test", b"range_test", "remote_hardware", b"remote_hardware", "serial", b"serial", "statusmessage", b"statusmessage", "store_forward", b"store_forward", "tak", b"tak", "telemetry", b"telemetry", "traffic_management", b"traffic_management"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["ambient_lighting", b"ambient_lighting", "audio", b"audio", "canned_message", b"canned_message", "detection_sensor", b"detection_sensor", "external_notification", b"external_notification", "mesh_beacon", b"mesh_beacon", "mqtt", b"mqtt", "neighbor_info", b"neighbor_info", "paxcounter", b"paxcounter", "range_test", b"range_test", "remote_hardware", b"remote_hardware", "serial", b"serial", "statusmessage", b"statusmessage", "store_forward", b"store_forward", "tak", b"tak", "telemetry", b"telemetry", "traffic_management", b"traffic_management", "version", b"version"]) -> None: ...
global___LocalModuleConfig = LocalModuleConfig
+31
View File
@@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: meshtastic/protobuf/mesh_beacon.proto
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
from meshtastic.protobuf import channel_pb2 as meshtastic_dot_protobuf_dot_channel__pb2
from meshtastic.protobuf import config_pb2 as meshtastic_dot_protobuf_dot_config__pb2
from meshtastic.protobuf import nanopb_pb2 as meshtastic_dot_protobuf_dot_nanopb__pb2
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n%meshtastic/protobuf/mesh_beacon.proto\x12\x13meshtastic.protobuf\x1a!meshtastic/protobuf/channel.proto\x1a meshtastic/protobuf/config.proto\x1a meshtastic/protobuf/nanopb.proto\"\x8a\x02\n\nMeshBeacon\x12\x16\n\x07message\x18\x01 \x01(\tB\x05\x92?\x02\x08\x65\x12;\n\roffer_channel\x18\x02 \x01(\x0b\x32$.meshtastic.protobuf.ChannelSettings\x12G\n\x0coffer_region\x18\x03 \x01(\x0e\x32\x31.meshtastic.protobuf.Config.LoRaConfig.RegionCode\x12M\n\x0coffer_preset\x18\x04 \x01(\x0e\x32\x32.meshtastic.protobuf.Config.LoRaConfig.ModemPresetH\x00\x88\x01\x01\x42\x0f\n\r_offer_presetBf\n\x14org.meshtastic.protoB\x10MeshBeaconProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.mesh_beacon_pb2', _globals)
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\020MeshBeaconProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
_MESHBEACON.fields_by_name['message']._options = None
_MESHBEACON.fields_by_name['message']._serialized_options = b'\222?\002\010e'
_globals['_MESHBEACON']._serialized_start=166
_globals['_MESHBEACON']._serialized_end=432
# @@protoc_insertion_point(module_scope)
+62
View File
@@ -0,0 +1,62 @@
"""
@generated by mypy-protobuf. Do not edit manually!
isort:skip_file
"""
import builtins
import google.protobuf.descriptor
import google.protobuf.message
import meshtastic.protobuf.channel_pb2
import meshtastic.protobuf.config_pb2
import typing
DESCRIPTOR: google.protobuf.descriptor.FileDescriptor
@typing.final
class MeshBeacon(google.protobuf.message.Message):
"""
Payload for MESH_BEACON_APP packets.
Periodically broadcast by nodes in beacon mode.
Listeners deliver the text message to the local inbox and cache any offered
channel/preset for the client app to act on the firmware never auto-applies them.
"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
MESSAGE_FIELD_NUMBER: builtins.int
OFFER_CHANNEL_FIELD_NUMBER: builtins.int
OFFER_REGION_FIELD_NUMBER: builtins.int
OFFER_PRESET_FIELD_NUMBER: builtins.int
message: builtins.str
"""
Human-readable beacon message. Max 100 bytes enforced by firmware on send.
"""
offer_region: meshtastic.protobuf.config_pb2.Config.LoRaConfig.RegionCode.ValueType
"""
Optional region being advertised alongside offer_preset.
"""
offer_preset: meshtastic.protobuf.config_pb2.Config.LoRaConfig.ModemPreset.ValueType
"""
Optional modem preset being advertised.
Combined with offer_region, tells a client "there is a mesh on this preset/region".
"""
@property
def offer_channel(self) -> meshtastic.protobuf.channel_pb2.ChannelSettings:
"""
Optional channel (name + PSK) being advertised to listening clients.
A client app may offer to switch the user to this channel; firmware never applies it automatically.
"""
def __init__(
self,
*,
message: builtins.str = ...,
offer_channel: meshtastic.protobuf.channel_pb2.ChannelSettings | None = ...,
offer_region: meshtastic.protobuf.config_pb2.Config.LoRaConfig.RegionCode.ValueType = ...,
offer_preset: meshtastic.protobuf.config_pb2.Config.LoRaConfig.ModemPreset.ValueType | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["_offer_preset", b"_offer_preset", "offer_channel", b"offer_channel", "offer_preset", b"offer_preset"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["_offer_preset", b"_offer_preset", "message", b"message", "offer_channel", b"offer_channel", "offer_preset", b"offer_preset", "offer_region", b"offer_region"]) -> None: ...
def WhichOneof(self, oneof_group: typing.Literal["_offer_preset", b"_offer_preset"]) -> typing.Literal["offer_preset"] | None: ...
global___MeshBeacon = MeshBeacon
+235 -88
View File
File diff suppressed because one or more lines are too long.
+990 -29
View File
File diff suppressed because it is too large. Load diff
+114 -46
View File
File diff suppressed because one or more lines are too long.
+325 -5
View File
@@ -9,6 +9,9 @@ import google.protobuf.descriptor
import google.protobuf.internal.containers
import google.protobuf.internal.enum_type_wrapper
import google.protobuf.message
import meshtastic.protobuf.atak_pb2
import meshtastic.protobuf.channel_pb2
import meshtastic.protobuf.config_pb2
import sys
import typing
@@ -112,7 +115,7 @@ class ModuleConfig(google.protobuf.message.Message):
"""
json_enabled: builtins.bool
"""
Whether to send / consume json packets on MQTT
Deprecated: JSON packet support on MQTT was removed, and this field is ignored.
"""
tls_enabled: builtins.bool
"""
@@ -492,6 +495,58 @@ class ModuleConfig(google.protobuf.message.Message):
) -> None: ...
def ClearField(self, field_name: typing.Literal["ble_threshold", b"ble_threshold", "enabled", b"enabled", "paxcounter_update_interval", b"paxcounter_update_interval", "wifi_threshold", b"wifi_threshold"]) -> None: ...
@typing.final
class TrafficManagementConfig(google.protobuf.message.Message):
"""
Config for the Traffic Management module.
Provides packet inspection and traffic shaping to help reduce channel utilization
"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
POSITION_MIN_INTERVAL_SECS_FIELD_NUMBER: builtins.int
NODEINFO_DIRECT_RESPONSE_MAX_HOPS_FIELD_NUMBER: builtins.int
RATE_LIMIT_WINDOW_SECS_FIELD_NUMBER: builtins.int
RATE_LIMIT_MAX_PACKETS_FIELD_NUMBER: builtins.int
UNKNOWN_PACKET_THRESHOLD_FIELD_NUMBER: builtins.int
position_min_interval_secs: builtins.int
"""
Minimum interval in seconds between position updates from the same node.
A non-zero value implicitly enables the suppression window; 0 disables it.
"""
nodeinfo_direct_response_max_hops: builtins.int
"""
Maximum hop distance from the requestor at which direct NodeInfo responses
are served from the local cache. A non-zero value implicitly enables direct
response; 0 disables it.
"""
rate_limit_window_secs: builtins.int
"""
Time window in seconds for per-node rate limiting.
A non-zero value implicitly enables rate limiting; 0 disables it.
"""
rate_limit_max_packets: builtins.int
"""
Maximum packets allowed per node within the rate limit window.
A non-zero value implicitly enables rate limiting; 0 disables it.
"""
unknown_packet_threshold: builtins.int
"""
Maximum unknown/undecryptable packets per rate window before the source
is dropped. A non-zero value implicitly enables unknown-packet filtering;
0 disables it.
"""
def __init__(
self,
*,
position_min_interval_secs: builtins.int = ...,
nodeinfo_direct_response_max_hops: builtins.int = ...,
rate_limit_window_secs: builtins.int = ...,
rate_limit_max_packets: builtins.int = ...,
unknown_packet_threshold: builtins.int = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["nodeinfo_direct_response_max_hops", b"nodeinfo_direct_response_max_hops", "position_min_interval_secs", b"position_min_interval_secs", "rate_limit_max_packets", b"rate_limit_max_packets", "rate_limit_window_secs", b"rate_limit_window_secs", "unknown_packet_threshold", b"unknown_packet_threshold"]) -> None: ...
@typing.final
class SerialConfig(google.protobuf.message.Message):
"""
@@ -568,6 +623,12 @@ class ModuleConfig(google.protobuf.message.Message):
"""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):
"""
@@ -591,6 +652,12 @@ class ModuleConfig(google.protobuf.message.Message):
"""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
@@ -875,6 +942,7 @@ class ModuleConfig(google.protobuf.message.Message):
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
@@ -940,6 +1008,10 @@ class ModuleConfig(google.protobuf.message.Message):
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,
*,
@@ -957,8 +1029,9 @@ class ModuleConfig(google.protobuf.message.Message):
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_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: ...
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):
@@ -1164,6 +1237,221 @@ class ModuleConfig(google.protobuf.message.Message):
) -> None: ...
def ClearField(self, field_name: typing.Literal["blue", b"blue", "current", b"current", "green", b"green", "led_state", b"led_state", "red", b"red"]) -> None: ...
@typing.final
class StatusMessageConfig(google.protobuf.message.Message):
"""
StatusMessage config - Allows setting a status message for a node to periodically rebroadcast
"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
NODE_STATUS_FIELD_NUMBER: builtins.int
node_status: builtins.str
"""
The actual status string
"""
def __init__(
self,
*,
node_status: builtins.str = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["node_status", b"node_status"]) -> None: ...
@typing.final
class MeshBeaconConfig(google.protobuf.message.Message):
"""
MeshBeacon module config
"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
class _Flags:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
class _FlagsEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[ModuleConfig.MeshBeaconConfig._Flags.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
FLAG_NONE: ModuleConfig.MeshBeaconConfig._Flags.ValueType # 0
"""
No options enabled.
"""
FLAG_LISTEN_ENABLED: ModuleConfig.MeshBeaconConfig._Flags.ValueType # 1
"""
Enable receiving MESH_BEACON_APP packets from other nodes.
The text portion is delivered to the local message inbox.
Offered channel/preset are stored for the client app to act on.
"""
FLAG_BROADCAST_ENABLED: ModuleConfig.MeshBeaconConfig._Flags.ValueType # 2
"""
Enable periodically broadcasting MESH_BEACON_APP packets from this node.
"""
FLAG_LEGACY_SPLIT: ModuleConfig.MeshBeaconConfig._Flags.ValueType # 4
"""
When both text and offer content are present, split the beacon into a separate
MESH_BEACON_APP (offer only) and TEXT_MESSAGE_APP (text only) packet, so firmware
that only decodes TEXT_MESSAGE_APP still receives the human-readable text.
"""
class Flags(_Flags, metaclass=_FlagsEnumTypeWrapper):
"""
Boolean options for the beacon module, packed into the `flags` bitfield below.
OR the FLAG_* values together; a flag is on when its bit is set.
"""
FLAG_NONE: ModuleConfig.MeshBeaconConfig.Flags.ValueType # 0
"""
No options enabled.
"""
FLAG_LISTEN_ENABLED: ModuleConfig.MeshBeaconConfig.Flags.ValueType # 1
"""
Enable receiving MESH_BEACON_APP packets from other nodes.
The text portion is delivered to the local message inbox.
Offered channel/preset are stored for the client app to act on.
"""
FLAG_BROADCAST_ENABLED: ModuleConfig.MeshBeaconConfig.Flags.ValueType # 2
"""
Enable periodically broadcasting MESH_BEACON_APP packets from this node.
"""
FLAG_LEGACY_SPLIT: ModuleConfig.MeshBeaconConfig.Flags.ValueType # 4
"""
When both text and offer content are present, split the beacon into a separate
MESH_BEACON_APP (offer only) and TEXT_MESSAGE_APP (text only) packet, so firmware
that only decodes TEXT_MESSAGE_APP still receives the human-readable text.
"""
@typing.final
class BroadcastTarget(google.protobuf.message.Message):
"""
One entry in the broadcast destination list.
Each entry names one set of radio settings to send a beacon copy on.
"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
PRESET_FIELD_NUMBER: builtins.int
REGION_FIELD_NUMBER: builtins.int
CHANNEL_INDEX_FIELD_NUMBER: builtins.int
preset: meshtastic.protobuf.config_pb2.Config.LoRaConfig.ModemPreset.ValueType
"""
Modem preset to use for this target.
Falls back to the running config preset if unset.
"""
region: meshtastic.protobuf.config_pb2.Config.LoRaConfig.RegionCode.ValueType
"""
Region to use for this target. UNSET means use the running config region.
"""
channel_index: builtins.int
"""Tag 3 was an embedded ChannelSettings; replaced by channel_index (tag 4) to keep
ModuleConfig within the BLE FromRadio size budget. Branch unreleased, so tag 3 is a gap.
Index into the device's channel table (0..MAX_NUM_CHANNELS-1) of the channel to
transmit this target's beacon on. The referenced channel must already be configured
on the node (its key is needed to encrypt). If unset, the default channel for the
preset is used.
"""
def __init__(
self,
*,
preset: meshtastic.protobuf.config_pb2.Config.LoRaConfig.ModemPreset.ValueType | None = ...,
region: meshtastic.protobuf.config_pb2.Config.LoRaConfig.RegionCode.ValueType = ...,
channel_index: builtins.int | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["_channel_index", b"_channel_index", "_preset", b"_preset", "channel_index", b"channel_index", "preset", b"preset"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["_channel_index", b"_channel_index", "_preset", b"_preset", "channel_index", b"channel_index", "preset", b"preset", "region", b"region"]) -> None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_channel_index", b"_channel_index"]) -> typing.Literal["channel_index"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_preset", b"_preset"]) -> typing.Literal["preset"] | None: ...
FLAGS_FIELD_NUMBER: builtins.int
BROADCAST_MESSAGE_FIELD_NUMBER: builtins.int
BROADCAST_OFFER_CHANNEL_FIELD_NUMBER: builtins.int
BROADCAST_OFFER_REGION_FIELD_NUMBER: builtins.int
BROADCAST_OFFER_PRESET_FIELD_NUMBER: builtins.int
BROADCAST_INTERVAL_SECS_FIELD_NUMBER: builtins.int
BROADCAST_TARGETS_FIELD_NUMBER: builtins.int
flags: builtins.int
"""
Bitwise-OR of Flags values (listen / broadcast / legacy-split toggles).
"""
broadcast_message: builtins.str
"""
Message to include in each beacon broadcast. Max 100 bytes enforced by firmware.
"""
broadcast_offer_region: meshtastic.protobuf.config_pb2.Config.LoRaConfig.RegionCode.ValueType
"""
Optional region to advertise in the MeshBeacon offer_region field.
"""
broadcast_offer_preset: meshtastic.protobuf.config_pb2.Config.LoRaConfig.ModemPreset.ValueType
"""
Optional modem preset to advertise in the MeshBeacon offer_preset field.
"""
broadcast_interval_secs: builtins.int
"""
How often to broadcast, in seconds. Min 3600 (1 h), default 3600.
"""
@property
def broadcast_offer_channel(self) -> meshtastic.protobuf.channel_pb2.ChannelSettings:
"""
Optional channel (name + PSK) to advertise in the MeshBeacon offer_channel field.
"""
@property
def broadcast_targets(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ModuleConfig.MeshBeaconConfig.BroadcastTarget]:
"""
Broadcast destination list.
The broadcaster sends one beacon copy per distinct destination, in sequence, temporarily
switching the radio to that entry's preset/region/channel for each.
When empty, a single beacon is sent on the node's running preset and region over the
primary channel.
Entries that resolve to the same effective preset, region and channel are deduplicated, so
a duplicate entry does not produce a second transmission.
"""
def __init__(
self,
*,
flags: builtins.int = ...,
broadcast_message: builtins.str = ...,
broadcast_offer_channel: meshtastic.protobuf.channel_pb2.ChannelSettings | None = ...,
broadcast_offer_region: meshtastic.protobuf.config_pb2.Config.LoRaConfig.RegionCode.ValueType = ...,
broadcast_offer_preset: meshtastic.protobuf.config_pb2.Config.LoRaConfig.ModemPreset.ValueType | None = ...,
broadcast_interval_secs: builtins.int = ...,
broadcast_targets: collections.abc.Iterable[global___ModuleConfig.MeshBeaconConfig.BroadcastTarget] | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["_broadcast_offer_preset", b"_broadcast_offer_preset", "broadcast_offer_channel", b"broadcast_offer_channel", "broadcast_offer_preset", b"broadcast_offer_preset"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["_broadcast_offer_preset", b"_broadcast_offer_preset", "broadcast_interval_secs", b"broadcast_interval_secs", "broadcast_message", b"broadcast_message", "broadcast_offer_channel", b"broadcast_offer_channel", "broadcast_offer_preset", b"broadcast_offer_preset", "broadcast_offer_region", b"broadcast_offer_region", "broadcast_targets", b"broadcast_targets", "flags", b"flags"]) -> None: ...
def WhichOneof(self, oneof_group: typing.Literal["_broadcast_offer_preset", b"_broadcast_offer_preset"]) -> typing.Literal["broadcast_offer_preset"] | None: ...
@typing.final
class TAKConfig(google.protobuf.message.Message):
"""
TAK team/role configuration
"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
TEAM_FIELD_NUMBER: builtins.int
ROLE_FIELD_NUMBER: builtins.int
team: meshtastic.protobuf.atak_pb2.Team.ValueType
"""
Team color.
Default Unspecifed_Color -> firmware uses Cyan
"""
role: meshtastic.protobuf.atak_pb2.MemberRole.ValueType
"""
Member role.
Default Unspecifed -> firmware uses TeamMember
"""
def __init__(
self,
*,
team: meshtastic.protobuf.atak_pb2.Team.ValueType = ...,
role: meshtastic.protobuf.atak_pb2.MemberRole.ValueType = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["role", b"role", "team", b"team"]) -> None: ...
MQTT_FIELD_NUMBER: builtins.int
SERIAL_FIELD_NUMBER: builtins.int
EXTERNAL_NOTIFICATION_FIELD_NUMBER: builtins.int
@@ -1177,6 +1465,10 @@ class ModuleConfig(google.protobuf.message.Message):
AMBIENT_LIGHTING_FIELD_NUMBER: builtins.int
DETECTION_SENSOR_FIELD_NUMBER: builtins.int
PAXCOUNTER_FIELD_NUMBER: builtins.int
STATUSMESSAGE_FIELD_NUMBER: builtins.int
TRAFFIC_MANAGEMENT_FIELD_NUMBER: builtins.int
TAK_FIELD_NUMBER: builtins.int
MESH_BEACON_FIELD_NUMBER: builtins.int
@property
def mqtt(self) -> global___ModuleConfig.MQTTConfig:
"""
@@ -1255,6 +1547,30 @@ class ModuleConfig(google.protobuf.message.Message):
TODO: REPLACE
"""
@property
def statusmessage(self) -> global___ModuleConfig.StatusMessageConfig:
"""
TODO: REPLACE
"""
@property
def traffic_management(self) -> global___ModuleConfig.TrafficManagementConfig:
"""
Traffic management module config for mesh network optimization
"""
@property
def tak(self) -> global___ModuleConfig.TAKConfig:
"""
TAK team/role configuration for TAK_TRACKER
"""
@property
def mesh_beacon(self) -> global___ModuleConfig.MeshBeaconConfig:
"""
MeshBeacon module config
"""
def __init__(
self,
*,
@@ -1271,10 +1587,14 @@ class ModuleConfig(google.protobuf.message.Message):
ambient_lighting: global___ModuleConfig.AmbientLightingConfig | None = ...,
detection_sensor: global___ModuleConfig.DetectionSensorConfig | None = ...,
paxcounter: global___ModuleConfig.PaxcounterConfig | None = ...,
statusmessage: global___ModuleConfig.StatusMessageConfig | None = ...,
traffic_management: global___ModuleConfig.TrafficManagementConfig | None = ...,
tak: global___ModuleConfig.TAKConfig | None = ...,
mesh_beacon: global___ModuleConfig.MeshBeaconConfig | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["ambient_lighting", b"ambient_lighting", "audio", b"audio", "canned_message", b"canned_message", "detection_sensor", b"detection_sensor", "external_notification", b"external_notification", "mqtt", b"mqtt", "neighbor_info", b"neighbor_info", "paxcounter", b"paxcounter", "payload_variant", b"payload_variant", "range_test", b"range_test", "remote_hardware", b"remote_hardware", "serial", b"serial", "store_forward", b"store_forward", "telemetry", b"telemetry"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["ambient_lighting", b"ambient_lighting", "audio", b"audio", "canned_message", b"canned_message", "detection_sensor", b"detection_sensor", "external_notification", b"external_notification", "mqtt", b"mqtt", "neighbor_info", b"neighbor_info", "paxcounter", b"paxcounter", "payload_variant", b"payload_variant", "range_test", b"range_test", "remote_hardware", b"remote_hardware", "serial", b"serial", "store_forward", b"store_forward", "telemetry", b"telemetry"]) -> None: ...
def WhichOneof(self, oneof_group: typing.Literal["payload_variant", b"payload_variant"]) -> typing.Literal["mqtt", "serial", "external_notification", "store_forward", "range_test", "telemetry", "canned_message", "audio", "remote_hardware", "neighbor_info", "ambient_lighting", "detection_sensor", "paxcounter"] | None: ...
def HasField(self, field_name: typing.Literal["ambient_lighting", b"ambient_lighting", "audio", b"audio", "canned_message", b"canned_message", "detection_sensor", b"detection_sensor", "external_notification", b"external_notification", "mesh_beacon", b"mesh_beacon", "mqtt", b"mqtt", "neighbor_info", b"neighbor_info", "paxcounter", b"paxcounter", "payload_variant", b"payload_variant", "range_test", b"range_test", "remote_hardware", b"remote_hardware", "serial", b"serial", "statusmessage", b"statusmessage", "store_forward", b"store_forward", "tak", b"tak", "telemetry", b"telemetry", "traffic_management", b"traffic_management"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["ambient_lighting", b"ambient_lighting", "audio", b"audio", "canned_message", b"canned_message", "detection_sensor", b"detection_sensor", "external_notification", b"external_notification", "mesh_beacon", b"mesh_beacon", "mqtt", b"mqtt", "neighbor_info", b"neighbor_info", "paxcounter", b"paxcounter", "payload_variant", b"payload_variant", "range_test", b"range_test", "remote_hardware", b"remote_hardware", "serial", b"serial", "statusmessage", b"statusmessage", "store_forward", b"store_forward", "tak", b"tak", "telemetry", b"telemetry", "traffic_management", b"traffic_management"]) -> None: ...
def WhichOneof(self, oneof_group: typing.Literal["payload_variant", b"payload_variant"]) -> typing.Literal["mqtt", "serial", "external_notification", "store_forward", "range_test", "telemetry", "canned_message", "audio", "remote_hardware", "neighbor_info", "ambient_lighting", "detection_sensor", "paxcounter", "statusmessage", "traffic_management", "tak", "mesh_beacon"] | None: ...
global___ModuleConfig = ModuleConfig
+20 -5
View File
@@ -13,9 +13,10 @@ _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\"\x83\x04\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(\r\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')
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1emeshtastic/protobuf/mqtt.proto\x12\x13meshtastic.protobuf\x1a meshtastic/protobuf/config.proto\x1a\x1emeshtastic/protobuf/mesh.proto\x1a meshtastic/protobuf/nanopb.proto\"\x7f\n\x0fServiceEnvelope\x12\x36\n\x06packet\x18\x01 \x01(\x0b\x32\x1f.meshtastic.protobuf.MeshPacketB\x05\x92?\x02\x18\x04\x12\x19\n\nchannel_id\x18\x02 \x01(\tB\x05\x92?\x02\x18\x04\x12\x19\n\ngateway_id\x18\x03 \x01(\tB\x05\x92?\x02\x18\x04\"\x9f\x04\n\tMapReport\x12\x18\n\tlong_name\x18\x01 \x01(\tB\x05\x92?\x02\x08\x19\x12\x19\n\nshort_name\x18\x02 \x01(\tB\x05\x92?\x02\x08\x05\x12;\n\x04role\x18\x03 \x01(\x0e\x32-.meshtastic.protobuf.Config.DeviceConfig.Role\x12\x34\n\x08hw_model\x18\x04 \x01(\x0e\x32\".meshtastic.protobuf.HardwareModel\x12\x1f\n\x10\x66irmware_version\x18\x05 \x01(\tB\x05\x92?\x02\x08\x12\x12\x41\n\x06region\x18\x06 \x01(\x0e\x32\x31.meshtastic.protobuf.Config.LoRaConfig.RegionCode\x12H\n\x0cmodem_preset\x18\x07 \x01(\x0e\x32\x32.meshtastic.protobuf.Config.LoRaConfig.ModemPreset\x12\x1b\n\x13has_default_channel\x18\x08 \x01(\x08\x12\x12\n\nlatitude_i\x18\t \x01(\x0f\x12\x13\n\x0blongitude_i\x18\n \x01(\x0f\x12\x10\n\x08\x61ltitude\x18\x0b \x01(\x05\x12\x1a\n\x12position_precision\x18\x0c \x01(\r\x12%\n\x16num_online_local_nodes\x18\r \x01(\rB\x05\x92?\x02\x38\x10\x12!\n\x19has_opted_report_location\x18\x0e \x01(\x08\x42`\n\x14org.meshtastic.protoB\nMQTTProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
@@ -23,8 +24,22 @@ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.mqtt_pb
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\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=745
_SERVICEENVELOPE.fields_by_name['packet']._options = None
_SERVICEENVELOPE.fields_by_name['packet']._serialized_options = b'\222?\002\030\004'
_SERVICEENVELOPE.fields_by_name['channel_id']._options = None
_SERVICEENVELOPE.fields_by_name['channel_id']._serialized_options = b'\222?\002\030\004'
_SERVICEENVELOPE.fields_by_name['gateway_id']._options = None
_SERVICEENVELOPE.fields_by_name['gateway_id']._serialized_options = b'\222?\002\030\004'
_MAPREPORT.fields_by_name['long_name']._options = None
_MAPREPORT.fields_by_name['long_name']._serialized_options = b'\222?\002\010\031'
_MAPREPORT.fields_by_name['short_name']._options = None
_MAPREPORT.fields_by_name['short_name']._serialized_options = b'\222?\002\010\005'
_MAPREPORT.fields_by_name['firmware_version']._options = None
_MAPREPORT.fields_by_name['firmware_version']._serialized_options = b'\222?\002\010\022'
_MAPREPORT.fields_by_name['num_online_local_nodes']._options = None
_MAPREPORT.fields_by_name['num_online_local_nodes']._serialized_options = b'\222?\0028\020'
_globals['_SERVICEENVELOPE']._serialized_start=155
_globals['_SERVICEENVELOPE']._serialized_end=282
_globals['_MAPREPORT']._serialized_start=285
_globals['_MAPREPORT']._serialized_end=828
# @@protoc_insertion_point(module_scope)
+2 -2
View File
@@ -13,7 +13,7 @@ _sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"meshtastic/protobuf/portnums.proto\x12\x13meshtastic.protobuf*\xf6\x04\n\x07PortNum\x12\x0f\n\x0bUNKNOWN_APP\x10\x00\x12\x14\n\x10TEXT_MESSAGE_APP\x10\x01\x12\x17\n\x13REMOTE_HARDWARE_APP\x10\x02\x12\x10\n\x0cPOSITION_APP\x10\x03\x12\x10\n\x0cNODEINFO_APP\x10\x04\x12\x0f\n\x0bROUTING_APP\x10\x05\x12\r\n\tADMIN_APP\x10\x06\x12\x1f\n\x1bTEXT_MESSAGE_COMPRESSED_APP\x10\x07\x12\x10\n\x0cWAYPOINT_APP\x10\x08\x12\r\n\tAUDIO_APP\x10\t\x12\x18\n\x14\x44\x45TECTION_SENSOR_APP\x10\n\x12\r\n\tALERT_APP\x10\x0b\x12\x18\n\x14KEY_VERIFICATION_APP\x10\x0c\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\x18\n\x14RETICULUM_TUNNEL_APP\x10L\x12\x0f\n\x0b\x43\x41YENNE_APP\x10M\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')
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"meshtastic/protobuf/portnums.proto\x12\x13meshtastic.protobuf*\xa4\x06\n\x07PortNum\x12\x0f\n\x0bUNKNOWN_APP\x10\x00\x12\x14\n\x10TEXT_MESSAGE_APP\x10\x01\x12\x17\n\x13REMOTE_HARDWARE_APP\x10\x02\x12\x10\n\x0cPOSITION_APP\x10\x03\x12\x10\n\x0cNODEINFO_APP\x10\x04\x12\x0f\n\x0bROUTING_APP\x10\x05\x12\r\n\tADMIN_APP\x10\x06\x12\x1f\n\x1bTEXT_MESSAGE_COMPRESSED_APP\x10\x07\x12\x10\n\x0cWAYPOINT_APP\x10\x08\x12\r\n\tAUDIO_APP\x10\t\x12\x18\n\x14\x44\x45TECTION_SENSOR_APP\x10\n\x12\r\n\tALERT_APP\x10\x0b\x12\x18\n\x14KEY_VERIFICATION_APP\x10\x0c\x12\x14\n\x10REMOTE_SHELL_APP\x10\r\x12\r\n\tREPLY_APP\x10 \x12\x11\n\rIP_TUNNEL_APP\x10!\x12\x12\n\x0ePAXCOUNTER_APP\x10\"\x12\x1e\n\x1aSTORE_FORWARD_PLUSPLUS_APP\x10#\x12\x13\n\x0fNODE_STATUS_APP\x10$\x12\x13\n\x0fMESH_BEACON_APP\x10%\x12\x0e\n\nSERIAL_APP\x10@\x12\x15\n\x11STORE_FORWARD_APP\x10\x41\x12\x12\n\x0eRANGE_TEST_APP\x10\x42\x12\x11\n\rTELEMETRY_APP\x10\x43\x12\x0b\n\x07ZPS_APP\x10\x44\x12\x11\n\rSIMULATOR_APP\x10\x45\x12\x12\n\x0eTRACEROUTE_APP\x10\x46\x12\x14\n\x10NEIGHBORINFO_APP\x10G\x12\x0f\n\x0b\x41TAK_PLUGIN\x10H\x12\x12\n\x0eMAP_REPORT_APP\x10I\x12\x13\n\x0fPOWERSTRESS_APP\x10J\x12\x12\n\x0eLORAWAN_BRIDGE\x10K\x12\x18\n\x14RETICULUM_TUNNEL_APP\x10L\x12\x0f\n\x0b\x43\x41YENNE_APP\x10M\x12\x12\n\x0e\x41TAK_PLUGIN_V2\x10N\x12\x10\n\x0cLORA_OTA_APP\x10O\x12\x12\n\x0eGROUPALARM_APP\x10p\x12\x10\n\x0bPRIVATE_APP\x10\x80\x02\x12\x13\n\x0e\x41TAK_FORWARDER\x10\x81\x02\x12\x08\n\x03MAX\x10\xff\x03\x42^\n\x14org.meshtastic.protoB\x08PortnumsZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
@@ -22,5 +22,5 @@ if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
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=690
_globals['_PORTNUM']._serialized_end=864
# @@protoc_insertion_point(module_scope)
+94
View File
@@ -101,6 +101,10 @@ class _PortNumEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTy
"""
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.
@@ -117,6 +121,27 @@ class _PortNumEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTy
Paxcounter lib included in the firmware
ENCODING: protobuf
"""
STORE_FORWARD_PLUSPLUS_APP: _PortNum.ValueType # 35
"""
Store and Forward++ module included in the firmware
ENCODING: protobuf
This module is specifically for Native Linux nodes, and provides a Git-style
chain of messages.
"""
NODE_STATUS_APP: _PortNum.ValueType # 36
"""
Node Status module
ENCODING: protobuf
This module allows setting an extra string of status for a node.
Broadcasts on change and on a timer, possibly once a day.
"""
MESH_BEACON_APP: _PortNum.ValueType # 37
"""
Beacon module broadcast packets.
ENCODING: protobuf
Periodically broadcast by nodes in beacon mode; received by nodes with MeshBeaconConfig.FLAG_LISTEN_ENABLED.
Carries a text message plus optional channel/preset offers for client apps.
"""
SERIAL_APP: _PortNum.ValueType # 64
"""
Provides a hardware serial interface to send and receive from the Meshtastic network.
@@ -183,6 +208,11 @@ 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
@@ -194,6 +224,23 @@ class _PortNumEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTy
arbitrary telemetry over meshtastic that is not covered by telemetry.proto
ENCODING: CayenneLLP
"""
ATAK_PLUGIN_V2: _PortNum.ValueType # 78
"""
ATAK Plugin V2
Portnum for payloads from the official Meshtastic ATAK plugin using
TAKPacketV2 with zstd dictionary compression.
"""
LORA_OTA_APP: _PortNum.ValueType # 79
"""signed firmware updates over lora.
ENCODING: binary (ota-common transport frames)
"""
GROUPALARM_APP: _PortNum.ValueType # 112
"""
GroupAlarm integration
Used for transporting GroupAlarm-related messages between Meshtastic nodes
and companion applications/services.
"""
PRIVATE_APP: _PortNum.ValueType # 256
"""
Private applications should use portnums >= 256.
@@ -305,6 +352,10 @@ 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.
@@ -321,6 +372,27 @@ PAXCOUNTER_APP: PortNum.ValueType # 34
Paxcounter lib included in the firmware
ENCODING: protobuf
"""
STORE_FORWARD_PLUSPLUS_APP: PortNum.ValueType # 35
"""
Store and Forward++ module included in the firmware
ENCODING: protobuf
This module is specifically for Native Linux nodes, and provides a Git-style
chain of messages.
"""
NODE_STATUS_APP: PortNum.ValueType # 36
"""
Node Status module
ENCODING: protobuf
This module allows setting an extra string of status for a node.
Broadcasts on change and on a timer, possibly once a day.
"""
MESH_BEACON_APP: PortNum.ValueType # 37
"""
Beacon module broadcast packets.
ENCODING: protobuf
Periodically broadcast by nodes in beacon mode; received by nodes with MeshBeaconConfig.FLAG_LISTEN_ENABLED.
Carries a text message plus optional channel/preset offers for client apps.
"""
SERIAL_APP: PortNum.ValueType # 64
"""
Provides a hardware serial interface to send and receive from the Meshtastic network.
@@ -387,6 +459,11 @@ 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
@@ -398,6 +475,23 @@ App for transporting Cayenne Low Power Payload, popular for LoRaWAN sensor nodes
arbitrary telemetry over meshtastic that is not covered by telemetry.proto
ENCODING: CayenneLLP
"""
ATAK_PLUGIN_V2: PortNum.ValueType # 78
"""
ATAK Plugin V2
Portnum for payloads from the official Meshtastic ATAK plugin using
TAKPacketV2 with zstd dictionary compression.
"""
LORA_OTA_APP: PortNum.ValueType # 79
"""signed firmware updates over lora.
ENCODING: binary (ota-common transport frames)
"""
GROUPALARM_APP: PortNum.ValueType # 112
"""
GroupAlarm integration
Used for transporting GroupAlarm-related messages between Meshtastic nodes
and companion applications/services.
"""
PRIVATE_APP: PortNum.ValueType # 256
"""
Private applications should use portnums >= 256.
+6 -3
View File
@@ -11,9 +11,10 @@ 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(\tBg\n\x14org.meshtastic.protoB\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)
@@ -21,6 +22,8 @@ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.rtttl_p
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\021RTTTLConfigProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
_globals['_RTTTLCONFIG']._serialized_start=56
_globals['_RTTTLCONFIG']._serialized_end=87
_RTTTLCONFIG.fields_by_name['ringtone']._options = None
_RTTTLCONFIG.fields_by_name['ringtone']._serialized_options = b'\222?\003\010\347\001'
_globals['_RTTTLCONFIG']._serialized_start=90
_globals['_RTTTLCONFIG']._serialized_end=129
# @@protoc_insertion_point(module_scope)
+39
View File
@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: meshtastic/protobuf/serial_hal.proto
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
from meshtastic.protobuf import nanopb_pb2 as meshtastic_dot_protobuf_dot_nanopb__pb2
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n$meshtastic/protobuf/serial_hal.proto\x12\x13meshtastic.protobuf\x1a meshtastic/protobuf/nanopb.proto\"\xb3\x02\n\x10SerialHalCommand\x12\x16\n\x0etransaction_id\x18\x01 \x01(\r\x12\x38\n\x04type\x18\x02 \x01(\x0e\x32*.meshtastic.protobuf.SerialHalCommand.Type\x12\x0b\n\x03pin\x18\x03 \x01(\r\x12\r\n\x05value\x18\x04 \x01(\r\x12\x0c\n\x04mode\x18\x05 \x01(\r\x12\x14\n\x04\x64\x61ta\x18\x06 \x01(\x0c\x42\x06\x92?\x03\x08\x80\x04\"\x8c\x01\n\x04Type\x12\t\n\x05UNSET\x10\x00\x12\x0c\n\x08PIN_MODE\x10\x01\x12\x11\n\rDIGITAL_WRITE\x10\x02\x12\x10\n\x0c\x44IGITAL_READ\x10\x03\x12\x14\n\x10\x41TTACH_INTERRUPT\x10\x04\x12\x14\n\x10\x44\x45TACH_INTERRUPT\x10\x05\x12\x10\n\x0cSPI_TRANSFER\x10\x06\x12\x08\n\x04NOOP\x10\x07\"\xe4\x01\n\x11SerialHalResponse\x12\x16\n\x0etransaction_id\x18\x01 \x01(\r\x12=\n\x06result\x18\x02 \x01(\x0e\x32-.meshtastic.protobuf.SerialHalResponse.Result\x12\r\n\x05value\x18\x03 \x01(\r\x12\x14\n\x04\x64\x61ta\x18\x04 \x01(\x0c\x42\x06\x92?\x03\x08\x80\x04\x12\x14\n\x05\x65rror\x18\x05 \x01(\tB\x05\x92?\x02\x08P\"=\n\x06Result\x12\x06\n\x02OK\x10\x00\x12\t\n\x05\x45RROR\x10\x01\x12\x0f\n\x0b\x42\x41\x44_REQUEST\x10\x02\x12\x0f\n\x0bUNSUPPORTED\x10\x03\x42_\n\x14org.meshtastic.protoB\tSerialHalZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.serial_hal_pb2', _globals)
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\tSerialHalZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
_SERIALHALCOMMAND.fields_by_name['data']._options = None
_SERIALHALCOMMAND.fields_by_name['data']._serialized_options = b'\222?\003\010\200\004'
_SERIALHALRESPONSE.fields_by_name['data']._options = None
_SERIALHALRESPONSE.fields_by_name['data']._serialized_options = b'\222?\003\010\200\004'
_SERIALHALRESPONSE.fields_by_name['error']._options = None
_SERIALHALRESPONSE.fields_by_name['error']._serialized_options = b'\222?\002\010P'
_globals['_SERIALHALCOMMAND']._serialized_start=96
_globals['_SERIALHALCOMMAND']._serialized_end=403
_globals['_SERIALHALCOMMAND_TYPE']._serialized_start=263
_globals['_SERIALHALCOMMAND_TYPE']._serialized_end=403
_globals['_SERIALHALRESPONSE']._serialized_start=406
_globals['_SERIALHALRESPONSE']._serialized_end=634
_globals['_SERIALHALRESPONSE_RESULT']._serialized_start=573
_globals['_SERIALHALRESPONSE_RESULT']._serialized_end=634
# @@protoc_insertion_point(module_scope)
+140
View File
@@ -0,0 +1,140 @@
"""
@generated by mypy-protobuf. Do not edit manually!
isort:skip_file
"""
import builtins
import google.protobuf.descriptor
import google.protobuf.internal.enum_type_wrapper
import google.protobuf.message
import sys
import typing
if sys.version_info >= (3, 10):
import typing as typing_extensions
else:
import typing_extensions
DESCRIPTOR: google.protobuf.descriptor.FileDescriptor
@typing.final
class SerialHalCommand(google.protobuf.message.Message):
"""SerialHalCommand messages are sent from host to device over the SerialHal
framing stream. Responses normally come back as SerialHalResponse with the
same transaction_id.
Interrupt notifications are the one asynchronous exception: the device emits
an unsolicited SerialHalResponse with transaction_id == 0 and value == pin.
Host implementations should treat those frames as interrupt events rather
than replies to an outstanding request.
"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
class _Type:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
class _TypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[SerialHalCommand._Type.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
UNSET: SerialHalCommand._Type.ValueType # 0
PIN_MODE: SerialHalCommand._Type.ValueType # 1
DIGITAL_WRITE: SerialHalCommand._Type.ValueType # 2
DIGITAL_READ: SerialHalCommand._Type.ValueType # 3
ATTACH_INTERRUPT: SerialHalCommand._Type.ValueType # 4
DETACH_INTERRUPT: SerialHalCommand._Type.ValueType # 5
SPI_TRANSFER: SerialHalCommand._Type.ValueType # 6
NOOP: SerialHalCommand._Type.ValueType # 7
class Type(_Type, metaclass=_TypeEnumTypeWrapper): ...
UNSET: SerialHalCommand.Type.ValueType # 0
PIN_MODE: SerialHalCommand.Type.ValueType # 1
DIGITAL_WRITE: SerialHalCommand.Type.ValueType # 2
DIGITAL_READ: SerialHalCommand.Type.ValueType # 3
ATTACH_INTERRUPT: SerialHalCommand.Type.ValueType # 4
DETACH_INTERRUPT: SerialHalCommand.Type.ValueType # 5
SPI_TRANSFER: SerialHalCommand.Type.ValueType # 6
NOOP: SerialHalCommand.Type.ValueType # 7
TRANSACTION_ID_FIELD_NUMBER: builtins.int
TYPE_FIELD_NUMBER: builtins.int
PIN_FIELD_NUMBER: builtins.int
VALUE_FIELD_NUMBER: builtins.int
MODE_FIELD_NUMBER: builtins.int
DATA_FIELD_NUMBER: builtins.int
transaction_id: builtins.int
"""Host-assigned request id. Replies echo this id back in
SerialHalResponse.transaction_id.
"""
type: global___SerialHalCommand.Type.ValueType
pin: builtins.int
value: builtins.int
mode: builtins.int
data: builtins.bytes
def __init__(
self,
*,
transaction_id: builtins.int = ...,
type: global___SerialHalCommand.Type.ValueType = ...,
pin: builtins.int = ...,
value: builtins.int = ...,
mode: builtins.int = ...,
data: builtins.bytes = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["data", b"data", "mode", b"mode", "pin", b"pin", "transaction_id", b"transaction_id", "type", b"type", "value", b"value"]) -> None: ...
global___SerialHalCommand = SerialHalCommand
@typing.final
class SerialHalResponse(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
class _Result:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
class _ResultEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[SerialHalResponse._Result.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
OK: SerialHalResponse._Result.ValueType # 0
ERROR: SerialHalResponse._Result.ValueType # 1
BAD_REQUEST: SerialHalResponse._Result.ValueType # 2
UNSUPPORTED: SerialHalResponse._Result.ValueType # 3
class Result(_Result, metaclass=_ResultEnumTypeWrapper): ...
OK: SerialHalResponse.Result.ValueType # 0
ERROR: SerialHalResponse.Result.ValueType # 1
BAD_REQUEST: SerialHalResponse.Result.ValueType # 2
UNSUPPORTED: SerialHalResponse.Result.ValueType # 3
TRANSACTION_ID_FIELD_NUMBER: builtins.int
RESULT_FIELD_NUMBER: builtins.int
VALUE_FIELD_NUMBER: builtins.int
DATA_FIELD_NUMBER: builtins.int
ERROR_FIELD_NUMBER: builtins.int
transaction_id: builtins.int
"""Matches the originating SerialHalCommand.transaction_id for normal
request/response traffic.
A value of 0 indicates an unsolicited interrupt notification generated by
the device. In that case, the host should interpret value as the GPIO pin
that triggered.
"""
result: global___SerialHalResponse.Result.ValueType
value: builtins.int
"""Used by DIGITAL_READ replies and interrupt notifications. For interrupt
notifications (transaction_id == 0), this carries the pin number.
"""
data: builtins.bytes
error: builtins.str
def __init__(
self,
*,
transaction_id: builtins.int = ...,
result: global___SerialHalResponse.Result.ValueType = ...,
value: builtins.int = ...,
data: builtins.bytes = ...,
error: builtins.str = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["data", b"data", "error", b"error", "result", b"result", "transaction_id", b"transaction_id", "value", b"value"]) -> None: ...
global___SerialHalResponse = SerialHalResponse
+14 -11
View File
@@ -11,9 +11,10 @@ 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\x07variantBk\n\x14org.meshtastic.protoB\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)
@@ -21,14 +22,16 @@ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.storefo
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\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
_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)
+84 -21
View File
File diff suppressed because one or more lines are too long.
+498 -24
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
"""
@@ -207,6 +209,54 @@ class _TelemetrySensorTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wra
"""
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
"""
SPA06: _TelemetrySensorType.ValueType # 54
"""
SPA06 pressure and temperature
"""
HM330X: _TelemetrySensorType.ValueType # 55
"""
HM330X PM SENSOR
"""
SEN6X: _TelemetrySensorType.ValueType # 56
"""
Sensirion SEN6X PM/RHT/VOC/NOx/CO2/HCHO sensor family (SEN62, SEN63C, SEN65, SEN66, SEN68, SEN69C)
"""
AS3935: _TelemetrySensorType.ValueType # 57
"""
AS3935 Franklin lightning sensor
"""
class TelemetrySensorType(_TelemetrySensorType, metaclass=_TelemetrySensorTypeEnumTypeWrapper):
"""
@@ -243,7 +293,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
"""
@@ -263,7 +313,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
"""
@@ -283,7 +333,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
"""
@@ -397,6 +447,54 @@ 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
"""
SPA06: TelemetrySensorType.ValueType # 54
"""
SPA06 pressure and temperature
"""
HM330X: TelemetrySensorType.ValueType # 55
"""
HM330X PM SENSOR
"""
SEN6X: TelemetrySensorType.ValueType # 56
"""
Sensirion SEN6X PM/RHT/VOC/NOx/CO2/HCHO sensor family (SEN62, SEN63C, SEN65, SEN66, SEN68, SEN69C)
"""
AS3935: TelemetrySensorType.ValueType # 57
"""
AS3935 Franklin lightning sensor
"""
global___TelemetrySensorType = TelemetrySensorType
@typing.final
@@ -486,6 +584,25 @@ class EnvironmentMetrics(google.protobuf.message.Message):
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
ADC_VOLTAGE_CH0_FIELD_NUMBER: builtins.int
ADC_VOLTAGE_CH1_FIELD_NUMBER: builtins.int
ADC_VOLTAGE_CH2_FIELD_NUMBER: builtins.int
ADC_VOLTAGE_CH3_FIELD_NUMBER: builtins.int
ADC_VOLTAGE_CH4_FIELD_NUMBER: builtins.int
ADC_VOLTAGE_CH5_FIELD_NUMBER: builtins.int
ADC_VOLTAGE_CH6_FIELD_NUMBER: builtins.int
ADC_VOLTAGE_CH7_FIELD_NUMBER: builtins.int
ONE_WIRE_TEMPERATURE_CH0_FIELD_NUMBER: builtins.int
ONE_WIRE_TEMPERATURE_CH1_FIELD_NUMBER: builtins.int
ONE_WIRE_TEMPERATURE_CH2_FIELD_NUMBER: builtins.int
ONE_WIRE_TEMPERATURE_CH3_FIELD_NUMBER: builtins.int
ONE_WIRE_TEMPERATURE_CH4_FIELD_NUMBER: builtins.int
ONE_WIRE_TEMPERATURE_CH5_FIELD_NUMBER: builtins.int
ONE_WIRE_TEMPERATURE_CH6_FIELD_NUMBER: builtins.int
ONE_WIRE_TEMPERATURE_CH7_FIELD_NUMBER: builtins.int
LIGHTNING_STRIKE_COUNT_1H_FIELD_NUMBER: builtins.int
LIGHTNING_DISTANCE_KM_FIELD_NUMBER: builtins.int
temperature: builtins.float
"""
Temperature measured
@@ -576,6 +693,84 @@ class EnvironmentMetrics(google.protobuf.message.Message):
"""
Soil temperature measured (*C)
"""
adc_voltage_ch0: builtins.float
"""
Multi-channel ADC Voltage Channel 0 (V)
"""
adc_voltage_ch1: builtins.float
"""
Multi-channel ADC Voltage Channel 1 (V)
"""
adc_voltage_ch2: builtins.float
"""
Multi-channel ADC Voltage Channel 2 (V)
"""
adc_voltage_ch3: builtins.float
"""
Multi-channel ADC Voltage Channel 3 (V)
"""
adc_voltage_ch4: builtins.float
"""
Multi-channel ADC Voltage Channel 4 (V)
"""
adc_voltage_ch5: builtins.float
"""
Multi-channel ADC Voltage Channel 5 (V)
"""
adc_voltage_ch6: builtins.float
"""
Multi-channel ADC Voltage Channel 6 (V)
"""
adc_voltage_ch7: builtins.float
"""
Multi-channel ADC Voltage Channel 7 (V)
"""
one_wire_temperature_ch0: builtins.float
"""
Multi-channel One-Wire Temperature Channel 0 (*C)
"""
one_wire_temperature_ch1: builtins.float
"""
Multi-channel One-Wire Temperature Channel 1 (*C)
"""
one_wire_temperature_ch2: builtins.float
"""
Multi-channel One-Wire Temperature Channel 2 (*C)
"""
one_wire_temperature_ch3: builtins.float
"""
Multi-channel One-Wire Temperature Channel 3 (*C)
"""
one_wire_temperature_ch4: builtins.float
"""
Multi-channel One-Wire Temperature Channel 4 (*C)
"""
one_wire_temperature_ch5: builtins.float
"""
Multi-channel One-Wire Temperature Channel 5 (*C)
"""
one_wire_temperature_ch6: builtins.float
"""
Multi-channel One-Wire Temperature Channel 6 (*C)
"""
one_wire_temperature_ch7: builtins.float
"""
Multi-channel One-Wire Temperature Channel 7 (*C)
"""
lightning_strike_count_1h: builtins.int
"""
Lightning strikes detected in the last hour
"""
lightning_distance_km: builtins.float
"""
Estimated distance to the leading edge of the storm, in km
"""
@property
def one_wire_temperature(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.float]:
"""
Never implemented, but Voltage may be mis-interpreted by old clients as temperature
"""
def __init__(
self,
*,
@@ -601,9 +796,44 @@ class EnvironmentMetrics(google.protobuf.message.Message):
rainfall_24h: builtins.float | None = ...,
soil_moisture: builtins.int | None = ...,
soil_temperature: builtins.float | None = ...,
one_wire_temperature: collections.abc.Iterable[builtins.float] | None = ...,
adc_voltage_ch0: builtins.float | None = ...,
adc_voltage_ch1: builtins.float | None = ...,
adc_voltage_ch2: builtins.float | None = ...,
adc_voltage_ch3: builtins.float | None = ...,
adc_voltage_ch4: builtins.float | None = ...,
adc_voltage_ch5: builtins.float | None = ...,
adc_voltage_ch6: builtins.float | None = ...,
adc_voltage_ch7: builtins.float | None = ...,
one_wire_temperature_ch0: builtins.float | None = ...,
one_wire_temperature_ch1: builtins.float | None = ...,
one_wire_temperature_ch2: builtins.float | None = ...,
one_wire_temperature_ch3: builtins.float | None = ...,
one_wire_temperature_ch4: builtins.float | None = ...,
one_wire_temperature_ch5: builtins.float | None = ...,
one_wire_temperature_ch6: builtins.float | None = ...,
one_wire_temperature_ch7: builtins.float | None = ...,
lightning_strike_count_1h: builtins.int | None = ...,
lightning_distance_km: 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", "_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", "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: ...
def HasField(self, field_name: typing.Literal["_adc_voltage_ch0", b"_adc_voltage_ch0", "_adc_voltage_ch1", b"_adc_voltage_ch1", "_adc_voltage_ch2", b"_adc_voltage_ch2", "_adc_voltage_ch3", b"_adc_voltage_ch3", "_adc_voltage_ch4", b"_adc_voltage_ch4", "_adc_voltage_ch5", b"_adc_voltage_ch5", "_adc_voltage_ch6", b"_adc_voltage_ch6", "_adc_voltage_ch7", b"_adc_voltage_ch7", "_barometric_pressure", b"_barometric_pressure", "_current", b"_current", "_distance", b"_distance", "_gas_resistance", b"_gas_resistance", "_iaq", b"_iaq", "_ir_lux", b"_ir_lux", "_lightning_distance_km", b"_lightning_distance_km", "_lightning_strike_count_1h", b"_lightning_strike_count_1h", "_lux", b"_lux", "_one_wire_temperature_ch0", b"_one_wire_temperature_ch0", "_one_wire_temperature_ch1", b"_one_wire_temperature_ch1", "_one_wire_temperature_ch2", b"_one_wire_temperature_ch2", "_one_wire_temperature_ch3", b"_one_wire_temperature_ch3", "_one_wire_temperature_ch4", b"_one_wire_temperature_ch4", "_one_wire_temperature_ch5", b"_one_wire_temperature_ch5", "_one_wire_temperature_ch6", b"_one_wire_temperature_ch6", "_one_wire_temperature_ch7", b"_one_wire_temperature_ch7", "_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", "adc_voltage_ch0", b"adc_voltage_ch0", "adc_voltage_ch1", b"adc_voltage_ch1", "adc_voltage_ch2", b"adc_voltage_ch2", "adc_voltage_ch3", b"adc_voltage_ch3", "adc_voltage_ch4", b"adc_voltage_ch4", "adc_voltage_ch5", b"adc_voltage_ch5", "adc_voltage_ch6", b"adc_voltage_ch6", "adc_voltage_ch7", b"adc_voltage_ch7", "barometric_pressure", b"barometric_pressure", "current", b"current", "distance", b"distance", "gas_resistance", b"gas_resistance", "iaq", b"iaq", "ir_lux", b"ir_lux", "lightning_distance_km", b"lightning_distance_km", "lightning_strike_count_1h", b"lightning_strike_count_1h", "lux", b"lux", "one_wire_temperature_ch0", b"one_wire_temperature_ch0", "one_wire_temperature_ch1", b"one_wire_temperature_ch1", "one_wire_temperature_ch2", b"one_wire_temperature_ch2", "one_wire_temperature_ch3", b"one_wire_temperature_ch3", "one_wire_temperature_ch4", b"one_wire_temperature_ch4", "one_wire_temperature_ch5", b"one_wire_temperature_ch5", "one_wire_temperature_ch6", b"one_wire_temperature_ch6", "one_wire_temperature_ch7", b"one_wire_temperature_ch7", "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["_adc_voltage_ch0", b"_adc_voltage_ch0", "_adc_voltage_ch1", b"_adc_voltage_ch1", "_adc_voltage_ch2", b"_adc_voltage_ch2", "_adc_voltage_ch3", b"_adc_voltage_ch3", "_adc_voltage_ch4", b"_adc_voltage_ch4", "_adc_voltage_ch5", b"_adc_voltage_ch5", "_adc_voltage_ch6", b"_adc_voltage_ch6", "_adc_voltage_ch7", b"_adc_voltage_ch7", "_barometric_pressure", b"_barometric_pressure", "_current", b"_current", "_distance", b"_distance", "_gas_resistance", b"_gas_resistance", "_iaq", b"_iaq", "_ir_lux", b"_ir_lux", "_lightning_distance_km", b"_lightning_distance_km", "_lightning_strike_count_1h", b"_lightning_strike_count_1h", "_lux", b"_lux", "_one_wire_temperature_ch0", b"_one_wire_temperature_ch0", "_one_wire_temperature_ch1", b"_one_wire_temperature_ch1", "_one_wire_temperature_ch2", b"_one_wire_temperature_ch2", "_one_wire_temperature_ch3", b"_one_wire_temperature_ch3", "_one_wire_temperature_ch4", b"_one_wire_temperature_ch4", "_one_wire_temperature_ch5", b"_one_wire_temperature_ch5", "_one_wire_temperature_ch6", b"_one_wire_temperature_ch6", "_one_wire_temperature_ch7", b"_one_wire_temperature_ch7", "_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", "adc_voltage_ch0", b"adc_voltage_ch0", "adc_voltage_ch1", b"adc_voltage_ch1", "adc_voltage_ch2", b"adc_voltage_ch2", "adc_voltage_ch3", b"adc_voltage_ch3", "adc_voltage_ch4", b"adc_voltage_ch4", "adc_voltage_ch5", b"adc_voltage_ch5", "adc_voltage_ch6", b"adc_voltage_ch6", "adc_voltage_ch7", b"adc_voltage_ch7", "barometric_pressure", b"barometric_pressure", "current", b"current", "distance", b"distance", "gas_resistance", b"gas_resistance", "iaq", b"iaq", "ir_lux", b"ir_lux", "lightning_distance_km", b"lightning_distance_km", "lightning_strike_count_1h", b"lightning_strike_count_1h", "lux", b"lux", "one_wire_temperature", b"one_wire_temperature", "one_wire_temperature_ch0", b"one_wire_temperature_ch0", "one_wire_temperature_ch1", b"one_wire_temperature_ch1", "one_wire_temperature_ch2", b"one_wire_temperature_ch2", "one_wire_temperature_ch3", b"one_wire_temperature_ch3", "one_wire_temperature_ch4", b"one_wire_temperature_ch4", "one_wire_temperature_ch5", b"one_wire_temperature_ch5", "one_wire_temperature_ch6", b"one_wire_temperature_ch6", "one_wire_temperature_ch7", b"one_wire_temperature_ch7", "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["_adc_voltage_ch0", b"_adc_voltage_ch0"]) -> typing.Literal["adc_voltage_ch0"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_adc_voltage_ch1", b"_adc_voltage_ch1"]) -> typing.Literal["adc_voltage_ch1"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_adc_voltage_ch2", b"_adc_voltage_ch2"]) -> typing.Literal["adc_voltage_ch2"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_adc_voltage_ch3", b"_adc_voltage_ch3"]) -> typing.Literal["adc_voltage_ch3"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_adc_voltage_ch4", b"_adc_voltage_ch4"]) -> typing.Literal["adc_voltage_ch4"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_adc_voltage_ch5", b"_adc_voltage_ch5"]) -> typing.Literal["adc_voltage_ch5"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_adc_voltage_ch6", b"_adc_voltage_ch6"]) -> typing.Literal["adc_voltage_ch6"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_adc_voltage_ch7", b"_adc_voltage_ch7"]) -> typing.Literal["adc_voltage_ch7"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_barometric_pressure", b"_barometric_pressure"]) -> typing.Literal["barometric_pressure"] | None: ...
@typing.overload
@@ -617,8 +847,28 @@ class EnvironmentMetrics(google.protobuf.message.Message):
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_ir_lux", b"_ir_lux"]) -> typing.Literal["ir_lux"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_lightning_distance_km", b"_lightning_distance_km"]) -> typing.Literal["lightning_distance_km"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_lightning_strike_count_1h", b"_lightning_strike_count_1h"]) -> typing.Literal["lightning_strike_count_1h"] | None: ...
@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["_one_wire_temperature_ch0", b"_one_wire_temperature_ch0"]) -> typing.Literal["one_wire_temperature_ch0"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_one_wire_temperature_ch1", b"_one_wire_temperature_ch1"]) -> typing.Literal["one_wire_temperature_ch1"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_one_wire_temperature_ch2", b"_one_wire_temperature_ch2"]) -> typing.Literal["one_wire_temperature_ch2"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_one_wire_temperature_ch3", b"_one_wire_temperature_ch3"]) -> typing.Literal["one_wire_temperature_ch3"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_one_wire_temperature_ch4", b"_one_wire_temperature_ch4"]) -> typing.Literal["one_wire_temperature_ch4"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_one_wire_temperature_ch5", b"_one_wire_temperature_ch5"]) -> typing.Literal["one_wire_temperature_ch5"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_one_wire_temperature_ch6", b"_one_wire_temperature_ch6"]) -> typing.Literal["one_wire_temperature_ch6"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_one_wire_temperature_ch7", b"_one_wire_temperature_ch7"]) -> typing.Literal["one_wire_temperature_ch7"] | 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: ...
@@ -701,43 +951,43 @@ class PowerMetrics(google.protobuf.message.Message):
"""
ch4_voltage: builtins.float
"""
Voltage (Ch4)
Voltage (Ch4) - TODO Remove
"""
ch4_current: builtins.float
"""
Current (Ch4)
Current (Ch4) - TODO Remove
"""
ch5_voltage: builtins.float
"""
Voltage (Ch5)
Voltage (Ch5) - TODO Remove
"""
ch5_current: builtins.float
"""
Current (Ch5)
Current (Ch5) - TODO Remove
"""
ch6_voltage: builtins.float
"""
Voltage (Ch6)
Voltage (Ch6) - TODO Remove
"""
ch6_current: builtins.float
"""
Current (Ch6)
Current (Ch6) - TODO Remove
"""
ch7_voltage: builtins.float
"""
Voltage (Ch7)
Voltage (Ch7) - TODO Remove
"""
ch7_current: builtins.float
"""
Current (Ch7)
Current (Ch7) - TODO Remove
"""
ch8_voltage: builtins.float
"""
Voltage (Ch8)
Voltage (Ch8) - TODO Remove
"""
ch8_current: builtins.float
"""
Current (Ch8)
Current (Ch8) - TODO Remove
"""
def __init__(
self,
@@ -829,6 +1079,7 @@ class AirQualityMetrics(google.protobuf.message.Message):
PM_VOC_IDX_FIELD_NUMBER: builtins.int
PM_NOX_IDX_FIELD_NUMBER: builtins.int
PARTICLES_TPS_FIELD_NUMBER: builtins.int
PM_STATUS_FLAGS_FIELD_NUMBER: builtins.int
pm10_standard: builtins.int
"""
Concentration Units Standard PM1.0 in ug/m3
@@ -929,6 +1180,13 @@ class AirQualityMetrics(google.protobuf.message.Message):
"""
Typical Particle Size in um
"""
pm_status_flags: builtins.int
"""
Raw PM sensor device status/error register bitmask, as defined by the sensor's own datasheet
(currently populated by the SEN6X family: bit 4 fan error, bit 6 RH&T error, bit 7 gas/VOC-NOx
error, bit 9 CO2 error (SEN66), bit 10 HCHO error, bit 11 PM error, bit 12 CO2 error (SEN63C/SEN69C),
bit 21 fan speed warning)
"""
def __init__(
self,
*,
@@ -957,9 +1215,10 @@ class AirQualityMetrics(google.protobuf.message.Message):
pm_voc_idx: builtins.float | None = ...,
pm_nox_idx: builtins.float | None = ...,
particles_tps: builtins.float | None = ...,
pm_status_flags: builtins.int | None = ...,
) -> 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: ...
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_status_flags", b"_pm_status_flags", "_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_status_flags", b"pm_status_flags", "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_status_flags", b"_pm_status_flags", "_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_status_flags", b"pm_status_flags", "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
@@ -1007,6 +1266,8 @@ class AirQualityMetrics(google.protobuf.message.Message):
@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_status_flags", b"_pm_status_flags"]) -> typing.Literal["pm_status_flags"] | 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: ...
@@ -1035,6 +1296,7 @@ class LocalStats(google.protobuf.message.Message):
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)
@@ -1093,6 +1355,10 @@ class LocalStats(google.protobuf.message.Message):
"""
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,
*,
@@ -1110,11 +1376,70 @@ class LocalStats(google.protobuf.message.Message):
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", "heap_free_bytes", b"heap_free_bytes", "heap_total_bytes", b"heap_total_bytes", "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: ...
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):
"""
@@ -1250,6 +1575,7 @@ class Telemetry(google.protobuf.message.Message):
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
@@ -1296,6 +1622,12 @@ class Telemetry(google.protobuf.message.Message):
Linux host metrics
"""
@property
def traffic_management_stats(self) -> global___TrafficManagementStats:
"""
Traffic management statistics
"""
def __init__(
self,
*,
@@ -1307,10 +1639,11 @@ class Telemetry(google.protobuf.message.Message):
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", "host_metrics", b"host_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", "host_metrics", b"host_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", "host_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
@@ -1341,3 +1674,144 @@ 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 AS3935Config(google.protobuf.message.Message):
"""
AS3935 lightning sensor configuration, for saving to flash
"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
TUNING_CAP_PF_FIELD_NUMBER: builtins.int
tuning_cap_pf: builtins.int
"""
Antenna tuning capacitance in pF, 0 to 120 in steps of 8. The chip does not retain
this across power loss, so it is stored here and re-applied on every boot.
"""
def __init__(
self,
*,
tuning_cap_pf: builtins.int = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["tuning_cap_pf", b"tuning_cap_pf"]) -> None: ...
global___AS3935Config = AS3935Config
@typing.final
class SEN5XState(google.protobuf.message.Message):
"""
SEN5X State, for saving to flash (to be merged with SEN6XState)
"""
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
@typing.final
class SEN6XState(google.protobuf.message.Message):
"""
SEN6X 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 SEN6X
"""
last_cleaning_valid: builtins.bool
"""
Last cleaning time for SEN6X - 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 models with a VOC sensor (SEN65, SEN66, SEN68, SEN69C)
"""
voc_state_valid: builtins.bool
"""
Last VOC state validity flag, for models with a VOC sensor (SEN65, SEN66, SEN68, SEN69C)
"""
voc_state_array: builtins.int
"""
VOC state array (8x uint8t), for models with a VOC sensor (SEN65, SEN66, SEN68, SEN69C)
"""
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___SEN6XState = SEN6XState
+12 -5
View File
@@ -11,9 +11,10 @@ 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\x62\n\x14org.meshtastic.protoB\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)
@@ -21,8 +22,14 @@ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.xmodem_
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\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
_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)
+11 -7
View File
@@ -35,8 +35,6 @@ class SerialInterface(StreamInterface):
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:
@@ -52,22 +50,28 @@ class SerialInterface(StreamInterface):
else:
self.devPath = ports[0]
StreamInterface.__init__(
self, debugOut=debugOut, noProto=noProto, connectNow=connectNow, noNodes=noNodes, timeout=timeout
)
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(self.devPath, encoding="utf8") as f:
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, timeout=timeout
)
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
+26 -9
View File
@@ -1,5 +1,6 @@
"""Stream Interface base class
"""
import contextlib
import io
import logging
import threading
@@ -39,15 +40,14 @@ class StreamInterface(MeshInterface):
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
@@ -61,9 +61,17 @@ class StreamInterface(MeshInterface):
# 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
@@ -136,7 +144,16 @@ class StreamInterface(MeshInterface):
# 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."""
+96 -33
View File
@@ -4,14 +4,21 @@
import contextlib
import logging
import socket
import threading
import time
from typing import Optional
from meshtastic.stream_interface import StreamInterface
DEFAULT_TCP_PORT = 4403
# How long close() gives the device to consume what we last wrote before forcing
# the connection down. See close() for why this exists.
GRACEFUL_CLOSE_TIMEOUT = 0.25
logger = logging.getLogger(__name__)
class TCPInterface(StreamInterface):
"""Interface class for meshtastic devices over a TCP link"""
@@ -19,10 +26,10 @@ 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
@@ -31,20 +38,19 @@ class TCPInterface(StreamInterface):
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:
self.myConnect()
else:
self.socket = None
super().__init__(debugOut=debugOut, noProto=noProto, connectNow=connectNow, noNodes=noNodes, timeout=timeout)
super().__init__(
debugOut=debugOut,
noProto=noProto,
connectNow=connectNow,
noNodes=noNodes,
timeout=timeout,
)
def __repr__(self):
rep = f"TCPInterface({self.hostname!r}"
@@ -68,30 +74,70 @@ class TCPInterface(StreamInterface):
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"""
logger.debug(f"Connecting to {self.hostname}") # type: ignore[str-bytes-safe]
"""Connect to socket (without attempting to start the interface's receive thread)"""
logger.debug(f"Connecting to {self.hostname}") # type: ignore[str-bytes-safe]
server_address = (self.hostname, self.portNumber)
self.socket = socket.create_connection(server_address)
def _wait_for_reader_exit(self, timeout: float) -> None:
"""Wait briefly for the reader thread to drain and exit after a half-close.
Returns as soon as it exits, or after timeout: the device is not obliged
to close just because we did.
"""
rx = getattr(self, "_rxThread", None)
if rx is None or rx is threading.current_thread():
return
with contextlib.suppress(Exception):
rx.join(timeout)
def close(self) -> None:
"""Close a connection to the device"""
"""Close a connection to the device."""
logger.debug("Closing TCP stream")
super().close()
# Sometimes the socket read might be blocked in the reader thread.
# Therefore we force the shutdown by closing the socket here
# 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
# Half-close first. shutdown(SHUT_WR) sends FIN, which tells the
# device we are done writing and lets it consume what we last wrote
# before the connection goes away -- typically the admin message a
# one-shot command such as `--set` just sent.
#
# Going straight to shutdown(SHUT_RDWR) + close() while either side
# still has unread data makes the stack send RST instead. Winsock
# then discards data the peer had already received but not yet read,
# so the write is lost; Linux delivers it before reporting
# ECONNRESET, which is why this only bites on Windows.
with contextlib.suppress(Exception):
self.socket.shutdown(socket.SHUT_WR)
self._wait_for_reader_exit(GRACEFUL_CLOSE_TIMEOUT)
with contextlib.suppress(
Exception
): # Ignore errors in shutdown, because we might have a race with the server
self._socket_shutdown()
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"""
"""Write an array of bytes to our stream"""
if self.socket is not None:
self.socket.send(b)
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"""
@@ -99,19 +145,36 @@ class TCPInterface(StreamInterface):
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("dead socket, re-connecting")
# cleanup and reconnect socket without breaking reader thread
with contextlib.suppress(Exception):
self._socket_shutdown()
self.socket.close()
self.socket = None
time.sleep(1)
self.myConnect()
self._startConfig()
return None
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()
+81
View File
@@ -8,6 +8,87 @@ import pytest
from meshtastic import mt_config
from ..mesh_interface import MeshInterface
from .firmware_harness import (
CHAIN_TOPOLOGY,
DEFAULT_BASE_PORT,
SimMesh,
find_meshtasticd,
is_compatible_host,
)
from .fw_helpers import set_region
# Use a different base port for the single-node fixture so it doesn't
# conflict with the multi-node mesh fixture.
SINGLE_NODE_BASE_PORT = DEFAULT_BASE_PORT + 100
def _skip_firmware_if_unavailable() -> None:
"""Skip the test when meshtasticd can't run on this host."""
if not is_compatible_host():
pytest.skip("meshtasticd firmware tests require Linux")
if find_meshtasticd() is None:
pytest.skip(
"meshtasticd not found — set MESHTASTICD_BIN or install it on PATH"
)
@pytest.fixture(scope="function")
def firmware_node():
"""A single meshtasticd sim node for smokevirt tests.
Function-scoped so every test gets a freshly-erased node with no
state leaking from previous tests. This makes destructive commands
(``--reboot``, ``--set factory_reset true``) safe to run and lets
tests be order-independent.
Yields the SimNode instance. The node is booted with a fresh erased
config and listens on localhost at its TCP port. Region is set to US
so modem-preset tests work against firmware >= 2.8, which clamps
presets to the legal set for the current region.
"""
_skip_firmware_if_unavailable()
mesh = SimMesh(n_nodes=1, base_port=SINGLE_NODE_BASE_PORT)
mesh.start()
node = mesh.get_node(0)
set_region(node.port, "US")
# The region commit restarts the TCP listener, so reconnect the harness
# interface in case a test wants to use it directly.
if node.iface is not None:
try:
node.iface.close()
except Exception: # pylint: disable=broad-except
pass
node.connect()
yield node
mesh.stop()
@pytest.fixture(scope="function")
def firmware_mesh():
"""A 3-node chain (A-B-C) meshtasticd sim mesh for smokemesh tests.
Yields the SimMesh instance. Nodes are connected and the SIMULATOR_APP
packet bridge is running. Region is set to US for firmware >= 2.8
compatibility, interfaces are reconnected after the region change, and
node DB convergence is awaited.
"""
_skip_firmware_if_unavailable()
mesh = SimMesh(n_nodes=3, topology=CHAIN_TOPOLOGY)
mesh.start()
for node in mesh.nodes:
set_region(node.port, "US")
# The region commit restarts each node's TCP listener, so reconnect the
# harness interfaces before waiting for convergence.
for node in mesh.nodes:
if node.iface is not None:
try:
node.iface.close()
except Exception: # pylint: disable=broad-except
pass
node.connect()
mesh.wait_for_convergence(timeout=30)
yield mesh
mesh.stop()
@pytest.fixture
+357
View File
@@ -0,0 +1,357 @@
"""Test harness for running real meshtasticd firmware instances.
Launches one or more meshtasticd processes in simulator mode (-s) and bridges
their "over-the-air" packets via the SIMULATOR_APP protocol so that multiple
instances can communicate as if via LoRa, with a configurable topology.
The harness expects meshtasticd to be available on PATH or via the
MESHTASTICD_BIN environment variable. It does not download or build the
binary itself.
"""
import logging
import os
import platform
import shutil
import signal
import socket
import subprocess
import tempfile
import time
from typing import Dict, List, Optional, Set
from pubsub import pub # type: ignore[import-untyped]
from meshtastic import BROADCAST_NUM, mesh_pb2, portnums_pb2
from meshtastic.tcp_interface import TCPInterface
logger = logging.getLogger(__name__)
HW_ID_OFFSET = 16
DEFAULT_BASE_PORT = 4404
DEFAULT_RSSI = -50
DEFAULT_SNR = 10.0
BOOT_TIMEOUT = 30
CONNECT_TIMEOUT = 30
CHAIN_TOPOLOGY: Dict[int, Set[int]] = {
0: {1},
1: {0, 2},
2: {1},
}
def find_meshtasticd() -> Optional[str]:
"""Return the path to the meshtasticd binary, or None if not found."""
env_path = os.environ.get("MESHTASTICD_BIN")
if env_path and os.path.isfile(env_path) and os.access(env_path, os.X_OK):
return env_path
return shutil.which("meshtasticd")
def is_compatible_host() -> bool:
"""True when the host can run meshtasticd natively (Linux only)."""
return platform.system() == "Linux"
def _wait_for_port(port: int, timeout: int = BOOT_TIMEOUT) -> bool:
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
try:
s = socket.create_connection(("localhost", port), timeout=0.5)
s.close()
return True
except OSError:
time.sleep(0.5)
return False
class SimNode:
"""A single meshtasticd simulator instance."""
def __init__(self, node_id: int, base_port: int = DEFAULT_BASE_PORT):
self.node_id = node_id
self.hw_id = node_id + HW_ID_OFFSET
self.port = base_port + node_id
self.process: Optional[subprocess.Popen] = None
self.workdir: Optional[str] = None
self.iface: Optional[TCPInterface] = None
self._log_files: list = []
@property
def node_num(self) -> int:
"""The firmware-assigned node number (== hw_id)."""
if self.iface and self.iface.myInfo:
return self.iface.myInfo.my_node_num
return self.hw_id
def start(self, binary: str) -> None:
"""Launch the meshtasticd process in simulator mode."""
self.workdir = tempfile.mkdtemp(prefix=f"mtd_node{self.node_id}_")
vfs_dir = os.path.join(self.workdir, "vfs")
os.mkdir(vfs_dir)
# Files are closed in _kill(); keep them open for the process lifetime.
log_stdout = open( # pylint: disable=consider-using-with
os.path.join(self.workdir, "meshtasticd.log"), "wb", buffering=0
)
log_stderr = open( # pylint: disable=consider-using-with
os.path.join(self.workdir, "meshtasticd.err"), "wb", buffering=0
)
self._log_files = [log_stdout, log_stderr]
self.process = subprocess.Popen( # pylint: disable=consider-using-with
[
binary,
"-s",
"-h", str(self.hw_id),
"-p", str(self.port),
"-d", vfs_dir,
"-e",
],
stdout=log_stdout,
stderr=log_stderr,
start_new_session=True,
)
if not _wait_for_port(self.port):
self._kill()
raise RuntimeError(
f"meshtasticd node {self.node_id} did not start listening on port {self.port}"
)
def connect(self) -> None:
"""Open a TCPInterface connection to this node."""
self.iface = TCPInterface(
hostname="localhost",
portNumber=self.port,
connectNow=False,
)
self.iface.myConnect()
self.iface.connect()
def close(self) -> None:
"""Close the interface and kill the process."""
if self.iface is not None:
try:
self.iface.localNode.exitSimulator()
except Exception:
pass
try:
self.iface.close()
except Exception:
pass
self.iface = None
self._kill()
if self.workdir:
shutil.rmtree(self.workdir, ignore_errors=True)
self.workdir = None
def _kill(self) -> None:
for f in self._log_files:
try:
f.close()
except Exception:
pass
self._log_files = []
if self.process is not None:
try:
os.killpg(os.getpgid(self.process.pid), signal.SIGTERM)
except (ProcessLookupError, OSError):
pass
try:
self.process.wait(timeout=5)
except Exception:
try:
os.killpg(os.getpgid(self.process.pid), signal.SIGKILL)
except Exception:
pass
# Give OS time to release TCP port (avoid TIME_WAIT preventing
# next instance from binding the same port)
time.sleep(1.0)
self.process = None
class SimMesh:
"""Manages N meshtasticd sim instances and bridges their SIMULATOR_APP packets.
When *topology* is None every node hears every other node (full mesh).
Otherwise *topology* maps a transmitter's node index to the set of receiver
node indices that can hear it.
"""
def __init__(
self,
n_nodes: int = 1,
topology: Optional[Dict[int, Set[int]]] = None,
base_port: int = DEFAULT_BASE_PORT,
):
self.n_nodes = n_nodes
self.topology = topology
self.base_port = base_port
self.nodes: List[SimNode] = [
SimNode(i, base_port) for i in range(n_nodes)
]
self._port_to_idx: Dict[int, int] = {}
self._started = False
def start(self) -> None:
"""Launch all nodes, connect, and start the packet bridge."""
binary = find_meshtasticd()
if binary is None:
raise RuntimeError(
"meshtasticd not found. Set MESHTASTICD_BIN or install it on PATH."
)
for node in self.nodes:
node.start(binary)
for node in self.nodes:
node.connect()
self._port_to_idx[node.port] = node.node_id
pub.subscribe(self._on_sim_packet, "meshtastic.receive.simulator")
self._started = True
if self.n_nodes > 1:
self._trigger_convergence()
def _trigger_convergence(self) -> None:
"""Actively trigger NodeInfo exchange instead of waiting passively.
Sends a NODEINFO_APP packet with wantResponse from each node so the
firmware's NodeInfoModule responds with its own user info, populating
all node DBs deterministically.
"""
for node in self.nodes:
iface = node.iface
if iface is None:
continue
user = mesh_pb2.User()
user.id = f"!{node.node_num:08x}"
user.long_name = f"Node {node.node_id}"
user.short_name = f"{node.node_id:04d}"[:4]
user.hw_model = mesh_pb2.HardwareModel.PORTDUINO
try:
iface.sendData(
user,
destinationId=BROADCAST_NUM,
portNum=portnums_pb2.PortNum.NODEINFO_APP,
wantAck=False,
wantResponse=True,
)
except Exception as ex:
logger.debug("NodeInfo trigger for node %d failed: %s", node.node_id, ex)
time.sleep(5)
def stop(self) -> None:
"""Shut down all nodes and clean up."""
if not self._started:
return
try:
pub.unsubscribe(self._on_sim_packet, "meshtastic.receive.simulator")
except Exception:
pass
for node in self.nodes:
node.close()
self._started = False
def get_node(self, idx: int) -> SimNode:
"""Return the SimNode at the given index."""
return self.nodes[idx]
def get_iface(self, idx: int) -> TCPInterface:
"""Return the TCPInterface for the node at the given index."""
iface = self.nodes[idx].iface
assert iface is not None, f"node {idx} has no interface"
return iface
def wait_for_convergence(self, timeout: int = 30) -> bool:
"""Wait until every node sees all others in its node DB.
Returns True if converged, False if timed out (non-fatal the packet
bridge forwards regardless of node DB state).
"""
if self.n_nodes <= 1:
return True
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
if all(
node.iface is not None
and node.iface.nodes is not None
and len(node.iface.nodes) >= self.n_nodes
for node in self.nodes
):
return True
time.sleep(2)
logger.warning("Mesh did not fully converge within %ds", timeout)
return False
def _get_receivers(self, tx_idx: int) -> List[int]:
"""Return node indices that can hear a transmission from *tx_idx*."""
if self.topology is not None:
return sorted(self.topology.get(tx_idx, set()))
return [i for i in range(self.n_nodes) if i != tx_idx]
def _on_sim_packet(self, interface, packet) -> None:
"""Bridge callback: forward a SIMULATOR_APP packet to receiving nodes."""
tx_port = getattr(interface, "portNumber", None)
tx_idx = self._port_to_idx.get(tx_port) if tx_port else None
if tx_idx is None:
return
rx_indices = self._get_receivers(tx_idx)
if not rx_indices:
return
data = packet["decoded"]["payload"]
if hasattr(data, "SerializeToString"):
data = data.SerializeToString()
if len(data) > mesh_pb2.Constants.DATA_PAYLOAD_LEN:
logger.warning("Simulator payload too big (%d bytes), dropping", len(data))
return
mesh_packet = _build_mesh_packet(packet, data)
for rx_idx in rx_indices:
rx_iface = self.nodes[rx_idx].iface
if rx_iface is None:
continue
mesh_packet.rx_rssi = DEFAULT_RSSI
mesh_packet.rx_snr = DEFAULT_SNR
to_radio = mesh_pb2.ToRadio()
to_radio.packet.CopyFrom(mesh_packet)
try:
rx_iface._sendToRadio(to_radio)
except Exception as ex:
logger.error("Error forwarding packet to node %d: %s", rx_idx, ex)
def __enter__(self):
self.start()
return self
def __exit__(self, *exc):
self.stop()
def _build_mesh_packet(packet: dict, data: bytes) -> mesh_pb2.MeshPacket:
"""Reconstruct a MeshPacket for SIMULATOR_APP injection."""
mp = mesh_pb2.MeshPacket()
mp.decoded.payload = data
mp.decoded.portnum = portnums_pb2.PortNum.SIMULATOR_APP
mp.to = packet.get("to", BROADCAST_NUM)
setattr(mp, "from", packet.get("from", 0))
mp.id = packet.get("id", 0)
mp.want_ack = packet.get("wantAck", False)
mp.hop_limit = packet.get("hopLimit", 0)
mp.hop_start = packet.get("hopStart", 0)
mp.via_mqtt = packet.get("viaMQTT", False)
mp.relay_node = packet.get("relayNode", 0)
mp.next_hop = packet.get("nextHop", 0)
mp.channel = int(packet.get("channel", 0))
decoded = packet.get("decoded", {})
if "requestId" in decoded:
mp.decoded.request_id = decoded["requestId"]
if "wantResponse" in decoded:
mp.decoded.want_response = decoded["wantResponse"]
if "bitfield" in decoded:
mp.decoded.bitfield = decoded["bitfield"]
return mp
+374
View File
@@ -0,0 +1,374 @@
"""Shared helpers for meshtasticd-backed smoke tests.
Both ``test_smokevirt`` (single node) and ``test_smokemesh`` (multi-node
chain) use these helpers to drive the ``meshtastic`` CLI against real
``meshtasticd`` simulator instances and then verify the resulting firmware
state through the Python library's ``TCPInterface``.
Verifying through the library (rather than regex on CLI stdout) is the
core design choice: it makes tests robust against CLI wording changes
while still exercising both the CLI argparse path and the firmware I/O
path of every feature.
"""
from __future__ import annotations
import logging
import shlex
import socket
import subprocess
import sys
import time
from typing import Callable, List, Optional, Tuple
from pubsub import pub # type: ignore[import-untyped]
from meshtastic.tcp_interface import TCPInterface
logger = logging.getLogger(__name__)
# Pause between a CLI command finishing and a verification interface
# opening, to let the firmware flush its TCP bookkeeping. Keeps the
# simulator happy when many short-lived connections are happening.
PAUSE_AFTER_CLI = 0.2
# Pause after changing the LoRa region. The firmware may restart the TCP
# listener while committing the new radio config, so give it time to settle
# before the next CLI call or harness reconnect.
PAUSE_AFTER_REGION_CHANGE = 2.0
# ---------------------------------------------------------------------------
# CLI invocation
# ---------------------------------------------------------------------------
def resolve_cli() -> str:
"""Return a shell-invokable ``meshtastic`` command.
Prefers the in-tree module run through the current interpreter so
tests exercise the source we are editing, regardless of any
separately-installed ``meshtastic`` entry point on PATH.
"""
# The PATH binary may live outside the nono sandbox's allowed paths;
# ``python -m meshtastic`` is more portable and always available.
return f"{sys.executable} -m meshtastic"
def run_cli(
port: int,
*args: str,
timeout: int = 60,
retries: int = 2,
retry_delay: float = 1.0,
) -> Tuple[int, str]:
"""Run the ``meshtastic`` CLI against the sim node on *port*.
Returns ``(return_code, merged_stdout_stderr)``. ``--host
localhost:PORT`` is prefixed automatically. stderr is merged into
stdout so callers can match warning text such as "Warning: Need to
specify ..." regardless of which stream it lands on.
If the CLI fails to connect on the first attempt (which happens
transiently when a freshly-booted sim node needs a moment to settle
after the harness interface connects), retry up to *retries*
additional times after *retry_delay* seconds.
"""
cli = resolve_cli()
argv = [*_shlex_split(cli)]
argv.extend(["--host", f"localhost:{port}"])
argv.extend(args)
logger.debug("run_cli: %s", argv)
last_out = ""
for attempt in range(retries + 1):
try:
proc = subprocess.run(
argv,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
timeout=timeout,
check=False,
)
except subprocess.TimeoutExpired as ex:
last_out = ex.stdout.decode("utf-8", errors="replace") if ex.stdout else ""
if attempt < retries:
time.sleep(retry_delay)
continue
return 124, last_out
out = proc.stdout.decode("utf-8", errors="replace")
rc = proc.returncode
# Retry on transient connection-refused / timed-out errors that
# are common right after a sim node spins up.
transient = (
"Error connecting" in out
or "Timed out waiting for connection" in out
or "Connection reset by peer" in out
)
if rc != 0 and transient and attempt < retries:
logger.debug("run_cli: transient failure, retry %d/%d", attempt + 1, retries)
time.sleep(retry_delay)
last_out = out
continue
return rc, out
return rc, last_out
def _shlex_split(cmd: str) -> List[str]:
"""Split a shell string into argv, honoring quotes."""
return shlex.split(cmd)
# ---------------------------------------------------------------------------
# Fresh-connection state verification
# ---------------------------------------------------------------------------
def _wait_for_port(port: int, timeout: float = 30.0) -> None:
"""Wait until *port* accepts a TCP connection (firmware sim comes up
or comes back after a config-commit reboot)."""
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
try:
s = socket.create_connection(("localhost", port), timeout=0.5)
s.close()
return
except OSError:
time.sleep(0.2)
raise TimeoutError(
f"port {port} did not accept connections within {timeout}s"
)
def connect_iface(
port: int,
no_nodes: bool = False,
retries: int = 4,
wait_timeout: float = 30.0,
) -> TCPInterface:
"""Open a fresh ``TCPInterface`` to *port* and block on the config exchange.
Firmware config writes (``writeChannel``, ``--seturl``, ``--factory_reset``)
can briefly restart the sim's TCP listener. We wait for the port to
come up first, then retry the connect+config-exchange a few times.
"""
last_exc: Optional[Exception] = None
for attempt in range(retries + 1):
try:
_wait_for_port(port, timeout=wait_timeout)
return TCPInterface(
hostname="localhost",
portNumber=port,
connectNow=True,
noNodes=no_nodes,
)
except Exception as ex: # pylint: disable=broad-except
last_exc = ex
if attempt < retries:
logger.debug(
"connect_iface attempt %d/%d failed: %s",
attempt + 1, retries, ex,
)
time.sleep(0.5)
continue
raise
assert last_exc is not None # type guard; unreachable
raise last_exc # pragma: no cover
def verify_state(
port: int,
verifier: Callable[[TCPInterface], None],
*,
no_nodes: bool = False,
) -> None:
"""Open a fresh interface and run *verifier(iface)*, then close.
Used after a CLI mutation to verify firmware state through the
library. Always closes the interface so the next test starts clean.
"""
iface = connect_iface(port, no_nodes=no_nodes)
try:
verifier(iface)
finally:
try:
iface.close()
except Exception: # pylint: disable=broad-except
pass
time.sleep(PAUSE_AFTER_CLI)
def cli_then_verify(
port: int,
cli_args: List[str],
verifier: Optional[Callable[[TCPInterface], None]],
*,
expect_rc: Optional[int] = 0,
no_nodes: bool = False,
cli_timeout: int = 60,
) -> str:
"""Run *cli_args* against *port*, optionally asserting *expect_rc*,
then (if *verifier* is not None) open a fresh interface and run
*verifier(iface)* against the just-mutated firmware state.
Returns the CLI stdout.
"""
rc, out = run_cli(port, *cli_args, timeout=cli_timeout)
if expect_rc is not None:
assert rc == expect_rc, f"CLI rc={rc} (expected {expect_rc}): {out}"
time.sleep(PAUSE_AFTER_CLI)
if verifier is not None:
verify_state(port, verifier, no_nodes=no_nodes)
return out
# ---------------------------------------------------------------------------
# Packet collectors (used by smokemesh receive-verification tests)
# ---------------------------------------------------------------------------
RECEIVE_TIMEOUT = 15.0
class PacketCollector:
"""Collect packets received on a specific interface via pubsub.
``Listener`` (pubsub 4.x) wraps handlers with a weak reference, so
we keep a strong reference to ``handler`` on the instance to prevent
garbage collection before the publishing thread gets to call it.
"""
def __init__(self):
self.packets: List[dict] = []
self._handler: Optional[Callable] = None
def on_receive(self, packet, interface): # pylint: disable=unused-argument
"""Append a received packet to the internal list."""
self.packets.append(packet)
def wait_for(self, count: int, timeout: float = RECEIVE_TIMEOUT) -> bool:
"""Wait until *count* packets have been collected or *timeout* expires."""
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
if len(self.packets) >= count:
return True
time.sleep(0.2)
return len(self.packets) >= count
@property
def texts(self) -> List[str]:
"""Return text payloads from TEXT_MESSAGE_APP packets."""
return [
p.get("decoded", {}).get("text", "")
for p in self.packets
if p.get("decoded", {}).get("portnum") == "TEXT_MESSAGE_APP"
]
@property
def traceroutes(self) -> List[dict]:
"""Return TRACEROUTE_APP packets."""
return [
p for p in self.packets
if p.get("decoded", {}).get("portnum") == "TRACEROUTE_APP"
]
@property
def telemetries(self) -> List[dict]:
"""Return TELEMETRY_APP packets."""
return [
p for p in self.packets
if p.get("decoded", {}).get("portnum") == "TELEMETRY_APP"
]
@property
def positions(self) -> List[dict]:
"""Return POSITION_APP packets."""
return [
p for p in self.packets
if p.get("decoded", {}).get("portnum") == "POSITION_APP"
]
def reset(self) -> None:
"""Clear all collected packets."""
self.packets.clear()
def _subscribe_topic(
iface: TCPInterface, topic: str
) -> PacketCollector:
"""Internal: subscribe a fully-qualified *topic* and return a collector.
Filters by *interface* so multi-node tests can subscribe several
collectors concurrently without cross-talk.
"""
collector = PacketCollector()
def handler(packet, interface):
if interface is iface:
collector.on_receive(packet, interface)
pub.subscribe(handler, topic)
collector._handler = handler # strong ref; see PacketCollector docstring
return collector
def subscribe_texts(iface: TCPInterface) -> PacketCollector:
"""Subscribe to ``meshtastic.receive.text`` filtered to *iface*."""
return _subscribe_topic(iface, "meshtastic.receive.text")
def subscribe_traceroutes(iface: TCPInterface) -> PacketCollector:
"""Subscribe to ``meshtastic.receive.traceroute`` filtered to *iface*."""
return _subscribe_topic(iface, "meshtastic.receive.traceroute")
def subscribe_telemetries(iface: TCPInterface) -> PacketCollector:
"""Subscribe to ``meshtastic.receive.telemetry`` filtered to *iface*."""
return _subscribe_topic(iface, "meshtastic.receive.telemetry")
def subscribe_positions(iface: TCPInterface) -> PacketCollector:
"""Subscribe to ``meshtastic.receive.position`` filtered to *iface*."""
return _subscribe_topic(iface, "meshtastic.receive.position")
def set_region(port: int, region: str = "US") -> None:
"""Set the LoRa region on a sim node via the CLI.
The node briefly restarts its TCP listener while committing the radio
config; ``run_cli()`` handles the reconnect retry, and we sleep long
enough for the new region to take effect before callers continue.
"""
rc, out = run_cli(port, "--set", "lora.region", region)
if rc != 0:
raise RuntimeError(f"Failed to set lora.region={region}: {out}")
time.sleep(PAUSE_AFTER_REGION_CHANGE)
def unsubscribe_all(topic: str) -> None:
"""Drop every handler currently registered on *topic*.
Tests use this in a ``finally`` block to keep pubsub clean across
the function-scoped mesh fixtures.
"""
try:
pub.unsubAll(topic)
except Exception: # pylint: disable=broad-except
pass
__all__ = [
"PAUSE_AFTER_CLI",
"PAUSE_AFTER_REGION_CHANGE",
"PacketCollector",
"RECEIVE_TIMEOUT",
"cli_then_verify",
"connect_iface",
"resolve_cli",
"run_cli",
"set_region",
"subscribe_positions",
"subscribe_telemetries",
"subscribe_texts",
"subscribe_traceroutes",
"unsubscribe_all",
"verify_state",
]
+69
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
@@ -0,0 +1,655 @@
"""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,
mqtt_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 type = FT_IGNORE from telemetry.options."""
env = telemetry_pb2.DESCRIPTOR.message_types_by_name["EnvironmentMetrics"]
opts = _field_opts(env, "one_wire_temperature")
assert opts.type == nanopb_pb2.FT_IGNORE
@pytest.mark.unit
def test_descriptor_mqtt_service_envelope_pointer_fields():
"""ServiceEnvelope fields carry type = FT_POINTER from mqtt.options."""
envelope = mqtt_pb2.DESCRIPTOR.message_types_by_name["ServiceEnvelope"]
for name in ("packet", "channel_id", "gateway_id"):
assert _field_opts(envelope, name).type == nanopb_pb2.FT_POINTER
File diff suppressed because it is too large. Load diff
+40
View File
@@ -757,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
@@ -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.position_min_interval_secs = 30
from_radio.moduleConfig.traffic_management.rate_limit_window_secs = 60
iface._handleFromRadio(from_radio.SerializeToString())
assert iface.localNode.moduleConfig.traffic_management.position_min_interval_secs == 30
assert iface.localNode.moduleConfig.traffic_management.rate_limit_window_secs == 60
iface.close()
+413 -1
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 admin_pb2, 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"""
@@ -308,6 +347,248 @@ def test_setURL_valid_URL_but_no_settings(capsys):
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):
@@ -388,6 +669,78 @@ def test_getChannelByChannelIndex():
assert anode.getChannelByChannelIndex(9) is None
def _build_channels(highest_secondary_index: int):
"""Build an 8-slot channel table with contiguous active channels.
Slot 0 is PRIMARY. Slots 1..highest_secondary_index are SECONDARY.
Remaining slots are DISABLED.
"""
channels = []
for idx in range(8):
ch = Channel()
ch.index = idx
if idx == 0:
ch.role = Channel.Role.PRIMARY
ch.settings.name = "primary"
elif idx <= highest_secondary_index:
ch.role = Channel.Role.SECONDARY
ch.settings.name = f"ch{idx}"
else:
ch.role = Channel.Role.DISABLED
channels.append(ch)
return channels
@pytest.mark.unit
@pytest.mark.parametrize(
"highest_secondary_index,delete_index,expected_writes",
[
pytest.param(1, 1, [1], id="active-0-1-del-1"),
pytest.param(2, 1, [1, 2], id="active-0-2-del-1"),
pytest.param(3, 1, [1, 2, 3], id="active-0-3-del-1"),
pytest.param(3, 2, [2, 3], id="active-0-3-del-2"),
],
)
def test_delete_channel_writes_only_changed_suffix(
highest_secondary_index, delete_index, expected_writes
):
"""deleteChannel should only write slots whose payload changed."""
iface = MagicMock()
anode = Node(iface, "bar", noProto=True)
iface.localNode = anode
anode.channels = _build_channels(highest_secondary_index)
writes = []
def fake_write(channel_index, adminIndex=0):
writes.append((channel_index, adminIndex))
anode.writeChannel = fake_write
anode.deleteChannel(delete_index)
written_indices = [idx for idx, _ in writes]
assert written_indices == expected_writes
assert all(admin_idx == 0 for _, admin_idx in writes)
assert 0 not in written_indices
assert all(idx < 4 for idx in written_indices)
@pytest.mark.unit
def test_delete_channel_rejects_primary():
"""deleteChannel should refuse deleting PRIMARY slot 0."""
iface = MagicMock()
anode = Node(iface, "bar", noProto=True)
iface.localNode = anode
anode.channels = _build_channels(3)
with pytest.raises(SystemExit) as pytest_wrapped_e:
anode.deleteChannel(0)
assert pytest_wrapped_e.type is SystemExit
assert pytest_wrapped_e.value.code == 1
# TODO
# @pytest.mark.unit
# def test_deleteChannel_try_to_delete_primary_channel(capsys):
@@ -794,6 +1147,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.position_min_interval_secs = 30
anode.moduleConfig.traffic_management.rate_limit_window_secs = 60
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.position_min_interval_secs == 30
assert sent_admin[0].set_module_config.traffic_management.rate_limit_window_secs == 60
# TODO
# @pytest.mark.unit
# def test_writeConfig(caplog):
@@ -1550,6 +1927,41 @@ def test_setOwner_valid_names(caplog):
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():
+455
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)
+202
View File
@@ -0,0 +1,202 @@
"""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_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 == ""
+128
View File
@@ -0,0 +1,128 @@
"""Meshtastic smoke tests with multiple meshtasticd sim instances.
Uses the ``firmware_mesh`` session fixture which provides a 3-node chain
topology (A-B-C) where A hears B, B hears A and C, and C hears B.
The SIMULATOR_APP packet bridge forwards transmissions between nodes
according to this topology.
"""
import time
import pytest
from .fw_helpers import (
RECEIVE_TIMEOUT,
subscribe_texts,
subscribe_traceroutes,
unsubscribe_all,
)
@pytest.mark.smokemesh
def test_smokemesh_node_db_convergence(firmware_mesh):
"""Each node should see all 3 nodes in its node DB after convergence."""
counts = [len(n.iface.nodes) for n in firmware_mesh.nodes if n.iface]
if any(c < 3 for c in counts):
pytest.skip(f"Mesh did not converge (counts={counts})")
for i, node in enumerate(firmware_mesh.nodes):
iface = node.iface
assert iface is not None
assert len(iface.nodes) >= 3, f"node {i} only sees {len(iface.nodes)} nodes"
@pytest.mark.smokemesh
def test_smokemesh_broadcast_text(firmware_mesh):
"""A broadcast from node A should arrive on node B."""
collector = subscribe_texts(firmware_mesh.get_iface(1))
try:
firmware_mesh.get_iface(0).sendText("hello mesh", wantAck=False)
assert collector.wait_for(1)
assert "hello mesh" in collector.texts
finally:
unsubscribe_all("meshtastic.receive.text")
@pytest.mark.smokemesh
def test_smokemesh_dm(firmware_mesh):
"""A DM from node A to node B should arrive on B."""
dest = firmware_mesh.get_node(1).node_num
collector = subscribe_texts(firmware_mesh.get_iface(1))
try:
firmware_mesh.get_iface(0).sendText(
"hey B", destinationId=dest, wantAck=False
)
assert collector.wait_for(1)
assert "hey B" in collector.texts
finally:
unsubscribe_all("meshtastic.receive.text")
@pytest.mark.smokemesh
def test_smokemesh_dm_across_relay(firmware_mesh):
"""A DM from node A to node C must relay through B (chain topology)."""
dest = firmware_mesh.get_node(2).node_num
collector = subscribe_texts(firmware_mesh.get_iface(2))
try:
firmware_mesh.get_iface(0).sendText(
"relay test", destinationId=dest, wantAck=False
)
assert collector.wait_for(1), "node C did not receive the DM within timeout"
assert "relay test" in collector.texts
finally:
unsubscribe_all("meshtastic.receive.text")
@pytest.mark.smokemesh
def test_smokemesh_hop_limit_prevents_relay(firmware_mesh):
"""A broadcast with hopLimit=0 from A reaches B but B does not relay to C."""
col_b = subscribe_texts(firmware_mesh.get_iface(1))
col_c = subscribe_texts(firmware_mesh.get_iface(2))
try:
firmware_mesh.get_iface(0).sendText(
"hop0", wantAck=False, hopLimit=0
)
assert col_b.wait_for(1), "B should receive A's broadcast"
assert "hop0" in col_b.texts, "B got wrong text"
time.sleep(RECEIVE_TIMEOUT)
assert "hop0" not in col_c.texts, (
"C should NOT receive — B must not relay hopLimit=0"
)
finally:
unsubscribe_all("meshtastic.receive.text")
@pytest.mark.smokemesh
def test_smokemesh_show_nodes(firmware_mesh):
"""showNodes should report the other nodes in the mesh."""
for i in range(3):
iface = firmware_mesh.get_iface(i)
iface.showNodes()
@pytest.mark.smokemesh
def test_smokemesh_traceroute_across_relay(firmware_mesh):
"""Traceroute from A to C should show route via B in both directions."""
col_a = subscribe_traceroutes(firmware_mesh.get_iface(0))
col_c = subscribe_traceroutes(firmware_mesh.get_iface(2))
try:
src_a = firmware_mesh.get_node(0).node_num
dest_c = firmware_mesh.get_node(2).node_num
node_b = firmware_mesh.get_node(1).node_num
firmware_mesh.get_iface(0).sendTraceRoute(dest=dest_c, hopLimit=3)
time.sleep(2)
assert len(col_a.traceroutes) >= 1, "A did not receive traceroute response"
a_resp = col_a.traceroutes[0]
assert a_resp["from"] == dest_c, "response source should be C"
route = a_resp["decoded"]["traceroute"]
assert route.get("route") == [node_b], "forward route should be A→B→C"
assert route.get("routeBack") == [node_b], "return route should be C→B→A"
assert len(col_c.traceroutes) >= 1, "C did not receive traceroute request"
c_req = col_c.traceroutes[0]
assert c_req["from"] == src_a, "request source should be A"
finally:
unsubscribe_all("meshtastic.receive.traceroute")
File diff suppressed because it is too large. Load diff
+109 -1
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
+115 -1
View File
@@ -1,7 +1,8 @@
"""Meshtastic unit tests for tcp_interface.py"""
import re
from unittest.mock import patch
import socket
from unittest.mock import MagicMock, patch
import pytest
@@ -54,3 +55,116 @@ def test_TCPInterface_without_connecting():
with patch("socket.socket"):
iface = TCPInterface(hostname="localhost", noProto=True, connectNow=False)
assert iface.socket is None
@pytest.mark.unit
def test_TCPInterface_close_shutdowns_socket_before_super_close():
"""Close should unblock socket reads before waiting on StreamInterface.close()."""
iface = TCPInterface(hostname="localhost", noProto=True, connectNow=False)
sock = MagicMock()
iface.socket = sock
call_order = []
with patch.object(TCPInterface, "_socket_shutdown", autospec=True) as mock_shutdown:
with patch(
"meshtastic.stream_interface.StreamInterface.close", autospec=True
) as mock_super_close:
mock_shutdown.side_effect = lambda _self: call_order.append("shutdown")
mock_super_close.side_effect = lambda _self: call_order.append("super_close")
iface.close()
assert call_order == ["shutdown", "super_close"]
sock.close.assert_called_once()
assert iface.socket is None
@pytest.mark.unit
def test_TCPInterface_close_half_closes_before_shutdown():
"""Close should send FIN and let the device drain before forcing the link down.
Going straight to shutdown(SHUT_RDWR)/close() with data still unread makes the
stack send RST, and Winsock then discards data the device had received but not
yet read, silently losing writes such as `--set`.
"""
iface = TCPInterface(hostname="localhost", noProto=True, connectNow=False)
sock = MagicMock()
iface.socket = sock
call_order = []
with patch.object(TCPInterface, "_socket_shutdown", autospec=True) as mock_shutdown:
with patch.object(
TCPInterface, "_wait_for_reader_exit", autospec=True
) as mock_wait:
with patch(
"meshtastic.stream_interface.StreamInterface.close", autospec=True
) as mock_super_close:
sock.shutdown.side_effect = lambda how: call_order.append(f"shutdown_{how}")
mock_wait.side_effect = lambda _self, _t: call_order.append("wait")
mock_shutdown.side_effect = lambda _self: call_order.append("full_shutdown")
mock_super_close.side_effect = lambda _self: call_order.append("super_close")
iface.close()
assert call_order == [
f"shutdown_{socket.SHUT_WR}",
"wait",
"full_shutdown",
"super_close",
]
@pytest.mark.unit
def test_TCPInterface_close_survives_half_close_failure():
"""A peer that already vanished must not break close()."""
iface = TCPInterface(hostname="localhost", noProto=True, connectNow=False)
sock = MagicMock()
sock.shutdown.side_effect = OSError("already gone")
iface.socket = sock
with patch("meshtastic.stream_interface.StreamInterface.close", autospec=True):
iface.close() # must not raise
sock.close.assert_called_once()
assert iface.socket is None
@pytest.mark.unit
def test_TCPInterface_reconnect():
"""Test that _reconnect correctly reconnects"""
with patch("socket.socket") as mock_socket:
with patch("time.sleep"):
iface = TCPInterface(hostname="localhost", noProto=True)
old_socket = iface.socket
assert old_socket is not None
iface._reconnect()
assert old_socket.close.called
# We expect socket class to be instantiated at least twice (init + reconnect)
assert mock_socket.call_count >= 2
@pytest.mark.unit
def test_TCPInterface_writeBytes_reconnects():
"""Test that _writeBytes reconnects and re-raises on OSError."""
with patch("socket.socket"):
iface = TCPInterface(hostname="localhost", noProto=True)
iface.socket.sendall.side_effect = OSError("Broken pipe")
with patch.object(iface, "_reconnect") as mock_reconnect:
with pytest.raises(OSError, match="Broken pipe"):
iface._writeBytes(b"some data")
mock_reconnect.assert_called_once()
@pytest.mark.unit
def test_TCPInterface_readBytes_reconnects():
"""Test that _readBytes calls _reconnect on empty bytes"""
iface = TCPInterface(hostname="localhost", noProto=True, connectNow=False)
iface.socket = MagicMock()
iface.socket.recv.return_value = b""
with patch.object(iface, "_reconnect") as mock_reconnect:
iface._readBytes(10)
mock_reconnect.assert_called_once()
+268 -13
View File
@@ -3,13 +3,15 @@
import json
import logging
import re
import time
from types import SimpleNamespace
from unittest.mock import patch
import pytest
from hypothesis import given, strategies as st
from meshtastic.supported_device import SupportedDevice
from meshtastic.protobuf import mesh_pb2
from meshtastic.protobuf import mesh_pb2, config_pb2
from meshtastic.util import (
DEFAULT_KEY,
Timeout,
@@ -20,6 +22,8 @@ from meshtastic.util import (
convert_mac_addr,
eliminate_duplicate_port,
findPorts,
flags_from_list,
flags_to_list,
fixme,
fromPSK,
fromStr,
@@ -37,6 +41,7 @@ from meshtastic.util import (
stripnl,
support_info,
message_to_json,
to_node_num,
Acknowledgment
)
@@ -234,20 +239,111 @@ def test_remove_keys_from_dict_nested():
assert remove_keys_from_dict(keys, adict) == exp
@pytest.mark.unitslow
def test_Timeout_not_found():
"""Test Timeout()"""
to = Timeout(0.2)
attrs = "foo"
to.waitForSet("bar", attrs)
@pytest.mark.unit
def test_Timeout_waitForSet_found():
"""waitForSet returns True when all required attrs are truthy on target."""
to = Timeout(0.01)
target = SimpleNamespace(foo=1, bar="ok")
assert to.waitForSet(target, ("foo", "bar")) is True
@pytest.mark.unitslow
def test_Timeout_found():
"""Test Timeout()"""
to = Timeout(0.2)
attrs = ()
to.waitForSet("bar", attrs)
@pytest.mark.unit
@patch("meshtastic.util.time.sleep")
def test_Timeout_waitForSet_not_found(mock_sleep): # pylint: disable=unused-argument
"""waitForSet returns False when attrs remain falsy until timeout."""
to = Timeout(0.01)
target = SimpleNamespace(foo=None, bar=0)
assert to.waitForSet(target, ("foo", "bar")) is False
@pytest.mark.unit
def test_Timeout_waitForSet_empty_attrs():
"""waitForSet returns True immediately when attrs is empty (vacuous truth)."""
to = Timeout(0.01)
assert to.waitForSet(object(), ()) is True
@pytest.mark.unit
def test_Timeout_reset():
"""reset() sets expireTime to now + expireTimeout."""
to = Timeout(maxSecs=5)
before = time.time()
to.reset()
assert to.expireTime == pytest.approx(before + 5, abs=0.1)
@pytest.mark.unit
def test_Timeout_reset_custom():
"""reset(expireTimeout) overrides the stored expireTimeout."""
to = Timeout(maxSecs=5)
before = time.time()
to.reset(expireTimeout=10)
assert to.expireTime == pytest.approx(before + 10, abs=0.1)
@pytest.mark.unit
def test_Timeout_waitForAckNak_found():
"""waitForAckNak returns True when any acknowledgment attr is set, and clears it."""
to = Timeout(0.01)
ack = Acknowledgment()
ack.receivedAck = True
assert to.waitForAckNak(ack) is True
assert ack.receivedAck is False # cleared after detection
@pytest.mark.unit
@patch("meshtastic.util.time.sleep")
def test_Timeout_waitForAckNak_not_found(mock_sleep): # pylint: disable=unused-argument
"""waitForAckNak returns False when no acknowledgment attr is set."""
to = Timeout(0.01)
ack = Acknowledgment()
assert to.waitForAckNak(ack) is False
@pytest.mark.unit
def test_Timeout_waitForTraceRoute_found():
"""waitForTraceRoute returns True on receivedTraceRoute, and clears it."""
to = Timeout(0.01)
ack = Acknowledgment()
ack.receivedTraceRoute = True
assert to.waitForTraceRoute(3.0, ack) is True
assert ack.receivedTraceRoute is False
@pytest.mark.unit
@patch("meshtastic.util.time.sleep")
def test_Timeout_waitForTraceRoute_not_found(mock_sleep): # pylint: disable=unused-argument
"""waitForTraceRoute returns False on timeout."""
to = Timeout(0.01)
ack = Acknowledgment()
assert to.waitForTraceRoute(3.0, ack) is False
@pytest.mark.unit
def test_Timeout_waitForTelemetry_found():
"""waitForTelemetry returns True when receivedTelemetry is set."""
to = Timeout(0.01)
ack = Acknowledgment()
ack.receivedTelemetry = True
assert to.waitForTelemetry(ack) is True
@pytest.mark.unit
def test_Timeout_waitForPosition_found():
"""waitForPosition returns True when receivedPosition is set."""
to = Timeout(0.01)
ack = Acknowledgment()
ack.receivedPosition = True
assert to.waitForPosition(ack) is True
@pytest.mark.unit
def test_Timeout_waitForWaypoint_found():
"""waitForWaypoint returns True when receivedWaypoint is set."""
to = Timeout(0.01)
ack = Acknowledgment()
ack.receivedWaypoint = True
assert to.waitForWaypoint(ack) is True
@pytest.mark.unitslow
@@ -715,3 +811,162 @@ def test_generate_channel_hash_fuzz_aes256(channel_name, key_bytes):
"Test generate_channel_hash with fuzzed channel names and 256-bit keys, ensuring it produces single-byte values"
hashed = generate_channel_hash(channel_name, key_bytes)
assert 0 <= hashed <= 0xFF
@pytest.mark.unit
@pytest.mark.parametrize("input_val,expected", [
# int passthrough
(0, 0),
(1, 1),
(6, 6),
(502009325, 502009325),
(2198819370, 2198819370),
(0xFFFFFFFF, 0xFFFFFFFF),
# !hex format (always treated as hex)
("!00000000", 0x00000000),
("!00000001", 0x00000001),
("!00000010", 0x00000010),
("!000000ff", 0x000000FF),
("!830f522a", 0x830F522A),
("!1dec0ded", 0x1DEC0DED),
("!ffffffff", 0xFFFFFFFF),
("!FFFFFFFF", 0xFFFFFFFF),
# 0xhex format
("0x00000000", 0x00000000),
("0x00000010", 0x00000010),
("0x830f522a", 0x830F522A),
("0x1dec0ded", 0x1DEC0DED),
("0xFFFFFFFF", 0xFFFFFFFF),
# Unprefixed hex string (falls back to hex when decimal fails)
("830f522a", 0x830F522A),
("1dec0ded", 0x1DEC0DED),
# Decimal string
("42", 42),
("12345678", 12345678),
("0", 0),
("1", 1),
# With whitespace
(" !830f522a ", 2198819370),
(" !00000010 ", 16),
(" 0x830f522a ", 2198819370),
])
def test_to_node_num(input_val, expected):
"""Test to_node_num with various valid inputs"""
assert to_node_num(input_val) == expected
@pytest.mark.unit
@pytest.mark.parametrize("input_val", [
"",
"!",
"!!",
"!0x10",
"!xyz",
])
def test_to_node_num_invalid(input_val):
"""Test to_node_num raises ValueError for invalid inputs"""
with pytest.raises(ValueError):
to_node_num(input_val)
@pytest.mark.unit
@given(st.integers(min_value=0, max_value=2**32 - 1))
def test_to_node_num_hypothesis_roundtrip(n):
"""Property: all supported input formats roundtrip for any valid node number"""
assert to_node_num(n) == n
assert to_node_num(f"!{n:08x}") == n
assert to_node_num(f"0x{n:x}") == n
assert to_node_num(str(n)) == n
_EXCLUDED_MODULES = mesh_pb2.ExcludedModules
_POSITION_FLAGS = config_pb2.Config.PositionConfig.PositionFlags
_NETWORK_PROTOCOLS = config_pb2.Config.NetworkConfig.ProtocolFlags
@pytest.mark.unit
@pytest.mark.parametrize("flag_type,flags,expected", [
(_EXCLUDED_MODULES, 0, []),
(_EXCLUDED_MODULES, 1, ["MQTT_CONFIG"]),
(_EXCLUDED_MODULES, 3, ["MQTT_CONFIG", "SERIAL_CONFIG"]),
(_EXCLUDED_MODULES, 0x7FFF, [
"MQTT_CONFIG", "SERIAL_CONFIG", "EXTNOTIF_CONFIG", "STOREFORWARD_CONFIG",
"RANGETEST_CONFIG", "TELEMETRY_CONFIG", "CANNEDMSG_CONFIG", "AUDIO_CONFIG",
"REMOTEHARDWARE_CONFIG", "NEIGHBORINFO_CONFIG", "AMBIENTLIGHTING_CONFIG",
"DETECTIONSENSOR_CONFIG", "PAXCOUNTER_CONFIG", "BLUETOOTH_CONFIG",
"NETWORK_CONFIG",
]),
(_EXCLUDED_MODULES, 0x8000, ["UNKNOWN_ADDITIONAL_FLAGS(32768)"]),
(_EXCLUDED_MODULES, 0x8001, ["MQTT_CONFIG", "UNKNOWN_ADDITIONAL_FLAGS(32768)"]),
(_POSITION_FLAGS, 0, []),
(_POSITION_FLAGS, 0x09, ["ALTITUDE", "DOP"]),
(_POSITION_FLAGS, 0x1FF, [
"ALTITUDE", "ALTITUDE_MSL", "GEOIDAL_SEPARATION", "DOP", "HVDOP",
"SATINVIEW", "SEQ_NO", "TIMESTAMP", "HEADING",
]),
])
def test_flags_to_list(flag_type, flags, expected):
"""Test flags_to_list decodes set bits in enum order and reports unknown remainders."""
assert flags_to_list(flag_type, flags) == expected
@pytest.mark.unit
@given(st.integers(min_value=0, max_value=0xFFFFF))
def test_flags_to_list_conservation(flags):
"""Property: flags_to_list partitions `flags` into known names plus an exact unknown remainder.
Every known bit that is set must appear as a name, and the leftover reported in
UNKNOWN_ADDITIONAL_FLAGS(...) must together with the named bits reconstruct the input.
"""
for flag_type in (_EXCLUDED_MODULES, _POSITION_FLAGS):
known_union = 0
for key in flag_type.keys():
value = flag_type.Value(key)
if key != "EXCLUDED_NONE" and value:
known_union |= value
result = flags_to_list(flag_type, flags)
accounted = 0
leftover = 0
for name in result:
if name.startswith("UNKNOWN_ADDITIONAL_FLAGS("):
leftover = int(name[len("UNKNOWN_ADDITIONAL_FLAGS("):-1])
else:
accounted |= flag_type.Value(name)
assert accounted == (flags & known_union)
assert (accounted | leftover) == flags
@pytest.mark.unit
@pytest.mark.parametrize("flag_type, flags, expected", [
(_NETWORK_PROTOCOLS, ["UDP_BROADCAST"], 1),
(_NETWORK_PROTOCOLS, ["NO_BROADCAST"], 0),
(_NETWORK_PROTOCOLS, [], 0),
(_POSITION_FLAGS, ["ALTITUDE"], 1),
(_POSITION_FLAGS, ["ALTITUDE", "SPEED"], 513),
(_POSITION_FLAGS, ["ALTITUDE", " SPEED "], 513),
])
def test_flags_from_list(flag_type, flags, expected):
"""Test flags_from_list combines named flags into the expected bitmask."""
assert flags_from_list(flag_type, flags) == expected
@pytest.mark.unit
def test_flags_from_list_unknown_flag():
"""Test flags_from_list raises ValueError for an unknown flag name."""
with pytest.raises(ValueError, match="Unknown flag 'TCP'"):
flags_from_list(_NETWORK_PROTOCOLS, ["UDP_BROADCAST", "TCP"])
@pytest.mark.unit
@given(st.lists(st.sampled_from(list(_POSITION_FLAGS.keys())), unique=True))
def test_flags_from_list_roundtrip(flags):
"""Property: flags_from_list and flags_to_list are inverses for known position flags."""
combined = flags_from_list(_POSITION_FLAGS, flags)
decoded = flags_to_list(_POSITION_FLAGS, combined)
# flags_to_list drops zero-value flags and may report unknown remainders,
# but for combinations of known non-zero flags it should return the same set of names.
nonzero_flags = {f for f in flags if _POSITION_FLAGS.Value(f)}
assert set(decoded) == nonzero_flags
+41 -1
View File
@@ -728,10 +728,50 @@ def to_node_num(node_id: Union[int, str]) -> int:
return node_id
s = str(node_id).strip()
if s.startswith("!"):
s = s[1:]
s = "0x" + s[1:]
if s.lower().startswith("0x"):
return int(s, 16)
try:
return int(s, 10)
except ValueError:
return int(s, 16)
def flags_to_list(flag_type, flags: int) -> List[str]:
"""Given a flag_type that's a protobuf EnumTypeWrapper, and a flag int, give a list of flags enabled.
Zero-valued members (e.g. EXCLUDED_NONE, UNSET, NO_BROADCAST) never appear in the
result: they hold no bit, so `flags & value` is always False for them, and a flags
value of 0 therefore decodes to an empty list rather than a named "no flags" entry.
Any leftover bits not corresponding to a known member are reported via
`UNKNOWN_ADDITIONAL_FLAGS(<leftover>)`.
"""
ret = []
for key in flag_type.keys():
if flags & flag_type.Value(key):
ret.append(key)
flags = flags - flag_type.Value(key)
if flags > 0:
ret.append(f"UNKNOWN_ADDITIONAL_FLAGS({flags})")
return ret
def flags_from_list(flag_type, flags: List[str]) -> int:
"""Given a flag_type that's a protobuf EnumTypeWrapper, and a list of flag names, return the combined bitmask.
Zero-valued members (e.g. EXCLUDED_NONE, UNSET, NO_BROADCAST) are accepted but are
no-ops: they OR in 0 and thus set nothing. A list consisting solely of such a member
(or an empty list) yields 0, which round-trips back through flags_to_list as an
empty list rather than the original member name -- see flags_to_list's docstring.
"""
result = 0
valid_names = list(flag_type.keys())
for flag_name in flags:
flag_name = flag_name.strip()
if not flag_name:
continue
if flag_name not in valid_names:
raise ValueError(
f"Unknown flag '{flag_name}'. Valid choices: {', '.join(sorted(valid_names))}"
)
result |= flag_type.Value(flag_name)
return result
Generated
+421 -177
View File
@@ -608,7 +608,7 @@ files = [
{file = "click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc"},
{file = "click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4"},
]
markers = {main = "extra == \"analysis\" and python_version >= \"3.11\"", powermon = "python_version >= \"3.11\""}
markers = {main = "python_version >= \"3.11\" and extra == \"analysis\"", powermon = "python_version >= \"3.11\""}
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
@@ -1373,83 +1373,158 @@ dotenv = ["python-dotenv"]
[[package]]
name = "fonttools"
version = "4.60.1"
version = "4.60.2"
description = "Tools to manipulate font files"
optional = false
python-versions = ">=3.9"
groups = ["analysis"]
markers = "python_version < \"3.11\""
files = [
{file = "fonttools-4.60.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9a52f254ce051e196b8fe2af4634c2d2f02c981756c6464dc192f1b6050b4e28"},
{file = "fonttools-4.60.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c7420a2696a44650120cdd269a5d2e56a477e2bfa9d95e86229059beb1c19e15"},
{file = "fonttools-4.60.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee0c0b3b35b34f782afc673d503167157094a16f442ace7c6c5e0ca80b08f50c"},
{file = "fonttools-4.60.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:282dafa55f9659e8999110bd8ed422ebe1c8aecd0dc396550b038e6c9a08b8ea"},
{file = "fonttools-4.60.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4ba4bd646e86de16160f0fb72e31c3b9b7d0721c3e5b26b9fa2fc931dfdb2652"},
{file = "fonttools-4.60.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0b0835ed15dd5b40d726bb61c846a688f5b4ce2208ec68779bc81860adb5851a"},
{file = "fonttools-4.60.1-cp310-cp310-win32.whl", hash = "sha256:1525796c3ffe27bb6268ed2a1bb0dcf214d561dfaf04728abf01489eb5339dce"},
{file = "fonttools-4.60.1-cp310-cp310-win_amd64.whl", hash = "sha256:268ecda8ca6cb5c4f044b1fb9b3b376e8cd1b361cef275082429dc4174907038"},
{file = "fonttools-4.60.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7b4c32e232a71f63a5d00259ca3d88345ce2a43295bb049d21061f338124246f"},
{file = "fonttools-4.60.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3630e86c484263eaac71d117085d509cbcf7b18f677906824e4bace598fb70d2"},
{file = "fonttools-4.60.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5c1015318e4fec75dd4943ad5f6a206d9727adf97410d58b7e32ab644a807914"},
{file = "fonttools-4.60.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e6c58beb17380f7c2ea181ea11e7db8c0ceb474c9dd45f48e71e2cb577d146a1"},
{file = "fonttools-4.60.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec3681a0cb34c255d76dd9d865a55f260164adb9fa02628415cdc2d43ee2c05d"},
{file = "fonttools-4.60.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f4b5c37a5f40e4d733d3bbaaef082149bee5a5ea3156a785ff64d949bd1353fa"},
{file = "fonttools-4.60.1-cp311-cp311-win32.whl", hash = "sha256:398447f3d8c0c786cbf1209711e79080a40761eb44b27cdafffb48f52bcec258"},
{file = "fonttools-4.60.1-cp311-cp311-win_amd64.whl", hash = "sha256:d066ea419f719ed87bc2c99a4a4bfd77c2e5949cb724588b9dd58f3fd90b92bf"},
{file = "fonttools-4.60.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7b0c6d57ab00dae9529f3faf187f2254ea0aa1e04215cf2f1a8ec277c96661bc"},
{file = "fonttools-4.60.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:839565cbf14645952d933853e8ade66a463684ed6ed6c9345d0faf1f0e868877"},
{file = "fonttools-4.60.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8177ec9676ea6e1793c8a084a90b65a9f778771998eb919d05db6d4b1c0b114c"},
{file = "fonttools-4.60.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:996a4d1834524adbb423385d5a629b868ef9d774670856c63c9a0408a3063401"},
{file = "fonttools-4.60.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a46b2f450bc79e06ef3b6394f0c68660529ed51692606ad7f953fc2e448bc903"},
{file = "fonttools-4.60.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6ec722ee589e89a89f5b7574f5c45604030aa6ae24cb2c751e2707193b466fed"},
{file = "fonttools-4.60.1-cp312-cp312-win32.whl", hash = "sha256:b2cf105cee600d2de04ca3cfa1f74f1127f8455b71dbad02b9da6ec266e116d6"},
{file = "fonttools-4.60.1-cp312-cp312-win_amd64.whl", hash = "sha256:992775c9fbe2cf794786fa0ffca7f09f564ba3499b8fe9f2f80bd7197db60383"},
{file = "fonttools-4.60.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6f68576bb4bbf6060c7ab047b1574a1ebe5c50a17de62830079967b211059ebb"},
{file = "fonttools-4.60.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:eedacb5c5d22b7097482fa834bda0dafa3d914a4e829ec83cdea2a01f8c813c4"},
{file = "fonttools-4.60.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b33a7884fabd72bdf5f910d0cf46be50dce86a0362a65cfc746a4168c67eb96c"},
{file = "fonttools-4.60.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2409d5fb7b55fd70f715e6d34e7a6e4f7511b8ad29a49d6df225ee76da76dd77"},
{file = "fonttools-4.60.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c8651e0d4b3bdeda6602b85fdc2abbefc1b41e573ecb37b6779c4ca50753a199"},
{file = "fonttools-4.60.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:145daa14bf24824b677b9357c5e44fd8895c2a8f53596e1b9ea3496081dc692c"},
{file = "fonttools-4.60.1-cp313-cp313-win32.whl", hash = "sha256:2299df884c11162617a66b7c316957d74a18e3758c0274762d2cc87df7bc0272"},
{file = "fonttools-4.60.1-cp313-cp313-win_amd64.whl", hash = "sha256:a3db56f153bd4c5c2b619ab02c5db5192e222150ce5a1bc10f16164714bc39ac"},
{file = "fonttools-4.60.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:a884aef09d45ba1206712c7dbda5829562d3fea7726935d3289d343232ecb0d3"},
{file = "fonttools-4.60.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8a44788d9d91df72d1a5eac49b31aeb887a5f4aab761b4cffc4196c74907ea85"},
{file = "fonttools-4.60.1-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e852d9dda9f93ad3651ae1e3bb770eac544ec93c3807888798eccddf84596537"},
{file = "fonttools-4.60.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:154cb6ee417e417bf5f7c42fe25858c9140c26f647c7347c06f0cc2d47eff003"},
{file = "fonttools-4.60.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5664fd1a9ea7f244487ac8f10340c4e37664675e8667d6fee420766e0fb3cf08"},
{file = "fonttools-4.60.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:583b7f8e3c49486e4d489ad1deacfb8d5be54a8ef34d6df824f6a171f8511d99"},
{file = "fonttools-4.60.1-cp314-cp314-win32.whl", hash = "sha256:66929e2ea2810c6533a5184f938502cfdaea4bc3efb7130d8cc02e1c1b4108d6"},
{file = "fonttools-4.60.1-cp314-cp314-win_amd64.whl", hash = "sha256:f3d5be054c461d6a2268831f04091dc82753176f6ea06dc6047a5e168265a987"},
{file = "fonttools-4.60.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:b6379e7546ba4ae4b18f8ae2b9bc5960936007a1c0e30b342f662577e8bc3299"},
{file = "fonttools-4.60.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9d0ced62b59e0430b3690dbc5373df1c2aa7585e9a8ce38eff87f0fd993c5b01"},
{file = "fonttools-4.60.1-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:875cb7764708b3132637f6c5fb385b16eeba0f7ac9fa45a69d35e09b47045801"},
{file = "fonttools-4.60.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a184b2ea57b13680ab6d5fbde99ccef152c95c06746cb7718c583abd8f945ccc"},
{file = "fonttools-4.60.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:026290e4ec76583881763fac284aca67365e0be9f13a7fb137257096114cb3bc"},
{file = "fonttools-4.60.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f0e8817c7d1a0c2eedebf57ef9a9896f3ea23324769a9a2061a80fe8852705ed"},
{file = "fonttools-4.60.1-cp314-cp314t-win32.whl", hash = "sha256:1410155d0e764a4615774e5c2c6fc516259fe3eca5882f034eb9bfdbee056259"},
{file = "fonttools-4.60.1-cp314-cp314t-win_amd64.whl", hash = "sha256:022beaea4b73a70295b688f817ddc24ed3e3418b5036ffcd5658141184ef0d0c"},
{file = "fonttools-4.60.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:122e1a8ada290423c493491d002f622b1992b1ab0b488c68e31c413390dc7eb2"},
{file = "fonttools-4.60.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a140761c4ff63d0cb9256ac752f230460ee225ccef4ad8f68affc723c88e2036"},
{file = "fonttools-4.60.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0eae96373e4b7c9e45d099d7a523444e3554360927225c1cdae221a58a45b856"},
{file = "fonttools-4.60.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:596ecaca36367027d525b3b426d8a8208169d09edcf8c7506aceb3a38bfb55c7"},
{file = "fonttools-4.60.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2ee06fc57512144d8b0445194c2da9f190f61ad51e230f14836286470c99f854"},
{file = "fonttools-4.60.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b42d86938e8dda1cd9a1a87a6d82f1818eaf933348429653559a458d027446da"},
{file = "fonttools-4.60.1-cp39-cp39-win32.whl", hash = "sha256:8b4eb332f9501cb1cd3d4d099374a1e1306783ff95489a1026bde9eb02ccc34a"},
{file = "fonttools-4.60.1-cp39-cp39-win_amd64.whl", hash = "sha256:7473a8ed9ed09aeaa191301244a5a9dbe46fe0bf54f9d6cd21d83044c3321217"},
{file = "fonttools-4.60.1-py3-none-any.whl", hash = "sha256:906306ac7afe2156fcf0042173d6ebbb05416af70f6b370967b47f8f00103bbb"},
{file = "fonttools-4.60.1.tar.gz", hash = "sha256:ef00af0439ebfee806b25f24c8f92109157ff3fac5731dc7867957812e87b8d9"},
{file = "fonttools-4.60.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4e36fadcf7e8ca6e34d490eef86ed638d6fd9c55d2f514b05687622cfc4a7050"},
{file = "fonttools-4.60.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6e500fc9c04bee749ceabfc20cb4903f6981c2139050d85720ea7ada61b75d5c"},
{file = "fonttools-4.60.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22efea5e784e1d1cd8d7b856c198e360a979383ebc6dea4604743b56da1cbc34"},
{file = "fonttools-4.60.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:677aa92d84d335e4d301d8ba04afca6f575316bc647b6782cb0921943fcb6343"},
{file = "fonttools-4.60.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:edd49d3defbf35476e78b61ff737ff5efea811acff68d44233a95a5a48252334"},
{file = "fonttools-4.60.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:126839492b69cecc5baf2bddcde60caab2ffafd867bbae2a88463fce6078ca3a"},
{file = "fonttools-4.60.2-cp310-cp310-win32.whl", hash = "sha256:ffcab6f5537136046ca902ed2491ab081ba271b07591b916289b7c27ff845f96"},
{file = "fonttools-4.60.2-cp310-cp310-win_amd64.whl", hash = "sha256:9c68b287c7ffcd29dd83b5f961004b2a54a862a88825d52ea219c6220309ba45"},
{file = "fonttools-4.60.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a2aed0a7931401b3875265717a24c726f87ecfedbb7b3426c2ca4d2812e281ae"},
{file = "fonttools-4.60.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dea6868e9d2b816c9076cfea77754686f3c19149873bdbc5acde437631c15df1"},
{file = "fonttools-4.60.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2fa27f34950aa1fe0f0b1abe25eed04770a3b3b34ad94e5ace82cc341589678a"},
{file = "fonttools-4.60.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:13a53d479d187b09bfaa4a35ffcbc334fc494ff355f0a587386099cb66674f1e"},
{file = "fonttools-4.60.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fac5e921d3bd0ca3bb8517dced2784f0742bc8ca28579a68b139f04ea323a779"},
{file = "fonttools-4.60.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:648f4f9186fd7f1f3cd57dbf00d67a583720d5011feca67a5e88b3a491952cfb"},
{file = "fonttools-4.60.2-cp311-cp311-win32.whl", hash = "sha256:3274e15fad871bead5453d5ce02658f6d0c7bc7e7021e2a5b8b04e2f9e40da1a"},
{file = "fonttools-4.60.2-cp311-cp311-win_amd64.whl", hash = "sha256:91d058d5a483a1525b367803abb69de0923fbd45e1f82ebd000f5c8aa65bc78e"},
{file = "fonttools-4.60.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e0164b7609d2b5c5dd4e044b8085b7bd7ca7363ef8c269a4ab5b5d4885a426b2"},
{file = "fonttools-4.60.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1dd3d9574fc595c1e97faccae0f264dc88784ddf7fbf54c939528378bacc0033"},
{file = "fonttools-4.60.2-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:98d0719f1b11c2817307d2da2e94296a3b2a3503f8d6252a101dca3ee663b917"},
{file = "fonttools-4.60.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9d3ea26957dd07209f207b4fff64c702efe5496de153a54d3b91007ec28904dd"},
{file = "fonttools-4.60.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ee301273b0850f3a515299f212898f37421f42ff9adfc341702582ca5073c13"},
{file = "fonttools-4.60.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c6eb4694cc3b9c03b7c01d65a9cf35b577f21aa6abdbeeb08d3114b842a58153"},
{file = "fonttools-4.60.2-cp312-cp312-win32.whl", hash = "sha256:57f07b616c69c244cc1a5a51072eeef07dddda5ebef9ca5c6e9cf6d59ae65b70"},
{file = "fonttools-4.60.2-cp312-cp312-win_amd64.whl", hash = "sha256:310035802392f1fe5a7cf43d76f6ff4a24c919e4c72c0352e7b8176e2584b8a0"},
{file = "fonttools-4.60.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2bb5fd231e56ccd7403212636dcccffc96c5ae0d6f9e4721fa0a32cb2e3ca432"},
{file = "fonttools-4.60.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:536b5fab7b6fec78ccf59b5c59489189d9d0a8b0d3a77ed1858be59afb096696"},
{file = "fonttools-4.60.2-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6b9288fc38252ac86a9570f19313ecbc9ff678982e0f27c757a85f1f284d3400"},
{file = "fonttools-4.60.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93fcb420791d839ef592eada2b69997c445d0ce9c969b5190f2e16828ec10607"},
{file = "fonttools-4.60.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7916a381b094db4052ac284255186aebf74c5440248b78860cb41e300036f598"},
{file = "fonttools-4.60.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:58c8c393d5e16b15662cfc2d988491940458aa87894c662154f50c7b49440bef"},
{file = "fonttools-4.60.2-cp313-cp313-win32.whl", hash = "sha256:19c6e0afd8b02008caa0aa08ab896dfce5d0bcb510c49b2c499541d5cb95a963"},
{file = "fonttools-4.60.2-cp313-cp313-win_amd64.whl", hash = "sha256:6a500dc59e11b2338c2dba1f8cf11a4ae8be35ec24af8b2628b8759a61457b76"},
{file = "fonttools-4.60.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9387c532acbe323bbf2a920f132bce3c408a609d5f9dcfc6532fbc7e37f8ccbb"},
{file = "fonttools-4.60.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e6f1c824185b5b8fb681297f315f26ae55abb0d560c2579242feea8236b1cfef"},
{file = "fonttools-4.60.2-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:55a3129d1e4030b1a30260f1b32fe76781b585fb2111d04a988e141c09eb6403"},
{file = "fonttools-4.60.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b196e63753abc33b3b97a6fd6de4b7c4fef5552c0a5ba5e562be214d1e9668e0"},
{file = "fonttools-4.60.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:de76c8d740fb55745f3b154f0470c56db92ae3be27af8ad6c2e88f1458260c9a"},
{file = "fonttools-4.60.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6ba6303225c95998c9fda2d410aa792c3d2c1390a09df58d194b03e17583fa25"},
{file = "fonttools-4.60.2-cp314-cp314-win32.whl", hash = "sha256:0a89728ce10d7c816fedaa5380c06d2793e7a8a634d7ce16810e536c22047384"},
{file = "fonttools-4.60.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa8446e6ab8bd778b82cb1077058a2addba86f30de27ab9cc18ed32b34bc8667"},
{file = "fonttools-4.60.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4063bc81ac5a4137642865cb63dd270e37b3cd1f55a07c0d6e41d072699ccca2"},
{file = "fonttools-4.60.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ebfdb66fa69732ed604ab8e2a0431e6deff35e933a11d73418cbc7823d03b8e1"},
{file = "fonttools-4.60.2-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50b10b3b1a72d1d54c61b0e59239e1a94c0958f4a06a1febf97ce75388dd91a4"},
{file = "fonttools-4.60.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:beae16891a13b4a2ddec9b39b4de76092a3025e4d1c82362e3042b62295d5e4d"},
{file = "fonttools-4.60.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:522f017fdb3766fd5d2d321774ef351cc6ce88ad4e6ac9efe643e4a2b9d528db"},
{file = "fonttools-4.60.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:82cceceaf9c09a965a75b84a4b240dd3768e596ffb65ef53852681606fe7c9ba"},
{file = "fonttools-4.60.2-cp314-cp314t-win32.whl", hash = "sha256:bbfbc918a75437fe7e6d64d1b1e1f713237df1cf00f3a36dedae910b2ba01cee"},
{file = "fonttools-4.60.2-cp314-cp314t-win_amd64.whl", hash = "sha256:0e5cd9b0830f6550d58c84f3ab151a9892b50c4f9d538c5603c0ce6fff2eb3f1"},
{file = "fonttools-4.60.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a3c75b8b42f7f93906bdba9eb1197bb76aecbe9a0a7cf6feec75f7605b5e8008"},
{file = "fonttools-4.60.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0f86c8c37bc0ec0b9c141d5e90c717ff614e93c187f06d80f18c7057097f71bc"},
{file = "fonttools-4.60.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe905403fe59683b0e9a45f234af2866834376b8821f34633b1c76fb731b6311"},
{file = "fonttools-4.60.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38ce703b60a906e421e12d9e3a7f064883f5e61bb23e8961f4be33cfe578500b"},
{file = "fonttools-4.60.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9e810c06f3e79185cecf120e58b343ea5a89b54dd695fd644446bcf8c026da5e"},
{file = "fonttools-4.60.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:38faec8cc1d12122599814d15a402183f5123fb7608dac956121e7c6742aebc5"},
{file = "fonttools-4.60.2-cp39-cp39-win32.whl", hash = "sha256:80a45cf7bf659acb7b36578f300231873daba67bd3ca8cce181c73f861f14a37"},
{file = "fonttools-4.60.2-cp39-cp39-win_amd64.whl", hash = "sha256:c355d5972071938e1b1e0f5a1df001f68ecf1a62f34a3407dc8e0beccf052501"},
{file = "fonttools-4.60.2-py3-none-any.whl", hash = "sha256:73cf92eeda67cf6ff10c8af56fc8f4f07c1647d989a979be9e388a49be26552a"},
{file = "fonttools-4.60.2.tar.gz", hash = "sha256:d29552e6b155ebfc685b0aecf8d429cb76c14ab734c22ef5d3dea6fdf800c92c"},
]
[package.extras]
all = ["brotli (>=1.0.1) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\"", "lxml (>=4.0)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres ; platform_python_implementation == \"PyPy\"", "pycairo", "scipy ; platform_python_implementation != \"PyPy\"", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.1.0) ; python_version <= \"3.12\"", "xattr ; sys_platform == \"darwin\"", "zopfli (>=0.1.4)"]
all = ["brotli (>=1.0.1) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\"", "lxml (>=4.0)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres ; platform_python_implementation == \"PyPy\"", "pycairo", "scipy ; platform_python_implementation != \"PyPy\"", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.45.0)", "unicodedata2 (>=17.0.0) ; python_version <= \"3.14\"", "xattr ; sys_platform == \"darwin\"", "zopfli (>=0.1.4)"]
graphite = ["lz4 (>=1.7.4.2)"]
interpolatable = ["munkres ; platform_python_implementation == \"PyPy\"", "pycairo", "scipy ; platform_python_implementation != \"PyPy\""]
lxml = ["lxml (>=4.0)"]
pathops = ["skia-pathops (>=0.5.0)"]
plot = ["matplotlib"]
repacker = ["uharfbuzz (>=0.23.0)"]
repacker = ["uharfbuzz (>=0.45.0)"]
symfont = ["sympy"]
type1 = ["xattr ; sys_platform == \"darwin\""]
unicode = ["unicodedata2 (>=15.1.0) ; python_version <= \"3.12\""]
unicode = ["unicodedata2 (>=17.0.0) ; python_version <= \"3.14\""]
woff = ["brotli (>=1.0.1) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\"", "zopfli (>=0.1.4)"]
[[package]]
name = "fonttools"
version = "4.63.0"
description = "Tools to manipulate font files"
optional = false
python-versions = ">=3.10"
groups = ["analysis"]
markers = "python_version >= \"3.11\""
files = [
{file = "fonttools-4.63.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e3297a6a4059b4acc3a1e9a8b04741f240a80044eef08ebd32e8b5bcdddce75b"},
{file = "fonttools-4.63.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b1cd75a03ad8cb5bc40c90bfde68c0c47de423aa19e5c0f362b43520645eea94"},
{file = "fonttools-4.63.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0425b277a59cff3d80ca42162a8de360f318438a2ac83570842a678d826d579"},
{file = "fonttools-4.63.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d7e5c9973aa04c95650c96e5f5ad865fbf42d62079163ecfab1e01cbc2504c22"},
{file = "fonttools-4.63.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cb014d58140a38135f16064c74c652ed57aa0b75cbf8bb59cac821f7edb5334e"},
{file = "fonttools-4.63.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:032038247a96c1690f9f31e377c389383c902531b085aa4e4dabd6f57f870e69"},
{file = "fonttools-4.63.0-cp310-cp310-win32.whl", hash = "sha256:a8b33a82979e0a6a34ff435cc81317be1f95ec1ebb7a3a2d1c8a6a54f02ae44e"},
{file = "fonttools-4.63.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c18358a155d75034911c5ee397a5b44cd19dd325dbb8b35fb60bf421d6a72ac"},
{file = "fonttools-4.63.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2b8ae05d9eacf6081414d759c0a352769ac28ce31280d6bb8e77b03f9e3c449f"},
{file = "fonttools-4.63.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79cdc9f567aec74a72918fd060283911406750cbc9fd28c1316023deb6ce31a9"},
{file = "fonttools-4.63.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c14b4fd138c4bafcca294765c547914e1aa431ae1ca94ab99d8db08c958bd3b"},
{file = "fonttools-4.63.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76ac49f929aecaf82d83250b8347e099d7aecba0f4726c1d9b6df3b8bb5fe18"},
{file = "fonttools-4.63.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dcf076a4474fe0d7367e5bbf5b052c7284fa1feca729c04176ce513521afd8a0"},
{file = "fonttools-4.63.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7dd683fef0663e9f0f45cf541d788d24caa3ec9db50796b588e1757d8b3bc007"},
{file = "fonttools-4.63.0-cp311-cp311-win32.whl", hash = "sha256:afefc1ed0a59785a7fb06ea7e1678e849c193e1e387db783579bc7b3056fcfcb"},
{file = "fonttools-4.63.0-cp311-cp311-win_amd64.whl", hash = "sha256:063e08bd17bd5a90127a14123de0d6a952dbc847695fd98b63c043d58057f90c"},
{file = "fonttools-4.63.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:37dd23e621e3b0aef1baa70a303b80aaf38449632cfc8fd2a55fb285bbccfc02"},
{file = "fonttools-4.63.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a9faff9e0c1f76f9fd55899d2ce785832efebab37eb8ae13995853aef178bef0"},
{file = "fonttools-4.63.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef3048ef05dbb552b89817713d9cac912e00d0fde4a3105c00d29e52e10c89af"},
{file = "fonttools-4.63.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58dc6bb86a78d782f00f9190ca02c119cf5bbe2807536e361e18d42019f877d8"},
{file = "fonttools-4.63.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee08ebfa58f6e1aeff5697ab9582105bb620008c1caafb681e4c557e7483027b"},
{file = "fonttools-4.63.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:27fdc65af8da6f88b9c6121c47a464cbe359fcfff7ff6fc2d37a1f395d755b78"},
{file = "fonttools-4.63.0-cp312-cp312-win32.whl", hash = "sha256:af2fd1664d00a397d75f806985ddb36282091c2131a73a6485c23b4a34722263"},
{file = "fonttools-4.63.0-cp312-cp312-win_amd64.whl", hash = "sha256:59ac449f8cca9b4ffa08d2e7bbadad87ce710d69d1eda5c3c1ce579baa987272"},
{file = "fonttools-4.63.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:cd7e9857e5e63738b9d9fd707bc1f59c8b09e5177726d23664db393c59bb08bd"},
{file = "fonttools-4.63.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c2a2a42198b696a6f48fad91709afb55176e66a5e566131219dba372fb7f8c59"},
{file = "fonttools-4.63.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e874792a8212b44583ea02189d9e693906b2f78b261f372f95d6c563210ac1d"},
{file = "fonttools-4.63.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22135da48a348785c5e2d5d2d9d6bec5ed44adacbaeb9db12d9493bf6c6bfa68"},
{file = "fonttools-4.63.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ccf41f2efdf56994d22d73bef4ced1052161958169428d06ba9724ea9e9a64be"},
{file = "fonttools-4.63.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9ced0bd02ac751dd6319b0da88aaef24414e3b0dbc32bb4f24944821a3741a27"},
{file = "fonttools-4.63.0-cp313-cp313-win32.whl", hash = "sha256:85be818f5506e8a7753153def2c9550178f0ecae6a47b5e0e8dbb23f7cc90380"},
{file = "fonttools-4.63.0-cp313-cp313-win_amd64.whl", hash = "sha256:ba04cb5891d4c0c21b6da95eda8d7b090021508a294fff33464fc7d241e0856b"},
{file = "fonttools-4.63.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fd1e3094f42d806d3d7c79162fc59e5910fcbe3a7360c385b8da969bc4493745"},
{file = "fonttools-4.63.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6e528da43bc3791085f8cb6141b1d13e459226790240340fcbb4625649238b03"},
{file = "fonttools-4.63.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b2248c5decb223562f7902ff6325077a073f608ee8e33e88ad88db734eb9f49"},
{file = "fonttools-4.63.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:308f957cdeaf8abe4e5f2f124902ef405448af92c90f80e302a3b771c2e6116b"},
{file = "fonttools-4.63.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bf00f21eb5fb721dbaf73d1e9da6d02a1af7768f2ebcf9798be98beab8ba90f6"},
{file = "fonttools-4.63.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c1aaa4b9c75798400ac043ce04d74e7830376c85095a5a6ed7cba2f17a266bf4"},
{file = "fonttools-4.63.0-cp314-cp314-win32.whl", hash = "sha256:22693918177bd9ceabec4736d338045f357769416fc6b0b2508eefef75b08616"},
{file = "fonttools-4.63.0-cp314-cp314-win_amd64.whl", hash = "sha256:7d782fac32985914c351556f68ac0855391572bcd87de50e05970d3cd4c96fc5"},
{file = "fonttools-4.63.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6db5140a60a5d731d21ec076745b40a310607731b0a565b50776393188649001"},
{file = "fonttools-4.63.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d76edbff9014094dbf03bd2d074709dfa6ec7aba13d838c937a2b33d2d6a86e"},
{file = "fonttools-4.63.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0eac00b9118c3c2f87d272e45341871c5b3066baa3c86897fa634a7c3fb59096"},
{file = "fonttools-4.63.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51394295f1a51de8b5f30bdb1e1b9a4231536c7064ef5c6e211eec19fa36036f"},
{file = "fonttools-4.63.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9e12f105d2b6342c559c298afb674006bb2893afc7102dcf8a1b55b0486b4e40"},
{file = "fonttools-4.63.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:796f27556dbe094c4824f75ca85267e4df776c79036c8441469a4df37038c196"},
{file = "fonttools-4.63.0-cp314-cp314t-win32.whl", hash = "sha256:948428a275741f0b64b113c955425a953314f4b9ab9997f73a72c83e68e569c8"},
{file = "fonttools-4.63.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6d4741eb179121cab9eea4cb2393d24492373a260d7945006358c08cfbf45419"},
{file = "fonttools-4.63.0-py3-none-any.whl", hash = "sha256:445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d"},
{file = "fonttools-4.63.0.tar.gz", hash = "sha256:caeb583deeb5168e694b65cda8b4ee62abedfa66cf88488734466f2366b9c4e0"},
]
[package.extras]
all = ["brotli (>=1.0.1) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\"", "lxml (>=4.0)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres ; platform_python_implementation == \"PyPy\"", "pycairo", "scipy ; platform_python_implementation != \"PyPy\"", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.45.0)", "unicodedata2 (>=17.0.0) ; python_version <= \"3.14\"", "xattr ; sys_platform == \"darwin\"", "zopfli (>=0.1.4)"]
graphite = ["lz4 (>=1.7.4.2)"]
interpolatable = ["munkres ; platform_python_implementation == \"PyPy\"", "pycairo", "scipy ; platform_python_implementation != \"PyPy\""]
lxml = ["lxml (>=4.0)"]
pathops = ["skia-pathops (>=0.5.0)"]
plot = ["matplotlib"]
repacker = ["uharfbuzz (>=0.45.0)"]
symfont = ["sympy"]
type1 = ["xattr ; sys_platform == \"darwin\""]
unicode = ["unicodedata2 (>=17.0.0) ; python_version <= \"3.14\""]
woff = ["brotli (>=1.0.1) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\"", "zopfli (>=0.1.4)"]
[[package]]
@@ -1595,18 +1670,18 @@ zoneinfo = ["tzdata (>=2025.2) ; sys_platform == \"win32\" or sys_platform == \"
[[package]]
name = "idna"
version = "3.11"
version = "3.17"
description = "Internationalized Domain Names in Applications (IDNA)"
optional = false
python-versions = ">=3.8"
python-versions = ">=3.9"
groups = ["main", "analysis"]
files = [
{file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"},
{file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"},
{file = "idna-3.17-py3-none-any.whl", hash = "sha256:466e48829084efe2548012b855df21540b96f2e20e51bd124c851536556a592c"},
{file = "idna-3.17.tar.gz", hash = "sha256:5eb0cb53bc467c12eadcf6de83163ad8527cec9416f44b9b61b19caedad2b87f"},
]
[package.extras]
all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
all = ["mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
[[package]]
name = "importlib-metadata"
@@ -2158,14 +2233,15 @@ jupyter_server = ">=1.1.2"
[[package]]
name = "jupyter-server"
version = "2.17.0"
version = "2.18.2"
description = "The backend—i.e. core services, APIs, and REST endpoints—to Jupyter web applications."
optional = false
python-versions = ">=3.9"
groups = ["analysis"]
markers = "python_version < \"3.11\""
files = [
{file = "jupyter_server-2.17.0-py3-none-any.whl", hash = "sha256:e8cb9c7db4251f51ed307e329b81b72ccf2056ff82d50524debde1ee1870e13f"},
{file = "jupyter_server-2.17.0.tar.gz", hash = "sha256:c38ea898566964c888b4772ae1ed58eca84592e88251d2cfc4d171f81f7e99d5"},
{file = "jupyter_server-2.18.2-py3-none-any.whl", hash = "sha256:fa5e46539ded65791838035a2b6001f13e54d5f64b8b3752eb1e91fdd641a5b8"},
{file = "jupyter_server-2.18.2.tar.gz", hash = "sha256:06b4f40d8a7a00bb39d5216859c81374a0e7cfefe6d8a5a7facc5a5c37c679a7"},
]
[package.dependencies]
@@ -2190,9 +2266,47 @@ traitlets = ">=5.6.0"
websocket-client = ">=1.7"
[package.extras]
docs = ["ipykernel", "jinja2", "jupyter-client", "myst-parser", "nbformat", "prometheus-client", "pydata-sphinx-theme", "send2trash", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-openapi (>=0.8.0)", "sphinxcontrib-spelling", "sphinxemoji", "tornado", "typing-extensions"]
docs = ["ipykernel", "jinja2", "jupyter-client", "myst-parser", "nbformat", "prometheus-client", "pydata-sphinx-theme", "send2trash", "sphinx (<9.0)", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-openapi (>=0.8.0)", "sphinxcontrib-spelling", "sphinxemoji", "tornado", "typing-extensions"]
test = ["flaky", "ipykernel", "pre-commit", "pytest (>=7.0,<9)", "pytest-console-scripts", "pytest-jupyter[server] (>=0.7)", "pytest-timeout", "requests"]
[[package]]
name = "jupyter-server"
version = "2.19.0"
description = "The backend—i.e. core services, APIs, and REST endpoints—to Jupyter web applications."
optional = false
python-versions = ">=3.10"
groups = ["analysis"]
markers = "python_version >= \"3.11\""
files = [
{file = "jupyter_server-2.19.0-py3-none-any.whl", hash = "sha256:cb76591b76d7093584c2ad2ae72ac3d58614a4b597507a1bb04e1f9f683cf9ea"},
{file = "jupyter_server-2.19.0.tar.gz", hash = "sha256:1731236bc32b680223e1ceb9d68209a845203475012ef68773a81434b46a31a7"},
]
[package.dependencies]
anyio = ">=3.1.0"
argon2-cffi = ">=21.1"
jinja2 = ">=3.0.3"
jupyter-client = ">=7.4.4"
jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0"
jupyter-events = ">=0.11.0"
jupyter-server-terminals = ">=0.4.4"
nbconvert = ">=6.4.4"
nbformat = ">=5.3.0"
overrides = {version = ">=5.0", markers = "python_version < \"3.12\""}
packaging = ">=22.0"
prometheus-client = ">=0.9"
pywinpty = {version = ">=2.0.1", markers = "os_name == \"nt\""}
pyzmq = ">=24"
send2trash = ">=1.8.2"
terminado = ">=0.8.3"
tornado = ">=6.2.0"
traitlets = ">=5.6.0"
websocket-client = ">=1.7"
[package.extras]
docs = ["ipykernel", "jinja2", "jupyter-client", "myst-parser", "nbformat", "prometheus-client", "pydata-sphinx-theme", "send2trash", "sphinx (<9.0)", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-openapi (>=0.8.0)", "sphinxcontrib-spelling", "sphinxemoji", "tornado", "typing-extensions"]
test = ["flaky", "ipykernel", "pre-commit", "pytest (>=7.0,<10)", "pytest-console-scripts", "pytest-jupyter[server] (>=0.7)", "pytest-timeout", "requests"]
[[package]]
name = "jupyter-server-terminals"
version = "0.5.3"
@@ -2215,14 +2329,14 @@ test = ["jupyter-server (>=2.0.0)", "pytest (>=7.0)", "pytest-jupyter[server] (>
[[package]]
name = "jupyterlab"
version = "4.4.10"
version = "4.5.7"
description = "JupyterLab computational environment"
optional = false
python-versions = ">=3.9"
groups = ["analysis"]
files = [
{file = "jupyterlab-4.4.10-py3-none-any.whl", hash = "sha256:65939ab4c8dcd0c42185c2d0d1a9d60b254dc8c46fc4fdb286b63c51e9358e07"},
{file = "jupyterlab-4.4.10.tar.gz", hash = "sha256:521c017508af4e1d6d9d8a9d90f47a11c61197ad63b2178342489de42540a615"},
{file = "jupyterlab-4.5.7-py3-none-any.whl", hash = "sha256:fba4cb0e2c44a52859669d8c98b45de029d5e515f8407bf8534d2a8fc5f0964d"},
{file = "jupyterlab-4.5.7.tar.gz", hash = "sha256:55a9822c4754da305f41e113452c68383e214dcf96de760146af89ce5d5117b0"},
]
[package.dependencies]
@@ -2234,7 +2348,7 @@ jinja2 = ">=3.0.3"
jupyter-core = "*"
jupyter-lsp = ">=2.0.0"
jupyter-server = ">=2.4.0,<3"
jupyterlab-server = ">=2.27.1,<3"
jupyterlab-server = ">=2.28.0,<3"
notebook-shim = ">=0.2"
packaging = "*"
setuptools = ">=41.1.0"
@@ -2243,9 +2357,9 @@ tornado = ">=6.2.0"
traitlets = "*"
[package.extras]
dev = ["build", "bump2version", "coverage", "hatch", "pre-commit", "pytest-cov", "ruff (==0.11.4)"]
dev = ["build", "bump2version", "coverage", "hatch", "pre-commit", "pytest-cov", "ruff (==0.11.12)"]
docs = ["jsx-lexer", "myst-parser", "pydata-sphinx-theme (>=0.13.0)", "pytest", "pytest-check-links", "pytest-jupyter", "sphinx (>=1.8,<8.2.0)", "sphinx-copybutton"]
docs-screenshots = ["altair (==5.5.0)", "ipython (==8.16.1)", "ipywidgets (==8.1.5)", "jupyterlab-geojson (==3.4.0)", "jupyterlab-language-pack-zh-cn (==4.3.post1)", "matplotlib (==3.10.0)", "nbconvert (>=7.0.0)", "pandas (==2.2.3)", "scipy (==1.15.1)", "vega-datasets (==0.9.0)"]
docs-screenshots = ["altair (==6.0.0)", "ipython (==8.16.1)", "ipywidgets (==8.1.5)", "jupyterlab-geojson (==3.4.0)", "jupyterlab-language-pack-zh-cn (==4.3.post1)", "matplotlib (==3.10.0)", "nbconvert (>=7.0.0)", "pandas (==2.2.3)", "scipy (==1.15.1)"]
test = ["coverage", "pytest (>=7.0)", "pytest-check-links (>=0.7)", "pytest-console-scripts", "pytest-cov", "pytest-jupyter (>=0.5.3)", "pytest-timeout", "pytest-tornasync", "requests", "requests-cache", "virtualenv"]
upgrade-extension = ["copier (>=9,<10)", "jinja2-time (<0.3)", "pydantic (<3.0)", "pyyaml-include (<3.0)", "tomli-w (<2.0)"]
@@ -2573,14 +2687,14 @@ altgraph = ">=0.17"
[[package]]
name = "mako"
version = "1.3.10"
version = "1.3.12"
description = "A super-fast templating language that borrows the best ideas from the existing templating languages."
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59"},
{file = "mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28"},
{file = "mako-1.3.12-py3-none-any.whl", hash = "sha256:8f61569480282dbf557145ce441e4ba888be453c30989f879f0d652e39f53ea9"},
{file = "mako-1.3.12.tar.gz", hash = "sha256:9f778e93289bd410bb35daadeb4fc66d95a746f0b75777b942088b7fd7af550a"},
]
[package.dependencies]
@@ -2907,14 +3021,14 @@ files = [
[[package]]
name = "mistune"
version = "3.1.4"
version = "3.2.1"
description = "A sane and fast Markdown parser with useful plugins and renderers"
optional = false
python-versions = ">=3.8"
groups = ["analysis"]
files = [
{file = "mistune-3.1.4-py3-none-any.whl", hash = "sha256:93691da911e5d9d2e23bc54472892aff676df27a75274962ff9edc210364266d"},
{file = "mistune-3.1.4.tar.gz", hash = "sha256:b5a7f801d389f724ec702840c11d8fc48f2b33519102fc7ee739e8177b672164"},
{file = "mistune-3.2.1-py3-none-any.whl", hash = "sha256:78cdb0ba5e938053ccf63651b352508d2efa9411dc8810bfb05f2dc5140c0048"},
{file = "mistune-3.2.1.tar.gz", hash = "sha256:7c8e5501d38bac1582e067e46c8343f17d57ea1aaa735823f3aba1fd59c88a28"},
]
[package.dependencies]
@@ -3075,14 +3189,14 @@ test = ["flaky", "ipykernel (>=6.19.3)", "ipython", "ipywidgets", "nbconvert (>=
[[package]]
name = "nbconvert"
version = "7.16.6"
description = "Converting Jupyter Notebooks (.ipynb files) to other formats. Output formats include asciidoc, html, latex, markdown, pdf, py, rst, script. nbconvert can be used both as a Python library (`import nbconvert`) or as a command line tool (invoked as `jupyter nbconvert ...`)."
version = "7.17.1"
description = "Convert Jupyter Notebooks (.ipynb files) to other formats."
optional = false
python-versions = ">=3.8"
python-versions = ">=3.9"
groups = ["analysis"]
files = [
{file = "nbconvert-7.16.6-py3-none-any.whl", hash = "sha256:1375a7b67e0c2883678c48e506dc320febb57685e5ee67faa51b18a90f3a712b"},
{file = "nbconvert-7.16.6.tar.gz", hash = "sha256:576a7e37c6480da7b8465eefa66c17844243816ce1ccc372633c6b71c3c0f582"},
{file = "nbconvert-7.17.1-py3-none-any.whl", hash = "sha256:aa85c087b435e7bf1ffd03319f658e285f2b89eccab33bc1ba7025495ab3e7c8"},
{file = "nbconvert-7.17.1.tar.gz", hash = "sha256:34d0d0a7e73ce3cbab6c5aae8f4f468797280b01fd8bd2ca746da8569eddd7d2"},
]
[package.dependencies]
@@ -3103,8 +3217,8 @@ pygments = ">=2.4.1"
traitlets = ">=5.1"
[package.extras]
all = ["flaky", "ipykernel", "ipython", "ipywidgets (>=7.5)", "myst-parser", "nbsphinx (>=0.2.12)", "playwright", "pydata-sphinx-theme", "pyqtwebengine (>=5.15)", "pytest (>=7)", "sphinx (==5.0.2)", "sphinxcontrib-spelling", "tornado (>=6.1)"]
docs = ["ipykernel", "ipython", "myst-parser", "nbsphinx (>=0.2.12)", "pydata-sphinx-theme", "sphinx (==5.0.2)", "sphinxcontrib-spelling"]
all = ["flaky", "intersphinx-registry", "ipykernel", "ipython", "ipywidgets (>=7.5)", "myst-parser", "nbsphinx (>=0.2.12)", "playwright", "pydata-sphinx-theme", "pyqtwebengine (>=5.15)", "pytest (>=7)", "sphinx (>=5.0.2)", "sphinxcontrib-spelling", "tornado (>=6.1)"]
docs = ["intersphinx-registry", "ipykernel", "ipython", "myst-parser", "nbsphinx (>=0.2.12)", "pydata-sphinx-theme", "sphinx (>=5.0.2)", "sphinxcontrib-spelling"]
qtpdf = ["pyqtwebengine (>=5.15)"]
qtpng = ["pyqtwebengine (>=5.15)"]
serve = ["tornado (>=6.1)"]
@@ -3170,7 +3284,7 @@ version = "2.0.2"
description = "Fundamental package for array computing in Python"
optional = false
python-versions = ">=3.9"
groups = ["main", "analysis", "powermon"]
groups = ["main", "analysis"]
files = [
{file = "numpy-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece"},
{file = "numpy-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f15975dfec0cf2239224d80e32c3170b1d168335eaedee69da84fbe9f1f9cd04"},
@@ -3218,7 +3332,7 @@ files = [
{file = "numpy-2.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a46288ec55ebbd58947d31d72be2c63cbf839f0a63b49cb755022310792a3385"},
{file = "numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78"},
]
markers = {main = "python_version < \"3.11\" and extra == \"analysis\"", analysis = "python_version < \"3.11\"", powermon = "python_version < \"3.11\""}
markers = {main = "python_version < \"3.11\" and extra == \"analysis\"", analysis = "python_version < \"3.11\""}
[[package]]
name = "numpy"
@@ -3226,7 +3340,7 @@ version = "2.3.4"
description = "Fundamental package for array computing in Python"
optional = false
python-versions = ">=3.11"
groups = ["main", "analysis", "powermon"]
groups = ["main", "analysis"]
files = [
{file = "numpy-2.3.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e78aecd2800b32e8347ce49316d3eaf04aed849cd5b38e0af39f829a4e59f5eb"},
{file = "numpy-2.3.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7fd09cc5d65bda1e79432859c40978010622112e9194e581e3415a3eccc7f43f"},
@@ -3303,7 +3417,7 @@ files = [
{file = "numpy-2.3.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:81b3a59793523e552c4a96109dde028aa4448ae06ccac5a76ff6532a85558a7f"},
{file = "numpy-2.3.4.tar.gz", hash = "sha256:a7d018bfedb375a8d979ac758b120ba846a7fe764911a64465fd87b8729f4a6a"},
]
markers = {main = "extra == \"analysis\" and python_version >= \"3.11\"", analysis = "python_version >= \"3.11\"", powermon = "python_version >= \"3.11\""}
markers = {main = "python_version >= \"3.11\" and extra == \"analysis\"", analysis = "python_version >= \"3.11\""}
[[package]]
name = "overrides"
@@ -3455,7 +3569,7 @@ description = "Type annotations for pandas"
optional = true
python-versions = ">=3.10"
groups = ["main"]
markers = "extra == \"analysis\" and python_version >= \"3.11\""
markers = "python_version >= \"3.11\" and extra == \"analysis\""
files = [
{file = "pandas_stubs-2.3.2.250926-py3-none-any.whl", hash = "sha256:81121818453dcfe00f45c852f4dceee043640b813830f6e7bd084a4ef7ff7270"},
{file = "pandas_stubs-2.3.2.250926.tar.gz", hash = "sha256:c64b9932760ceefb96a3222b953e6a251321a9832a28548be6506df473a66406"},
@@ -3935,22 +4049,42 @@ wcwidth = "*"
[[package]]
name = "protobuf"
version = "6.33.0"
version = "6.33.6"
description = ""
optional = false
python-versions = ">=3.9"
groups = ["main", "dev"]
markers = "python_version < \"3.11\""
files = [
{file = "protobuf-6.33.0-cp310-abi3-win32.whl", hash = "sha256:d6101ded078042a8f17959eccd9236fb7a9ca20d3b0098bbcb91533a5680d035"},
{file = "protobuf-6.33.0-cp310-abi3-win_amd64.whl", hash = "sha256:9a031d10f703f03768f2743a1c403af050b6ae1f3480e9c140f39c45f81b13ee"},
{file = "protobuf-6.33.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:905b07a65f1a4b72412314082c7dbfae91a9e8b68a0cc1577515f8df58ecf455"},
{file = "protobuf-6.33.0-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e0697ece353e6239b90ee43a9231318302ad8353c70e6e45499fa52396debf90"},
{file = "protobuf-6.33.0-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:e0a1715e4f27355afd9570f3ea369735afc853a6c3951a6afe1f80d8569ad298"},
{file = "protobuf-6.33.0-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:35be49fd3f4fefa4e6e2aacc35e8b837d6703c37a2168a55ac21e9b1bc7559ef"},
{file = "protobuf-6.33.0-cp39-cp39-win32.whl", hash = "sha256:cd33a8e38ea3e39df66e1bbc462b076d6e5ba3a4ebbde58219d777223a7873d3"},
{file = "protobuf-6.33.0-cp39-cp39-win_amd64.whl", hash = "sha256:c963e86c3655af3a917962c9619e1a6b9670540351d7af9439d06064e3317cc9"},
{file = "protobuf-6.33.0-py3-none-any.whl", hash = "sha256:25c9e1963c6734448ea2d308cfa610e692b801304ba0908d7bfa564ac5132995"},
{file = "protobuf-6.33.0.tar.gz", hash = "sha256:140303d5c8d2037730c548f8c7b93b20bb1dc301be280c378b82b8894589c954"},
{file = "protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3"},
{file = "protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326"},
{file = "protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a"},
{file = "protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2"},
{file = "protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3"},
{file = "protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593"},
{file = "protobuf-6.33.6-cp39-cp39-win32.whl", hash = "sha256:bd56799fb262994b2c2faa1799693c95cc2e22c62f56fb43af311cae45d26f0e"},
{file = "protobuf-6.33.6-cp39-cp39-win_amd64.whl", hash = "sha256:f443a394af5ed23672bc6c486be138628fbe5c651ccbc536873d7da23d1868cf"},
{file = "protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901"},
{file = "protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135"},
]
[[package]]
name = "protobuf"
version = "7.35.0"
description = ""
optional = false
python-versions = ">=3.10"
groups = ["main", "dev"]
markers = "python_version >= \"3.11\""
files = [
{file = "protobuf-7.35.0-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:66be6c513931c794fa92c080ffee41671390da3d79da219cf9c0c0907f035dda"},
{file = "protobuf-7.35.0-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:fcbe42a4ac09d3ec9c987ddfcd956afd0b15f1ff613bd8371bde9405ffd5c8e5"},
{file = "protobuf-7.35.0-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:4cbf5cc286130e06a6c9bbefac442431173906dfcc979712183d4adcc01b37ee"},
{file = "protobuf-7.35.0-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:6c0f98f10c8a05ea30f8993dfef2de093d27b490fdae78bb60c8343795d55011"},
{file = "protobuf-7.35.0-cp310-abi3-win32.whl", hash = "sha256:4c4617b83ade0e279d1d2bfe04025a1adb87f9ed657de038620dc0ff959357f6"},
{file = "protobuf-7.35.0-cp310-abi3-win_amd64.whl", hash = "sha256:f05bcadf9a2a6b8dda047007075135fb7d08c73d9177aabc067e1be46881a201"},
{file = "protobuf-7.35.0-py3-none-any.whl", hash = "sha256:c13f325cf242bad135c350629eeb5d54b24228eb472fb3e2e9ebbd4c5dc20ca0"},
{file = "protobuf-7.35.0.tar.gz", hash = "sha256:a2efd84605f41e559f1881b0912b44099d0a2ac9bf46b3474823f10fb393b0e6"},
]
[[package]]
@@ -4016,52 +4150,120 @@ tests = ["pytest"]
[[package]]
name = "pyarrow"
version = "16.1.0"
version = "18.1.0"
description = "Python library for Apache Arrow"
optional = false
python-versions = ">=3.8"
python-versions = ">=3.9"
groups = ["powermon"]
markers = "python_version < \"3.14\""
files = [
{file = "pyarrow-16.1.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:17e23b9a65a70cc733d8b738baa6ad3722298fa0c81d88f63ff94bf25eaa77b9"},
{file = "pyarrow-16.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4740cc41e2ba5d641071d0ab5e9ef9b5e6e8c7611351a5cb7c1d175eaf43674a"},
{file = "pyarrow-16.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98100e0268d04e0eec47b73f20b39c45b4006f3c4233719c3848aa27a03c1aef"},
{file = "pyarrow-16.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f68f409e7b283c085f2da014f9ef81e885d90dcd733bd648cfba3ef265961848"},
{file = "pyarrow-16.1.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:a8914cd176f448e09746037b0c6b3a9d7688cef451ec5735094055116857580c"},
{file = "pyarrow-16.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:48be160782c0556156d91adbdd5a4a7e719f8d407cb46ae3bb4eaee09b3111bd"},
{file = "pyarrow-16.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:9cf389d444b0f41d9fe1444b70650fea31e9d52cfcb5f818b7888b91b586efff"},
{file = "pyarrow-16.1.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:d0ebea336b535b37eee9eee31761813086d33ed06de9ab6fc6aaa0bace7b250c"},
{file = "pyarrow-16.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2e73cfc4a99e796727919c5541c65bb88b973377501e39b9842ea71401ca6c1c"},
{file = "pyarrow-16.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf9251264247ecfe93e5f5a0cd43b8ae834f1e61d1abca22da55b20c788417f6"},
{file = "pyarrow-16.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddf5aace92d520d3d2a20031d8b0ec27b4395cab9f74e07cc95edf42a5cc0147"},
{file = "pyarrow-16.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:25233642583bf658f629eb230b9bb79d9af4d9f9229890b3c878699c82f7d11e"},
{file = "pyarrow-16.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a33a64576fddfbec0a44112eaf844c20853647ca833e9a647bfae0582b2ff94b"},
{file = "pyarrow-16.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:185d121b50836379fe012753cf15c4ba9638bda9645183ab36246923875f8d1b"},
{file = "pyarrow-16.1.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:2e51ca1d6ed7f2e9d5c3c83decf27b0d17bb207a7dea986e8dc3e24f80ff7d6f"},
{file = "pyarrow-16.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:06ebccb6f8cb7357de85f60d5da50e83507954af617d7b05f48af1621d331c9a"},
{file = "pyarrow-16.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b04707f1979815f5e49824ce52d1dceb46e2f12909a48a6a753fe7cafbc44a0c"},
{file = "pyarrow-16.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d32000693deff8dc5df444b032b5985a48592c0697cb6e3071a5d59888714e2"},
{file = "pyarrow-16.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:8785bb10d5d6fd5e15d718ee1d1f914fe768bf8b4d1e5e9bf253de8a26cb1628"},
{file = "pyarrow-16.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:e1369af39587b794873b8a307cc6623a3b1194e69399af0efd05bb202195a5a7"},
{file = "pyarrow-16.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:febde33305f1498f6df85e8020bca496d0e9ebf2093bab9e0f65e2b4ae2b3444"},
{file = "pyarrow-16.1.0-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:b5f5705ab977947a43ac83b52ade3b881eb6e95fcc02d76f501d549a210ba77f"},
{file = "pyarrow-16.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0d27bf89dfc2576f6206e9cd6cf7a107c9c06dc13d53bbc25b0bd4556f19cf5f"},
{file = "pyarrow-16.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d07de3ee730647a600037bc1d7b7994067ed64d0eba797ac74b2bc77384f4c2"},
{file = "pyarrow-16.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fbef391b63f708e103df99fbaa3acf9f671d77a183a07546ba2f2c297b361e83"},
{file = "pyarrow-16.1.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:19741c4dbbbc986d38856ee7ddfdd6a00fc3b0fc2d928795b95410d38bb97d15"},
{file = "pyarrow-16.1.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:f2c5fb249caa17b94e2b9278b36a05ce03d3180e6da0c4c3b3ce5b2788f30eed"},
{file = "pyarrow-16.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:e6b6d3cd35fbb93b70ade1336022cc1147b95ec6af7d36906ca7fe432eb09710"},
{file = "pyarrow-16.1.0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:18da9b76a36a954665ccca8aa6bd9f46c1145f79c0bb8f4f244f5f8e799bca55"},
{file = "pyarrow-16.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:99f7549779b6e434467d2aa43ab2b7224dd9e41bdde486020bae198978c9e05e"},
{file = "pyarrow-16.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f07fdffe4fd5b15f5ec15c8b64584868d063bc22b86b46c9695624ca3505b7b4"},
{file = "pyarrow-16.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddfe389a08ea374972bd4065d5f25d14e36b43ebc22fc75f7b951f24378bf0b5"},
{file = "pyarrow-16.1.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:3b20bd67c94b3a2ea0a749d2a5712fc845a69cb5d52e78e6449bbd295611f3aa"},
{file = "pyarrow-16.1.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:ba8ac20693c0bb0bf4b238751d4409e62852004a8cf031c73b0e0962b03e45e3"},
{file = "pyarrow-16.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:31a1851751433d89a986616015841977e0a188662fcffd1a5677453f1df2de0a"},
{file = "pyarrow-16.1.0.tar.gz", hash = "sha256:15fbb22ea96d11f0b5768504a3f961edab25eaf4197c341720c4a387f6c60315"},
{file = "pyarrow-18.1.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:e21488d5cfd3d8b500b3238a6c4b075efabc18f0f6d80b29239737ebd69caa6c"},
{file = "pyarrow-18.1.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:b516dad76f258a702f7ca0250885fc93d1fa5ac13ad51258e39d402bd9e2e1e4"},
{file = "pyarrow-18.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f443122c8e31f4c9199cb23dca29ab9427cef990f283f80fe15b8e124bcc49b"},
{file = "pyarrow-18.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0a03da7f2758645d17b7b4f83c8bffeae5bbb7f974523fe901f36288d2eab71"},
{file = "pyarrow-18.1.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:ba17845efe3aa358ec266cf9cc2800fa73038211fb27968bfa88acd09261a470"},
{file = "pyarrow-18.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:3c35813c11a059056a22a3bef520461310f2f7eea5c8a11ef9de7062a23f8d56"},
{file = "pyarrow-18.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:9736ba3c85129d72aefa21b4f3bd715bc4190fe4426715abfff90481e7d00812"},
{file = "pyarrow-18.1.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:eaeabf638408de2772ce3d7793b2668d4bb93807deed1725413b70e3156a7854"},
{file = "pyarrow-18.1.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:3b2e2239339c538f3464308fd345113f886ad031ef8266c6f004d49769bb074c"},
{file = "pyarrow-18.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f39a2e0ed32a0970e4e46c262753417a60c43a3246972cfc2d3eb85aedd01b21"},
{file = "pyarrow-18.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e31e9417ba9c42627574bdbfeada7217ad8a4cbbe45b9d6bdd4b62abbca4c6f6"},
{file = "pyarrow-18.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:01c034b576ce0eef554f7c3d8c341714954be9b3f5d5bc7117006b85fcf302fe"},
{file = "pyarrow-18.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:f266a2c0fc31995a06ebd30bcfdb7f615d7278035ec5b1cd71c48d56daaf30b0"},
{file = "pyarrow-18.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:d4f13eee18433f99adefaeb7e01d83b59f73360c231d4782d9ddfaf1c3fbde0a"},
{file = "pyarrow-18.1.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:9f3a76670b263dc41d0ae877f09124ab96ce10e4e48f3e3e4257273cee61ad0d"},
{file = "pyarrow-18.1.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:da31fbca07c435be88a0c321402c4e31a2ba61593ec7473630769de8346b54ee"},
{file = "pyarrow-18.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:543ad8459bc438efc46d29a759e1079436290bd583141384c6f7a1068ed6f992"},
{file = "pyarrow-18.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0743e503c55be0fdb5c08e7d44853da27f19dc854531c0570f9f394ec9671d54"},
{file = "pyarrow-18.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:d4b3d2a34780645bed6414e22dda55a92e0fcd1b8a637fba86800ad737057e33"},
{file = "pyarrow-18.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c52f81aa6f6575058d8e2c782bf79d4f9fdc89887f16825ec3a66607a5dd8e30"},
{file = "pyarrow-18.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:0ad4892617e1a6c7a551cfc827e072a633eaff758fa09f21c4ee548c30bcaf99"},
{file = "pyarrow-18.1.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:84e314d22231357d473eabec709d0ba285fa706a72377f9cc8e1cb3c8013813b"},
{file = "pyarrow-18.1.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:f591704ac05dfd0477bb8f8e0bd4b5dc52c1cadf50503858dce3a15db6e46ff2"},
{file = "pyarrow-18.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acb7564204d3c40babf93a05624fc6a8ec1ab1def295c363afc40b0c9e66c191"},
{file = "pyarrow-18.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:74de649d1d2ccb778f7c3afff6085bd5092aed4c23df9feeb45dd6b16f3811aa"},
{file = "pyarrow-18.1.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:f96bd502cb11abb08efea6dab09c003305161cb6c9eafd432e35e76e7fa9b90c"},
{file = "pyarrow-18.1.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:36ac22d7782554754a3b50201b607d553a8d71b78cdf03b33c1125be4b52397c"},
{file = "pyarrow-18.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:25dbacab8c5952df0ca6ca0af28f50d45bd31c1ff6fcf79e2d120b4a65ee7181"},
{file = "pyarrow-18.1.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6a276190309aba7bc9d5bd2933230458b3521a4317acfefe69a354f2fe59f2bc"},
{file = "pyarrow-18.1.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:ad514dbfcffe30124ce655d72771ae070f30bf850b48bc4d9d3b25993ee0e386"},
{file = "pyarrow-18.1.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aebc13a11ed3032d8dd6e7171eb6e86d40d67a5639d96c35142bd568b9299324"},
{file = "pyarrow-18.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6cf5c05f3cee251d80e98726b5c7cc9f21bab9e9783673bac58e6dfab57ecc8"},
{file = "pyarrow-18.1.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:11b676cd410cf162d3f6a70b43fb9e1e40affbc542a1e9ed3681895f2962d3d9"},
{file = "pyarrow-18.1.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:b76130d835261b38f14fc41fdfb39ad8d672afb84c447126b84d5472244cfaba"},
{file = "pyarrow-18.1.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:0b331e477e40f07238adc7ba7469c36b908f07c89b95dd4bd3a0ec84a3d1e21e"},
{file = "pyarrow-18.1.0-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:2c4dd0c9010a25ba03e198fe743b1cc03cd33c08190afff371749c52ccbbaf76"},
{file = "pyarrow-18.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f97b31b4c4e21ff58c6f330235ff893cc81e23da081b1a4b1c982075e0ed4e9"},
{file = "pyarrow-18.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a4813cb8ecf1809871fd2d64a8eff740a1bd3691bbe55f01a3cf6c5ec869754"},
{file = "pyarrow-18.1.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:05a5636ec3eb5cc2a36c6edb534a38ef57b2ab127292a716d00eabb887835f1e"},
{file = "pyarrow-18.1.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:73eeed32e724ea3568bb06161cad5fa7751e45bc2228e33dcb10c614044165c7"},
{file = "pyarrow-18.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:a1880dd6772b685e803011a6b43a230c23b566859a6e0c9a276c1e0faf4f4052"},
{file = "pyarrow-18.1.0.tar.gz", hash = "sha256:9386d3ca9c145b5539a1cfc75df07757dff870168c959b473a0bccbc3abc8c73"},
]
[package.dependencies]
numpy = ">=1.16.6"
[package.extras]
test = ["cffi", "hypothesis", "pandas", "pytest", "pytz"]
[[package]]
name = "pyarrow"
version = "22.0.0"
description = "Python library for Apache Arrow"
optional = false
python-versions = ">=3.10"
groups = ["powermon"]
markers = "python_version == \"3.14\""
files = [
{file = "pyarrow-22.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:77718810bd3066158db1e95a63c160ad7ce08c6b0710bc656055033e39cdad88"},
{file = "pyarrow-22.0.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:44d2d26cda26d18f7af7db71453b7b783788322d756e81730acb98f24eb90ace"},
{file = "pyarrow-22.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:b9d71701ce97c95480fecb0039ec5bb889e75f110da72005743451339262f4ce"},
{file = "pyarrow-22.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:710624ab925dc2b05a6229d47f6f0dac1c1155e6ed559be7109f684eba048a48"},
{file = "pyarrow-22.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f963ba8c3b0199f9d6b794c90ec77545e05eadc83973897a4523c9e8d84e9340"},
{file = "pyarrow-22.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bd0d42297ace400d8febe55f13fdf46e86754842b860c978dfec16f081e5c653"},
{file = "pyarrow-22.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:00626d9dc0f5ef3a75fe63fd68b9c7c8302d2b5bbc7f74ecaedba83447a24f84"},
{file = "pyarrow-22.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:3e294c5eadfb93d78b0763e859a0c16d4051fc1c5231ae8956d61cb0b5666f5a"},
{file = "pyarrow-22.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:69763ab2445f632d90b504a815a2a033f74332997052b721002298ed6de40f2e"},
{file = "pyarrow-22.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:b41f37cabfe2463232684de44bad753d6be08a7a072f6a83447eeaf0e4d2a215"},
{file = "pyarrow-22.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:35ad0f0378c9359b3f297299c3309778bb03b8612f987399a0333a560b43862d"},
{file = "pyarrow-22.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8382ad21458075c2e66a82a29d650f963ce51c7708c7c0ff313a8c206c4fd5e8"},
{file = "pyarrow-22.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1a812a5b727bc09c3d7ea072c4eebf657c2f7066155506ba31ebf4792f88f016"},
{file = "pyarrow-22.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:ec5d40dd494882704fb876c16fa7261a69791e784ae34e6b5992e977bd2e238c"},
{file = "pyarrow-22.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:bea79263d55c24a32b0d79c00a1c58bb2ee5f0757ed95656b01c0fb310c5af3d"},
{file = "pyarrow-22.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:12fe549c9b10ac98c91cf791d2945e878875d95508e1a5d14091a7aaa66d9cf8"},
{file = "pyarrow-22.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:334f900ff08ce0423407af97e6c26ad5d4e3b0763645559ece6fbf3747d6a8f5"},
{file = "pyarrow-22.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c6c791b09c57ed76a18b03f2631753a4960eefbbca80f846da8baefc6491fcfe"},
{file = "pyarrow-22.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c3200cb41cdbc65156e5f8c908d739b0dfed57e890329413da2748d1a2cd1a4e"},
{file = "pyarrow-22.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ac93252226cf288753d8b46280f4edf3433bf9508b6977f8dd8526b521a1bbb9"},
{file = "pyarrow-22.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:44729980b6c50a5f2bfcc2668d36c569ce17f8b17bccaf470c4313dcbbf13c9d"},
{file = "pyarrow-22.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e6e95176209257803a8b3d0394f21604e796dadb643d2f7ca21b66c9c0b30c9a"},
{file = "pyarrow-22.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:001ea83a58024818826a9e3f89bf9310a114f7e26dfe404a4c32686f97bd7901"},
{file = "pyarrow-22.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:ce20fe000754f477c8a9125543f1936ea5b8867c5406757c224d745ed033e691"},
{file = "pyarrow-22.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e0a15757fccb38c410947df156f9749ae4a3c89b2393741a50521f39a8cf202a"},
{file = "pyarrow-22.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cedb9dd9358e4ea1d9bce3665ce0797f6adf97ff142c8e25b46ba9cdd508e9b6"},
{file = "pyarrow-22.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:252be4a05f9d9185bb8c18e83764ebcfea7185076c07a7a662253af3a8c07941"},
{file = "pyarrow-22.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:a4893d31e5ef780b6edcaf63122df0f8d321088bb0dee4c8c06eccb1ca28d145"},
{file = "pyarrow-22.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:f7fe3dbe871294ba70d789be16b6e7e52b418311e166e0e3cba9522f0f437fb1"},
{file = "pyarrow-22.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:ba95112d15fd4f1105fb2402c4eab9068f0554435e9b7085924bcfaac2cc306f"},
{file = "pyarrow-22.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c064e28361c05d72eed8e744c9605cbd6d2bb7481a511c74071fd9b24bc65d7d"},
{file = "pyarrow-22.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:6f9762274496c244d951c819348afbcf212714902742225f649cf02823a6a10f"},
{file = "pyarrow-22.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a9d9ffdc2ab696f6b15b4d1f7cec6658e1d788124418cb30030afbae31c64746"},
{file = "pyarrow-22.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ec1a15968a9d80da01e1d30349b2b0d7cc91e96588ee324ce1b5228175043e95"},
{file = "pyarrow-22.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:bba208d9c7decf9961998edf5c65e3ea4355d5818dd6cd0f6809bec1afb951cc"},
{file = "pyarrow-22.0.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:9bddc2cade6561f6820d4cd73f99a0243532ad506bc510a75a5a65a522b2d74d"},
{file = "pyarrow-22.0.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:e70ff90c64419709d38c8932ea9fe1cc98415c4f87ea8da81719e43f02534bc9"},
{file = "pyarrow-22.0.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:92843c305330aa94a36e706c16209cd4df274693e777ca47112617db7d0ef3d7"},
{file = "pyarrow-22.0.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:6dda1ddac033d27421c20d7a7943eec60be44e0db4e079f33cc5af3b8280ccde"},
{file = "pyarrow-22.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:84378110dd9a6c06323b41b56e129c504d157d1a983ce8f5443761eb5256bafc"},
{file = "pyarrow-22.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:854794239111d2b88b40b6ef92aa478024d1e5074f364033e73e21e3f76b25e0"},
{file = "pyarrow-22.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:b883fe6fd85adad7932b3271c38ac289c65b7337c2c132e9569f9d3940620730"},
{file = "pyarrow-22.0.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7a820d8ae11facf32585507c11f04e3f38343c1e784c9b5a8b1da5c930547fe2"},
{file = "pyarrow-22.0.0-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:c6ec3675d98915bf1ec8b3c7986422682f7232ea76cad276f4c8abd5b7319b70"},
{file = "pyarrow-22.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:3e739edd001b04f654b166204fc7a9de896cf6007eaff33409ee9e50ceaff754"},
{file = "pyarrow-22.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:7388ac685cab5b279a41dfe0a6ccd99e4dbf322edfb63e02fc0443bf24134e91"},
{file = "pyarrow-22.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f633074f36dbc33d5c05b5dc75371e5660f1dbf9c8b1d95669def05e5425989c"},
{file = "pyarrow-22.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4c19236ae2402a8663a2c8f21f1870a03cc57f0bef7e4b6eb3238cc82944de80"},
{file = "pyarrow-22.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0c34fe18094686194f204a3b1787a27456897d8a2d62caf84b61e8dfbc0252ae"},
{file = "pyarrow-22.0.0.tar.gz", hash = "sha256:3d600dc583260d845c7d8a6db540339dd883081925da2bd1c5cb808f720b3cd9"},
]
[[package]]
name = "pyarrow-stubs"
@@ -4101,14 +4303,14 @@ files = [
[[package]]
name = "pygments"
version = "2.19.2"
version = "2.20.0"
description = "Pygments is a syntax highlighting package written in Python."
optional = false
python-versions = ">=3.8"
python-versions = ">=3.9"
groups = ["analysis", "dev"]
files = [
{file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"},
{file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"},
{file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"},
{file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"},
]
[package.extras]
@@ -4740,6 +4942,7 @@ description = "Python HTTP for Humans."
optional = false
python-versions = ">=3.9"
groups = ["main", "analysis"]
markers = "python_version < \"3.11\""
files = [
{file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"},
{file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"},
@@ -4755,6 +4958,29 @@ urllib3 = ">=1.21.1,<3"
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
[[package]]
name = "requests"
version = "2.34.2"
description = "Python HTTP for Humans."
optional = false
python-versions = ">=3.10"
groups = ["main", "analysis"]
markers = "python_version >= \"3.11\""
files = [
{file = "requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0"},
{file = "requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed"},
]
[package.dependencies]
certifi = ">=2023.5.7"
charset_normalizer = ">=2,<4"
idna = ">=2.5,<4"
urllib3 = ">=1.26,<3"
[package.extras]
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use-chardet-on-py3 = ["chardet (>=3.0.2,<8)"]
[[package]]
name = "retrying"
version = "1.4.2"
@@ -5145,25 +5371,25 @@ win32 = ["pywin32 ; sys_platform == \"win32\""]
[[package]]
name = "setuptools"
version = "80.9.0"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
version = "82.0.1"
description = "Most extensible Python build backend with support for C/C++ extension modules"
optional = false
python-versions = ">=3.9"
groups = ["main", "analysis", "dev"]
files = [
{file = "setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922"},
{file = "setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"},
{file = "setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb"},
{file = "setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9"},
]
markers = {main = "extra == \"analysis\""}
[package.extras]
check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""]
core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"]
check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.13.0) ; sys_platform != \"cygwin\""]
core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"]
cover = ["pytest-cov"]
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"]
enabler = ["pytest-enabler (>=2.2)"]
test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"]
type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"]
type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.18.*)", "pytest-mypy"]
[[package]]
name = "six"
@@ -5357,24 +5583,22 @@ files = [
[[package]]
name = "tornado"
version = "6.5.2"
version = "6.5.6"
description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed."
optional = false
python-versions = ">=3.9"
groups = ["analysis"]
files = [
{file = "tornado-6.5.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:2436822940d37cde62771cff8774f4f00b3c8024fe482e16ca8387b8a2724db6"},
{file = "tornado-6.5.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:583a52c7aa94ee046854ba81d9ebb6c81ec0fd30386d96f7640c96dad45a03ef"},
{file = "tornado-6.5.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0fe179f28d597deab2842b86ed4060deec7388f1fd9c1b4a41adf8af058907e"},
{file = "tornado-6.5.2-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b186e85d1e3536d69583d2298423744740986018e393d0321df7340e71898882"},
{file = "tornado-6.5.2-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e792706668c87709709c18b353da1f7662317b563ff69f00bab83595940c7108"},
{file = "tornado-6.5.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:06ceb1300fd70cb20e43b1ad8aaee0266e69e7ced38fa910ad2e03285009ce7c"},
{file = "tornado-6.5.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:74db443e0f5251be86cbf37929f84d8c20c27a355dd452a5cfa2aada0d001ec4"},
{file = "tornado-6.5.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b5e735ab2889d7ed33b32a459cac490eda71a1ba6857b0118de476ab6c366c04"},
{file = "tornado-6.5.2-cp39-abi3-win32.whl", hash = "sha256:c6f29e94d9b37a95013bb669616352ddb82e3bfe8326fccee50583caebc8a5f0"},
{file = "tornado-6.5.2-cp39-abi3-win_amd64.whl", hash = "sha256:e56a5af51cc30dd2cae649429af65ca2f6571da29504a07995175df14c18f35f"},
{file = "tornado-6.5.2-cp39-abi3-win_arm64.whl", hash = "sha256:d6c33dc3672e3a1f3618eb63b7ef4683a7688e7b9e6e8f0d9aa5726360a004af"},
{file = "tornado-6.5.2.tar.gz", hash = "sha256:ab53c8f9a0fa351e2c0741284e06c7a45da86afb544133201c5cc8578eb076a0"},
{file = "tornado-6.5.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:65fcfaafb079435c2c19dc9e07c0f1cf0fa9051759ed0a7d0a3ba7ea7f64919c"},
{file = "tornado-6.5.6-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:38bc01b4acacded2de63ae78023548e41ebe6fbed3ec05a796d7ae3ad893887e"},
{file = "tornado-6.5.6-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b942e6a137fda31ff54bf8e6e2c8d1c37f1f50583f3ed53fb840b53b9601d104"},
{file = "tornado-6.5.6-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8666946e70171b8c3f1fc9b7876fac492e84822c4c7f3746f4e8f8bc9ac92a79"},
{file = "tornado-6.5.6-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1c34cfab7ad6d104f052f55de06d39bbafc5885cfeb4da688803308dbcfa90b7"},
{file = "tornado-6.5.6-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:385f35e4e22fb52551dfcda4cdc8c30c61c2c001aef5ddad99cdfe116952efd3"},
{file = "tornado-6.5.6-cp39-abi3-win32.whl", hash = "sha256:db475f1b67b2809b10bb16264829087724ca8d24fe4ed47f7b8675cae453ef86"},
{file = "tornado-6.5.6-cp39-abi3-win_amd64.whl", hash = "sha256:6739bf1e8eb09230f1280ddbd3236f0309db70f2c551a8dbc40f62babdf82f79"},
{file = "tornado-6.5.6-cp39-abi3-win_arm64.whl", hash = "sha256:2543597b24a695d72338a9a77818362d72387c03ae173f1f169eadc5c91466ac"},
{file = "tornado-6.5.6.tar.gz", hash = "sha256:9a365179fe8ff6b8766f602c0f67c185d778193e9bdd828b19f0b6ed7764177d"},
]
[[package]]
@@ -5512,21 +5736,41 @@ dev = ["flake8", "flake8-annotations", "flake8-bandit", "flake8-bugbear", "flake
[[package]]
name = "urllib3"
version = "2.5.0"
version = "2.6.3"
description = "HTTP library with thread-safe connection pooling, file post, and more."
optional = false
python-versions = ">=3.9"
groups = ["main", "analysis", "dev"]
markers = "python_version < \"3.11\""
files = [
{file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"},
{file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"},
{file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"},
{file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"},
]
[package.extras]
brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""]
brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""]
h2 = ["h2 (>=4,<5)"]
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
zstd = ["zstandard (>=0.18.0)"]
zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""]
[[package]]
name = "urllib3"
version = "2.7.0"
description = "HTTP library with thread-safe connection pooling, file post, and more."
optional = false
python-versions = ">=3.10"
groups = ["main", "analysis", "dev"]
markers = "python_version >= \"3.11\""
files = [
{file = "urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897"},
{file = "urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c"},
]
[package.extras]
brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""]
h2 = ["h2 (>=4,<5)"]
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""]
[[package]]
name = "wcwidth"
@@ -5941,4 +6185,4 @@ tunnel = ["pytap2"]
[metadata]
lock-version = "2.1"
python-versions = "^3.9,<3.15"
content-hash = "e87e2eaffca4ad13aa7e1b8622ec4b37b23a4efe1f4febe0ca87b92db5fe6d1e"
content-hash = "7dd0dc28e13f7569f1a8cc28453d5983a6f742790d9328bafe86d1a2914d88ad"
+6 -3
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "meshtastic"
version = "2.7.5"
version = "2.7.11"
description = "Python API & client shell for talking to Meshtastic devices"
authors = ["Meshtastic Developers <contact@meshtastic.org>"]
license = "GPL-3.0-only"
@@ -15,7 +15,7 @@ requests = "^2.31.0"
pyyaml = "^6.0.1"
pypubsub = "^4.0.3"
bleak = ">=0.22.3"
packaging = "^24.0"
packaging = ">=24.0"
argcomplete = { version = "^3.5.2", optional = true }
pyqrcode = { version = "^1.2.1", optional = true }
dotmap = { version = "^1.3.30", optional = true }
@@ -51,7 +51,10 @@ optional = true
riden = { git = "https://github.com/geeksville/riden.git#1.2.1" }
ppk2-api = "^0.9.2"
parse = "^1.20.2"
pyarrow = "^16.1.0"
pyarrow = [
{ version = "^18.0.0", python = "<3.14" },
{ version = "^22.0.0", python = ">=3.14" }
]
platformdirs = "^4.2.2"
# If you are doing power analysis you might want these extra devtools
+2 -1
View File
@@ -1,6 +1,6 @@
[pytest]
addopts = -m "not int and not smoke1 and not smoke2 and not smokewifi and not examples and not smokevirt"
addopts = -m "not int and not smoke1 and not smoke2 and not smokewifi and not examples and not smokevirt and not smokemesh"
filterwarnings =
ignore::DeprecationWarning
@@ -13,4 +13,5 @@ markers =
smoke1: runs smoke tests on a single device connected via USB
smoke2: runs smoke tests on a two devices connected via USB
smokewifi: runs smoke test on an esp32 device setup with wifi
smokemesh: runs smoke tests against multiple meshtasticd sim instances
examples: runs the examples tests which validates the library