Compare commits

..

66 Commits

Author SHA1 Message Date
Ian McEwen
5c703aff1d Merge pull request #591 from ianmcorvidae/device-metadata-resp
Wait for response with --device-metadata. Fixes #527
2024-06-08 07:29:45 -07:00
Ian McEwen
5441266565 Wait for response with --device-metadata. Fixes #527 2024-06-08 07:26:20 -07:00
Ian McEwen
890557fa5d Merge branch 'more-tests' 2024-06-05 19:58:03 -07:00
Ian McEwen
e27d210a71 Test with --dest on setlat/remove-position for the error/exit case 2024-06-05 19:57:40 -07:00
Ian McEwen
16c08b8b47 Add simple --remove-position test 2024-06-05 19:54:12 -07:00
Ian McEwen
ebd3c7f5e8 Add test for fromStr base64 branch 2024-06-05 19:48:58 -07:00
Ian McEwen
da0312a5b0 more miscellaneous types 2024-06-05 19:44:18 -07:00
Ian McEwen
919ae8c40f make pylint happy, again 2024-06-05 19:32:45 -07:00
Ian McEwen
dd4fccbc77 Add a fairly simple property-based test as a starting point 2024-06-05 19:29:55 -07:00
Ian McEwen
32682b5230 Merge pull request #589 from ianmcorvidae/nodeless-startup
Allow a faster nodedb-less startup on 2.3.11+ with `--no-nodes`
2024-06-05 18:59:18 -07:00
Ian McEwen
9dab76bb64 quell pylint 2024-06-05 18:56:19 -07:00
Ian McEwen
e6d61c6603 Allow a faster nodedb-less startup on 2.3.11+ with --no-nodes and the magic value from meshtastic/firmware#3949 2024-06-05 18:52:35 -07:00
Ian McEwen
ee857c5128 Merge pull request #588 from nerdenator/quick-coverage
quick-coverage: simple test case just to cover uncovered code.
2024-06-03 23:22:33 -07:00
Nerdenator
87a4bb0888 quick-coverage: fixing linting issues. 2024-06-04 01:15:10 -05:00
Nerdenator
d72cc0e201 quick-coverage: simple test case just to cover uncovered code. 2024-06-04 01:01:32 -05:00
Ian McEwen
b350b9eab9 Update the main module docstring to be a bit more accurate, at least 2024-06-01 23:03:56 -07:00
Ian McEwen
dc112f2f3a protobufs: v2.3.11 2024-06-01 12:04:01 -07:00
github-actions
14ae4eeac1 bump version 2024-06-01 18:47:51 +00:00
Ian McEwen
bbc526d0a8 Merge pull request #584 from ianmcorvidae/improve-fixed-position
Use new fixed position admin messages and add `--remove-position` argument
2024-06-01 00:44:17 -07:00
Ian McEwen
abe98f5079 Merge pull request #585 from ianmcorvidae/position-rounding
Fix rounding of position values when converting from integer to float in _fixupPosition
2024-06-01 00:42:58 -07:00
Ian McEwen
e8dfee8454 Fix rounding of position values when converting from integer to float in _fixupPosition. Fixes #572 2024-05-31 18:57:30 -07:00
Ian McEwen
1746ad15d7 Use new fixed position admin messages and add --remove-position argument. Fixes #525 2024-05-31 18:44:33 -07:00
Ian McEwen
4d67e7fc76 Fix up/add some more types 2024-05-30 17:51:42 -07:00
Ian McEwen
3b112d2f49 Merge pull request #583 from ianmcorvidae/update-unknown-node-setup
Initialize unknown nodes more in line with meshtastic/design#16; show hardware in --nodes
2024-05-30 15:52:16 -07:00
Ian McEwen
93e9c1c66c Initialize unknown nodes more in line with meshtastic/design#16 2024-05-30 13:50:52 -07:00
Ian McEwen
8e641b3186 Merge pull request #581 from 868meshbot/868meshbot-fix-ignore-incoming
Fix the ignore_incoming management BUG 568
2024-05-26 00:03:11 -07:00
Ian McEwen
ed545cd9b4 Merge pull request #580 from todd-herbert/wait-to-disconnect
Add "wait to disconnect" argument
2024-05-26 00:02:08 -07:00
868meshbot
bcd60c9ef7 Update __main__.py
Simple patch to fix the ignore_incoming management aka
https://github.com/meshtastic/python/issues/568
2024-05-25 16:28:16 +01:00
Todd Herbert
c3d044e3f2 Optional pause before disconnecting 2024-05-24 17:10:07 +12:00
Ian McEwen
8d538e8f24 protobufs: v2.3.10 2024-05-18 12:56:41 -07:00
github-actions
fa1a3d7901 bump version 2024-05-18 00:15:11 +00:00
Ian McEwen
3adeb925dd tweak the roadmap in README a little 2024-05-14 16:05:21 -07:00
Ian McEwen
9de04a12e1 Merge pull request #574 from ianmcorvidae/warn-enable-disable
add a warning on --ch-enable and --ch-disable, which should usually be avoided
2024-05-13 21:30:01 -07:00
Ian McEwen
e01a1bb6e0 add a warning on --ch-enable and --ch-disable, which should usually be avoided 2024-05-13 21:27:53 -07:00
Ian McEwen
1ebea53703 make pylint happy with import order 2024-05-11 22:28:50 -07:00
Ian McEwen
ba2d6c9d93 continue progressively typing things (SerialInterface/BLEInterface initializations) 2024-05-11 22:27:31 -07:00
Ian McEwen
fef0e1b77f Remove an unuseful/inaccurate comment 2024-05-10 20:17:48 -07:00
Ian McEwen
dc1dec17b5 protobufs: v2.3.9 2024-05-10 14:56:07 -07:00
Ian McEwen
02ed17837b Improve FromRadio handling, publish messages for client proxy/xmodem stuff tentatively 2024-05-10 14:54:32 -07:00
Ian McEwen
8cf996a242 Merge pull request #564 from jhollowe-forks/fix-help-typo
Fix typo in __main__.py
2024-05-02 19:04:08 -07:00
John Hollowell
02189aae07 Fix typo in __main__.py 2024-05-02 21:51:09 -04:00
Ian McEwen
894581ce00 Merge pull request #563 from ianmcorvidae/show-channel-0
Print 0 for channel rather than N/A, since we should probably always have a channel
2024-05-02 18:29:00 -07:00
Ian McEwen
fb8db01427 Print 0 for channel rather than N/A, since we should probably always have a channel. Fixes #562 2024-05-02 18:22:39 -07:00
github-actions
fa5ede93ed bump version 2024-05-01 16:42:38 +00:00
Ian McEwen
022a8a1017 protobufs: v2.3.8 2024-05-01 09:40:04 -07:00
Ian McEwen
21f6e25ab0 Merge pull request #561 from ianmcorvidae/use-heartbeat
Use the new Heartbeat payload variant for the heartbeat instead of an empty ToRadio packet
2024-04-29 15:18:23 -07:00
Ian McEwen
243e297505 Use the new Heartbeat payload variant for the heartbeat instead of an empty ToRadio packet 2024-04-29 15:16:04 -07:00
Ian McEwen
9fc86f9450 Merge pull request #558 from geeksville/pr-fixjson
Pretty indent --info JSON output (see below for details)
2024-04-28 21:40:43 -07:00
Ian McEwen
e5999f5be6 Merge pull request #560 from geeksville/pr-fixchannel
(high-pri fix) Update python client to use the 'modern' meshtastic protocol init flow
2024-04-28 19:53:04 -07:00
geeksville
3886bc16d7 Update python client to use the 'modern' meshtastic protocol init flow
Some time ago I updated the protocol so that after sending NODEINFOs to
client devices (via fromRadio protobufs), it would proactively send the
channel definitions.  Prior to this the client had to ask for each channel
by sending an admin request.  This handshaking was really slow for mobile/
bluetooth clients.  So in the current device code (last couple of years)
the device automatically sends this info to the client during initial config
download.

Unfortunately I never updated the python client to expect this.  I assumed
(incorrectly) that it would just cope and keep sending the channel requests
the old way it always had (which is still supported by devices - and
required for remote administration of nodes).

This change removes sending channel requests (for the local node only)
from the python startup code.  It also now understands (no longer ignoring)
the channels which were sent proactively by the local device.

This makes connection/config/api operations from the python client 100%
reliable again.  Previously it would only work sometimes depending on how
quickly it was able to get the local node db downloaded.

Also this flow is much faster - which should help all users of the python
api.
2024-04-27 11:59:17 -07:00
geeksville
7fe98bc266 Pretty indent --info JSON output (see below for details)
Changes to make --info much more human readable (while still keeping
machine readabilty for anyone foolish enough to be parsing the existing
output as text)

* change message_to_json to optionally not strip the multiline JSON
* use multiline=True for the two places we are printing to the console
* make the node list JSON indented
2024-04-26 15:15:57 -07:00
github-actions
2f9307fd99 bump version 2024-04-25 18:33:55 +00:00
Ian McEwen
3d6fa369e8 Merge pull request #556 from ianmcorvidae/enter-dfu
Add --enter-dfu for entering DFU mode on NRF52 devices via admin message
2024-04-25 11:29:09 -07:00
Ian McEwen
6812f508bc Add --enter-dfu for entering DFU mode on NRF52 devices via admin message 2024-04-25 11:21:27 -07:00
Ian McEwen
158cac6b0b Merge pull request #555 from ianmcorvidae/channel-indices
Better support --ch-index for other commands (traceroute, telemetry, position)
2024-04-24 19:35:44 -07:00
Ian McEwen
4d10b6e1bd Better support --ch-index for other commands (traceroute, telemetry, position) 2024-04-24 17:18:17 -07:00
Ian McEwen
bb6e3b7a49 protobufs: v2.3.7 2024-04-23 21:35:15 -07:00
Ian McEwen
8b29dc683a Merge pull request #554 from GUVWAF/storeForward
Add missing StoreForward config from radio
2024-04-22 11:14:21 -07:00
GUVWAF
d4156d78c6 Add missing StoreForward config from radio 2024-04-22 20:09:30 +02:00
Ian McEwen
c52977781e Merge pull request #553 from ianmcorvidae/more-dest-not-allowed
Disallow --dest in remaining spots that don't actually use/allow it
2024-04-21 13:03:58 -07:00
Ian McEwen
5ebc8e6f95 Don't allow --dest with --tunnel either 2024-04-21 10:38:01 -07:00
Ian McEwen
56d1dcafe5 --dest doesn't work with --export-config 2024-04-21 10:35:59 -07:00
Ian McEwen
393c765557 Fix up tests 2024-04-21 10:32:07 -07:00
Ian McEwen
243b310eb2 Allow generating QR codes for remote nodes and for all channels 2024-04-21 09:41:07 -07:00
Ian McEwen
cd1ecab4e0 Don't allow --dest with --setlat/lon/alt. Fixes #545 2024-04-21 09:38:32 -07:00
github-actions
c710953b85 bump version 2024-04-20 17:50:14 +00:00
33 changed files with 757 additions and 303 deletions

1
.gitignore vendored
View File

@@ -15,3 +15,4 @@ venv/
__pycache__ __pycache__
examples/__pycache__ examples/__pycache__
meshtastic.spec meshtastic.spec
.hypothesis/

View File

@@ -28,8 +28,7 @@ If you're interested in contributing but don't have specific things you'd like t
This should always be considered a list in progress and flux -- inclusion doesn't guarantee implementation, and exclusion doesn't mean something's not wanted. GitHub issues are a great place to discuss ideas. This should always be considered a list in progress and flux -- inclusion doesn't guarantee implementation, and exclusion doesn't mean something's not wanted. GitHub issues are a great place to discuss ideas.
* Types * Types
* type annotations throughout the codebase * type annotations throughout the codebase, and upgrading mypy running in CI to `--strict`
* mypy running in CI to type-check new code
* async-friendliness * async-friendliness
* CLI completeness & consistency * CLI completeness & consistency
* the CLI should support all features of the firmware * the CLI should support all features of the firmware
@@ -40,9 +39,9 @@ This should always be considered a list in progress and flux -- inclusion doesn'
* pubsub events should be documented clearly * pubsub events should be documented clearly
* helpers for third-party code * helpers for third-party code
* it should be easy to write a script that supports similar options to the CLI so many tools support the same ways of connecting to nodes * it should be easy to write a script that supports similar options to the CLI so many tools support the same ways of connecting to nodes
* interactive client
* data storage & processing * data storage & processing
* there should be a standardized way of recording packets for later use, debugging, etc. * there should be a standardized way of recording packets for later use, debugging, etc.
* a persistence layer could also keep track of nodes beyond nodedb, as the apps do
* a sqlite database schema and tools for writing to it may be a good starting point * a sqlite database schema and tools for writing to it may be a good starting point
* enable maps, charts, visualizations * enable maps, charts, visualizations

View File

@@ -1,18 +1,22 @@
""" """
# an API for Meshtastic devices # A library for the Meshtastic Client API
Primary class: SerialInterface Primary interfaces: SerialInterface, TCPInterface, BLEInterface
Install with pip: "[pip3 install meshtastic](https://pypi.org/project/meshtastic/)" Install with pip: "[pip3 install meshtastic](https://pypi.org/project/meshtastic/)"
Source code on [github](https://github.com/meshtastic/python) Source code on [github](https://github.com/meshtastic/python)
properties of SerialInterface: notable properties of interface classes:
- localConfig - Current radio configuration and device settings, if you write to this the new settings will be applied to
the device.
- nodes - The database of received nodes. Includes always up-to-date location and username information for each - nodes - The database of received nodes. Includes always up-to-date location and username information for each
node in the mesh. This is a read-only datastructure. node in the mesh. This is a read-only datastructure.
- nodesByNum - like "nodes" but keyed by nodeNum instead of nodeId - nodesByNum - like "nodes" but keyed by nodeNum instead of nodeId
- myInfo - Contains read-only information about the local radio device (software version, hardware version, etc) - myInfo & metadata - Contain read-only information about the local radio device (software version, hardware version, etc)
- localNode - Pointer to a node object for the local node
notable properties of nodes:
- localConfig - Current radio settings, can be written to the radio with the `writeConfig` method.
- moduleConfig - Current module settings, can be written to the radio with the `writeConfig` method.
- channels - The node's channels, keyed by index.
# Published PubSub topics # Published PubSub topics
@@ -113,6 +117,9 @@ OUR_APP_VERSION = 20300
format is Mmmss (where M is 1+the numeric major number. i.e. 20120 means 1.1.20 format is Mmmss (where M is 1+the numeric major number. i.e. 20120 means 1.1.20
""" """
NODELESS_WANT_CONFIG_ID = 69420
"""A special thing to pass for want_config_id that instructs nodes to skip sending nodeinfos other than its own."""
publishingThread = DeferredExecution("publishing") publishingThread = DeferredExecution("publishing")

View File

@@ -20,6 +20,7 @@ from meshtastic import mt_config
from meshtastic import channel_pb2, config_pb2, portnums_pb2, remote_hardware, BROADCAST_ADDR from meshtastic import channel_pb2, config_pb2, portnums_pb2, remote_hardware, BROADCAST_ADDR
from meshtastic.version import get_active_version from meshtastic.version import get_active_version
from meshtastic.ble_interface import BLEInterface from meshtastic.ble_interface import BLEInterface
from meshtastic.mesh_interface import MeshInterface
def onReceive(packet, interface): def onReceive(packet, interface):
"""Callback invoked when a packet arrives""" """Callback invoked when a packet arrives"""
@@ -56,6 +57,11 @@ def onConnection(interface, topic=pub.AUTO_TOPIC): # pylint: disable=W0613
"""Callback invoked when we connect/disconnect from a radio""" """Callback invoked when we connect/disconnect from a radio"""
print(f"Connection changed: {topic.getName()}") print(f"Connection changed: {topic.getName()}")
def checkChannel(interface: MeshInterface, channelIndex: int) -> bool:
"""Given an interface and channel index, return True if that channel is non-disabled on the local node"""
ch = interface.localNode.getChannelByChannelIndex(channelIndex)
logging.debug(f"ch:{ch}")
return (ch and ch.role != channel_pb2.Channel.Role.DISABLED)
def getPref(node, comp_name): def getPref(node, comp_name):
"""Get a channel or preferences value""" """Get a channel or preferences value"""
@@ -220,13 +226,14 @@ def setPref(config, comp_name, valStr) -> bool:
config_values = getattr(config_part, config_type.name) config_values = getattr(config_part, config_type.name)
setattr(config_values, pref.name, valStr) setattr(config_values, pref.name, valStr)
else: else:
config_values = getattr(config, config_type.name)
if val == 0: if val == 0:
# clear values # clear values
print("Clearing ignore_incoming list") print("Clearing ignore_incoming list")
del config_type.message_type.ignore_incoming[:] del config_values.ignore_incoming[:]
else: else:
print(f"Adding '{val}' to the ignore_incoming list") print(f"Adding '{val}' to the ignore_incoming list")
config_type.message_type.ignore_incoming.extend([val]) config_values.ignore_incoming.extend([int(valStr)])
prefix = f"{'.'.join(name[0:-1])}." if config_type.message_type is not None else "" prefix = f"{'.'.join(name[0:-1])}." if config_type.message_type is not None else ""
if mt_config.camel_case: if mt_config.camel_case:
@@ -250,31 +257,41 @@ def onConnected(interface):
if not args.export_config: if not args.export_config:
print("Connected to radio") print("Connected to radio")
if args.setlat or args.setlon or args.setalt: if args.remove_position:
if args.dest != BROADCAST_ADDR:
print("Setting positions of remote nodes is not supported.")
return
closeNow = True
print("Removing fixed position and disabling fixed position setting")
interface.localNode.removeFixedPosition()
elif args.setlat or args.setlon or args.setalt:
if args.dest != BROADCAST_ADDR:
print("Setting latitude, longitude, and altitude of remote nodes is not supported.")
return
closeNow = True closeNow = True
alt = 0 alt = 0
lat = 0.0 lat = 0
lon = 0.0 lon = 0
# TODO: use getNode(args.dest) to be able to set it for a remote node
localConfig = interface.localNode.localConfig
if args.setalt: if args.setalt:
alt = int(args.setalt) alt = int(args.setalt)
localConfig.position.fixed_position = True
print(f"Fixing altitude at {alt} meters") print(f"Fixing altitude at {alt} meters")
if args.setlat: if args.setlat:
lat = float(args.setlat) try:
localConfig.position.fixed_position = True lat = int(args.setlat)
except ValueError:
lat = float(args.setlat)
print(f"Fixing latitude at {lat} degrees") print(f"Fixing latitude at {lat} degrees")
if args.setlon: if args.setlon:
lon = float(args.setlon) try:
localConfig.position.fixed_position = True lon = int(args.setlon)
except ValueError:
lon = float(args.setlon)
print(f"Fixing longitude at {lon} degrees") print(f"Fixing longitude at {lon} degrees")
print("Setting device position") print("Setting device position and enabling fixed position setting")
# can include lat/long/alt etc: latitude = 37.5, longitude = -122.1 # can include lat/long/alt etc: latitude = 37.5, longitude = -122.1
interface.sendPosition(lat, lon, alt) interface.localNode.setFixedPosition(lat, lon, alt)
interface.localNode.writeConfig("position")
elif not args.no_time: elif not args.no_time:
# We normally provide a current time to the mesh when we connect # We normally provide a current time to the mesh when we connect
if interface.localNode.nodeNum in interface.nodesByNum and "position" in interface.nodesByNum[interface.localNode.nodeNum]: if interface.localNode.nodeNum in interface.nodesByNum and "position" in interface.nodesByNum[interface.localNode.nodeNum]:
@@ -366,6 +383,11 @@ def onConnected(interface):
waitForAckNak = True waitForAckNak = True
interface.getNode(args.dest, False).rebootOTA() interface.getNode(args.dest, False).rebootOTA()
if args.enter_dfu:
closeNow = True
waitForAckNak = True
interface.getNode(args.dest, False).enterDFUMode()
if args.shutdown: if args.shutdown:
closeNow = True closeNow = True
waitForAckNak = True waitForAckNak = True
@@ -400,12 +422,8 @@ def onConnected(interface):
if args.sendtext: if args.sendtext:
closeNow = True closeNow = True
channelIndex = 0 channelIndex = mt_config.channel_index or 0
if args.ch_index is not None: if checkChannel(interface, channelIndex):
channelIndex = int(args.ch_index)
ch = interface.localNode.getChannelByChannelIndex(channelIndex)
logging.debug(f"ch:{ch}")
if ch and ch.role != channel_pb2.Channel.Role.DISABLED:
print( print(
f"Sending text message {args.sendtext} to {args.dest} on channelIndex:{channelIndex}" f"Sending text message {args.sendtext} to {args.dest} on channelIndex:{channelIndex}"
) )
@@ -425,22 +443,28 @@ def onConnected(interface):
loraConfig = getattr(interface.localNode.localConfig, "lora") loraConfig = getattr(interface.localNode.localConfig, "lora")
hopLimit = getattr(loraConfig, "hop_limit") hopLimit = getattr(loraConfig, "hop_limit")
dest = str(args.traceroute) dest = str(args.traceroute)
print(f"Sending traceroute request to {dest} (this could take a while)") channelIndex = mt_config.channel_index or 0
interface.sendTraceRoute(dest, hopLimit) if checkChannel(interface, channelIndex):
print(f"Sending traceroute request to {dest} on channelIndex:{channelIndex} (this could take a while)")
interface.sendTraceRoute(dest, hopLimit, channelIndex=channelIndex)
if args.request_telemetry: if args.request_telemetry:
if args.dest == BROADCAST_ADDR: if args.dest == BROADCAST_ADDR:
meshtastic.util.our_exit("Warning: Must use a destination node ID.") meshtastic.util.our_exit("Warning: Must use a destination node ID.")
else: else:
print(f"Sending telemetry request to {args.dest} (this could take a while)") channelIndex = mt_config.channel_index or 0
interface.sendTelemetry(destinationId=args.dest, wantResponse=True) if checkChannel(interface, channelIndex):
print(f"Sending telemetry request to {args.dest} on channelIndex:{channelIndex} (this could take a while)")
interface.sendTelemetry(destinationId=args.dest, wantResponse=True, channelIndex=channelIndex)
if args.request_position: if args.request_position:
if args.dest == BROADCAST_ADDR: if args.dest == BROADCAST_ADDR:
meshtastic.util.our_exit("Warning: Must use a destination node ID.") meshtastic.util.our_exit("Warning: Must use a destination node ID.")
else: else:
print(f"Sending position request to {args.dest} (this could take a while)") channelIndex = mt_config.channel_index or 0
interface.sendPosition(destinationId=args.dest, wantResponse=True) if checkChannel(interface, channelIndex):
print(f"Sending position request to {args.dest} on channelIndex:{channelIndex} (this could take a while)")
interface.sendPosition(destinationId=args.dest, wantResponse=True, channelIndex=channelIndex)
if args.gpio_wrb or args.gpio_rd or args.gpio_watch: if args.gpio_wrb or args.gpio_rd or args.gpio_watch:
if args.dest == BROADCAST_ADDR: if args.dest == BROADCAST_ADDR:
@@ -599,6 +623,9 @@ def onConnected(interface):
print("Writing modified configuration to device") print("Writing modified configuration to device")
if args.export_config: if args.export_config:
if args.dest != BROADCAST_ADDR:
print("Exporting configuration of remote nodes is not supported.")
return
# export the configuration (the opposite of '--configure') # export the configuration (the opposite of '--configure')
closeNow = True closeNow = True
export_config(interface) export_config(interface)
@@ -703,6 +730,10 @@ def onConnected(interface):
ch = interface.getNode(args.dest).channels[channelIndex] ch = interface.getNode(args.dest).channels[channelIndex]
if args.ch_enable or args.ch_disable: if args.ch_enable or args.ch_disable:
print(
"Warning: --ch-enable and --ch-disable can produce noncontiguous channels, "
"which can cause errors in some clients. Whenever possible, use --ch-add and --ch-del instead."
)
if channelIndex == 0: if channelIndex == 0:
meshtastic.util.our_exit( meshtastic.util.our_exit(
"Warning: Cannot enable/disable PRIMARY channel." "Warning: Cannot enable/disable PRIMARY channel."
@@ -801,10 +832,14 @@ def onConnected(interface):
return return
interface.showNodes() interface.showNodes()
if args.qr: if args.qr or args.qr_all:
closeNow = True closeNow = True
url = interface.localNode.getURL(includeAll=False) url = interface.getNode(args.dest, True).getURL(includeAll=args.qr_all)
print(f"Primary channel URL {url}") if args.qr_all:
urldesc = "Complete URL (includes all channels)"
else:
urldesc = "Primary channel URL"
print(f"{urldesc}: {url}")
qr = pyqrcode.create(url) qr = pyqrcode.create(url)
print(qr.terminal()) print(qr.terminal())
@@ -813,6 +848,9 @@ def onConnected(interface):
have_tunnel = platform.system() == "Linux" have_tunnel = platform.system() == "Linux"
if have_tunnel and args.tunnel: if have_tunnel and args.tunnel:
if args.dest != BROADCAST_ADDR:
print("A tunnel can only be created using the local node.")
return
# pylint: disable=C0415 # pylint: disable=C0415
from . import tunnel from . import tunnel
@@ -832,6 +870,10 @@ def onConnected(interface):
) )
interface.getNode(args.dest, False).iface.waitForAckNak() interface.getNode(args.dest, False).iface.waitForAckNak()
if args.wait_to_disconnect:
print(f"Waiting {args.wait_to_disconnect} seconds before disconnecting" )
time.sleep(int(args.wait_to_disconnect))
# if the user didn't ask for serial debugging output, we might want to exit after we've done our operation # if the user didn't ask for serial debugging output, we might want to exit after we've done our operation
if (not args.seriallog) and closeNow: if (not args.seriallog) and closeNow:
interface.close() # after running command then exit interface.close() # after running command then exit
@@ -1008,11 +1050,11 @@ def common():
meshtastic.util.our_exit("BLE scan finished", 0) meshtastic.util.our_exit("BLE scan finished", 0)
return return
elif args.ble: elif args.ble:
client = BLEInterface(args.ble, debugOut=logfile, noProto=args.noproto) client = BLEInterface(args.ble, debugOut=logfile, noProto=args.noproto, noNodes=args.no_nodes)
elif args.host: elif args.host:
try: try:
client = meshtastic.tcp_interface.TCPInterface( client = meshtastic.tcp_interface.TCPInterface(
args.host, debugOut=logfile, noProto=args.noproto args.host, debugOut=logfile, noProto=args.noproto, noNodes=args.no_nodes
) )
except Exception as ex: except Exception as ex:
meshtastic.util.our_exit( meshtastic.util.our_exit(
@@ -1021,7 +1063,7 @@ def common():
else: else:
try: try:
client = meshtastic.serial_interface.SerialInterface( client = meshtastic.serial_interface.SerialInterface(
args.port, debugOut=logfile, noProto=args.noproto args.port, debugOut=logfile, noProto=args.noproto, noNodes=args.no_nodes
) )
except PermissionError as ex: except PermissionError as ex:
username = os.getlogin() username = os.getlogin()
@@ -1036,7 +1078,7 @@ def common():
if client.devPath is None: if client.devPath is None:
try: try:
client = meshtastic.tcp_interface.TCPInterface( client = meshtastic.tcp_interface.TCPInterface(
"localhost", debugOut=logfile, noProto=args.noproto "localhost", debugOut=logfile, noProto=args.noproto, noNodes=args.no_nodes
) )
except Exception as ex: except Exception as ex:
meshtastic.util.our_exit( meshtastic.util.our_exit(
@@ -1154,7 +1196,16 @@ def initParser():
group.add_argument( group.add_argument(
"--qr", "--qr",
help="Display the QR code that corresponds to the current channel", help=(
"Display a QR code for the node's primary channel (or all channels with --qr-all). "
"Also shows the shareable channel URL."
),
action="store_true",
)
group.add_argument(
"--qr-all",
help="Display a QR code and URL for all of the node's channels.",
action="store_true", action="store_true",
) )
@@ -1316,7 +1367,7 @@ def initParser():
group.add_argument( group.add_argument(
"--request-position", "--request-position",
help="Request the position from a nade. " help="Request the position from a node. "
"You need to pass the destination ID as an argument with '--dest'. " "You need to pass the destination ID as an argument with '--dest'. "
"For repeaters, the nodeNum is required.", "For repeaters, the nodeNum is required.",
action="store_true", action="store_true",
@@ -1334,7 +1385,13 @@ def initParser():
group.add_argument( group.add_argument(
"--reboot-ota", "--reboot-ota",
help="Tell the destination node to reboot into factory firmware", help="Tell the destination node to reboot into factory firmware (ESP32)",
action="store_true",
)
group.add_argument(
"--enter-dfu",
help="Tell the destination node to enter DFU mode (NRF52)",
action="store_true", action="store_true",
) )
@@ -1396,12 +1453,32 @@ def initParser():
action="store_true", action="store_true",
) )
group.add_argument("--setalt", help="Set device altitude in meters (allows use without GPS)") group.add_argument(
"--no-nodes",
group.add_argument("--setlat", help="Set device latitude (allows use without GPS)") help="Request that the node not send node info to the client. "
"Will break things that depend on the nodedb, but will speed up startup. Requires 2.3.11+ firmware.",
action="store_true",
)
group.add_argument( group.add_argument(
"--setlon", help="Set device longitude (allows use without GPS)" "--setalt",
help="Set device altitude in meters (allows use without GPS), and enable fixed position.",
)
group.add_argument(
"--setlat",
help="Set device latitude (allows use without GPS), and enable fixed position. Accepts a decimal value or an integer premultiplied by 1e7.",
)
group.add_argument(
"--setlon",
help="Set device longitude (allows use without GPS), and enable fixed position. Accepts a decimal value or an integer premultiplied by 1e7.",
)
group.add_argument(
"--remove-position",
help="Clear any existing fixed position and disable fixed position.",
action="store_true",
) )
group.add_argument( group.add_argument(
@@ -1429,6 +1506,14 @@ def initParser():
action="store_true", action="store_true",
) )
group.add_argument(
"--wait-to-disconnect",
help="How many seconds to wait before disconnecting from the device.",
const="5",
nargs="?",
action="store",
)
group.add_argument( group.add_argument(
"--noproto", "--noproto",
help="Don't start the API, just function as a dumb serial terminal.", help="Don't start the API, just function as a dumb serial terminal.",

View File

@@ -13,7 +13,7 @@ _sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15meshtastic/atak.proto\x12\nmeshtastic\"\xe6\x01\n\tTAKPacket\x12\x15\n\ris_compressed\x18\x01 \x01(\x08\x12$\n\x07\x63ontact\x18\x02 \x01(\x0b\x32\x13.meshtastic.Contact\x12 \n\x05group\x18\x03 \x01(\x0b\x32\x11.meshtastic.Group\x12\"\n\x06status\x18\x04 \x01(\x0b\x32\x12.meshtastic.Status\x12\x1e\n\x03pli\x18\x05 \x01(\x0b\x32\x0f.meshtastic.PLIH\x00\x12#\n\x04\x63hat\x18\x06 \x01(\x0b\x32\x13.meshtastic.GeoChatH\x00\x42\x11\n\x0fpayload_variant\"2\n\x07GeoChat\x12\x0f\n\x07message\x18\x01 \x01(\t\x12\x0f\n\x02to\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\x05\n\x03_to\"M\n\x05Group\x12$\n\x04role\x18\x01 \x01(\x0e\x32\x16.meshtastic.MemberRole\x12\x1e\n\x04team\x18\x02 \x01(\x0e\x32\x10.meshtastic.Team\"\x19\n\x06Status\x12\x0f\n\x07\x62\x61ttery\x18\x01 \x01(\r\"4\n\x07\x43ontact\x12\x10\n\x08\x63\x61llsign\x18\x01 \x01(\t\x12\x17\n\x0f\x64\x65vice_callsign\x18\x02 \x01(\t\"_\n\x03PLI\x12\x12\n\nlatitude_i\x18\x01 \x01(\x0f\x12\x13\n\x0blongitude_i\x18\x02 \x01(\x0f\x12\x10\n\x08\x61ltitude\x18\x03 \x01(\x05\x12\r\n\x05speed\x18\x04 \x01(\r\x12\x0e\n\x06\x63ourse\x18\x05 \x01(\r*\xc0\x01\n\x04Team\x12\x14\n\x10Unspecifed_Color\x10\x00\x12\t\n\x05White\x10\x01\x12\n\n\x06Yellow\x10\x02\x12\n\n\x06Orange\x10\x03\x12\x0b\n\x07Magenta\x10\x04\x12\x07\n\x03Red\x10\x05\x12\n\n\x06Maroon\x10\x06\x12\n\n\x06Purple\x10\x07\x12\r\n\tDark_Blue\x10\x08\x12\x08\n\x04\x42lue\x10\t\x12\x08\n\x04\x43yan\x10\n\x12\x08\n\x04Teal\x10\x0b\x12\t\n\x05Green\x10\x0c\x12\x0e\n\nDark_Green\x10\r\x12\t\n\x05\x42rown\x10\x0e*\x7f\n\nMemberRole\x12\x0e\n\nUnspecifed\x10\x00\x12\x0e\n\nTeamMember\x10\x01\x12\x0c\n\x08TeamLead\x10\x02\x12\x06\n\x02HQ\x10\x03\x12\n\n\x06Sniper\x10\x04\x12\t\n\x05Medic\x10\x05\x12\x13\n\x0f\x46orwardObserver\x10\x06\x12\x07\n\x03RTO\x10\x07\x12\x06\n\x02K9\x10\x08\x42_\n\x13\x63om.geeksville.meshB\nATAKProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3') DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15meshtastic/atak.proto\x12\nmeshtastic\"\xe6\x01\n\tTAKPacket\x12\x15\n\ris_compressed\x18\x01 \x01(\x08\x12$\n\x07\x63ontact\x18\x02 \x01(\x0b\x32\x13.meshtastic.Contact\x12 \n\x05group\x18\x03 \x01(\x0b\x32\x11.meshtastic.Group\x12\"\n\x06status\x18\x04 \x01(\x0b\x32\x12.meshtastic.Status\x12\x1e\n\x03pli\x18\x05 \x01(\x0b\x32\x0f.meshtastic.PLIH\x00\x12#\n\x04\x63hat\x18\x06 \x01(\x0b\x32\x13.meshtastic.GeoChatH\x00\x42\x11\n\x0fpayload_variant\"\\\n\x07GeoChat\x12\x0f\n\x07message\x18\x01 \x01(\t\x12\x0f\n\x02to\x18\x02 \x01(\tH\x00\x88\x01\x01\x12\x18\n\x0bto_callsign\x18\x03 \x01(\tH\x01\x88\x01\x01\x42\x05\n\x03_toB\x0e\n\x0c_to_callsign\"M\n\x05Group\x12$\n\x04role\x18\x01 \x01(\x0e\x32\x16.meshtastic.MemberRole\x12\x1e\n\x04team\x18\x02 \x01(\x0e\x32\x10.meshtastic.Team\"\x19\n\x06Status\x12\x0f\n\x07\x62\x61ttery\x18\x01 \x01(\r\"4\n\x07\x43ontact\x12\x10\n\x08\x63\x61llsign\x18\x01 \x01(\t\x12\x17\n\x0f\x64\x65vice_callsign\x18\x02 \x01(\t\"_\n\x03PLI\x12\x12\n\nlatitude_i\x18\x01 \x01(\x0f\x12\x13\n\x0blongitude_i\x18\x02 \x01(\x0f\x12\x10\n\x08\x61ltitude\x18\x03 \x01(\x05\x12\r\n\x05speed\x18\x04 \x01(\r\x12\x0e\n\x06\x63ourse\x18\x05 \x01(\r*\xc0\x01\n\x04Team\x12\x14\n\x10Unspecifed_Color\x10\x00\x12\t\n\x05White\x10\x01\x12\n\n\x06Yellow\x10\x02\x12\n\n\x06Orange\x10\x03\x12\x0b\n\x07Magenta\x10\x04\x12\x07\n\x03Red\x10\x05\x12\n\n\x06Maroon\x10\x06\x12\n\n\x06Purple\x10\x07\x12\r\n\tDark_Blue\x10\x08\x12\x08\n\x04\x42lue\x10\t\x12\x08\n\x04\x43yan\x10\n\x12\x08\n\x04Teal\x10\x0b\x12\t\n\x05Green\x10\x0c\x12\x0e\n\nDark_Green\x10\r\x12\t\n\x05\x42rown\x10\x0e*\x7f\n\nMemberRole\x12\x0e\n\nUnspecifed\x10\x00\x12\x0e\n\nTeamMember\x10\x01\x12\x0c\n\x08TeamLead\x10\x02\x12\x06\n\x02HQ\x10\x03\x12\n\n\x06Sniper\x10\x04\x12\t\n\x05Medic\x10\x05\x12\x13\n\x0f\x46orwardObserver\x10\x06\x12\x07\n\x03RTO\x10\x07\x12\x06\n\x02K9\x10\x08\x42_\n\x13\x63om.geeksville.meshB\nATAKProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.atak_pb2', globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.atak_pb2', globals())
@@ -21,20 +21,20 @@ if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None DESCRIPTOR._options = None
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\nATAKProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000' DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\nATAKProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
_TEAM._serialized_start=580 _TEAM._serialized_start=622
_TEAM._serialized_end=772 _TEAM._serialized_end=814
_MEMBERROLE._serialized_start=774 _MEMBERROLE._serialized_start=816
_MEMBERROLE._serialized_end=901 _MEMBERROLE._serialized_end=943
_TAKPACKET._serialized_start=38 _TAKPACKET._serialized_start=38
_TAKPACKET._serialized_end=268 _TAKPACKET._serialized_end=268
_GEOCHAT._serialized_start=270 _GEOCHAT._serialized_start=270
_GEOCHAT._serialized_end=320 _GEOCHAT._serialized_end=362
_GROUP._serialized_start=322 _GROUP._serialized_start=364
_GROUP._serialized_end=399 _GROUP._serialized_end=441
_STATUS._serialized_start=401 _STATUS._serialized_start=443
_STATUS._serialized_end=426 _STATUS._serialized_end=468
_CONTACT._serialized_start=428 _CONTACT._serialized_start=470
_CONTACT._serialized_end=480 _CONTACT._serialized_end=522
_PLI._serialized_start=482 _PLI._serialized_start=524
_PLI._serialized_end=577 _PLI._serialized_end=619
# @@protoc_insertion_point(module_scope) # @@protoc_insertion_point(module_scope)

View File

@@ -302,6 +302,7 @@ class GeoChat(google.protobuf.message.Message):
MESSAGE_FIELD_NUMBER: builtins.int MESSAGE_FIELD_NUMBER: builtins.int
TO_FIELD_NUMBER: builtins.int TO_FIELD_NUMBER: builtins.int
TO_CALLSIGN_FIELD_NUMBER: builtins.int
message: builtins.str message: builtins.str
""" """
The text message The text message
@@ -310,15 +311,23 @@ class GeoChat(google.protobuf.message.Message):
""" """
Uid recipient of the message Uid recipient of the message
""" """
to_callsign: builtins.str
"""
Callsign of the recipient for the message
"""
def __init__( def __init__(
self, self,
*, *,
message: builtins.str = ..., message: builtins.str = ...,
to: builtins.str | None = ..., to: builtins.str | None = ...,
to_callsign: builtins.str | None = ...,
) -> None: ... ) -> None: ...
def HasField(self, field_name: typing_extensions.Literal["_to", b"_to", "to", b"to"]) -> builtins.bool: ... def HasField(self, field_name: typing_extensions.Literal["_to", b"_to", "_to_callsign", b"_to_callsign", "to", b"to", "to_callsign", b"to_callsign"]) -> builtins.bool: ...
def ClearField(self, field_name: typing_extensions.Literal["_to", b"_to", "message", b"message", "to", b"to"]) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["_to", b"_to", "_to_callsign", b"_to_callsign", "message", b"message", "to", b"to", "to_callsign", b"to_callsign"]) -> None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing_extensions.Literal["_to", b"_to"]) -> typing_extensions.Literal["to"] | None: ... def WhichOneof(self, oneof_group: typing_extensions.Literal["_to", b"_to"]) -> typing_extensions.Literal["to"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing_extensions.Literal["_to_callsign", b"_to_callsign"]) -> typing_extensions.Literal["to_callsign"] | None: ...
global___GeoChat = GeoChat global___GeoChat = GeoChat

View File

@@ -5,6 +5,8 @@ import time
import struct import struct
import asyncio import asyncio
from threading import Thread, Event from threading import Thread, Event
from typing import Optional
from bleak import BleakScanner, BleakClient from bleak import BleakScanner, BleakClient
from meshtastic.mesh_interface import MeshInterface from meshtastic.mesh_interface import MeshInterface
@@ -30,7 +32,7 @@ class BLEInterface(MeshInterface):
MESH = False MESH = False
def __init__(self, address, noProto = False, debugOut = None): def __init__(self, address: Optional[str], noProto: bool = False, debugOut = None, noNodes: bool = False):
self.state = BLEInterface.BLEState() self.state = BLEInterface.BLEState()
if not address: if not address:
@@ -58,7 +60,7 @@ class BLEInterface(MeshInterface):
return return
logging.debug("Mesh init starting") logging.debug("Mesh init starting")
MeshInterface.__init__(self, debugOut = debugOut, noProto = noProto) MeshInterface.__init__(self, debugOut = debugOut, noProto = noProto, noNodes = noNodes)
self._startConfig() self._startConfig()
if not self.noProto: if not self.noProto:
self._waitConnected(timeout = 60.0) self._waitConnected(timeout = 60.0)

View File

@@ -13,7 +13,7 @@ _sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x18meshtastic/channel.proto\x12\nmeshtastic\"\xb8\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\x33\n\x0fmodule_settings\x18\x07 \x01(\x0b\x32\x1a.meshtastic.ModuleSettings\",\n\x0eModuleSettings\x12\x1a\n\x12position_precision\x18\x01 \x01(\r\"\xa1\x01\n\x07\x43hannel\x12\r\n\x05index\x18\x01 \x01(\x05\x12-\n\x08settings\x18\x02 \x01(\x0b\x32\x1b.meshtastic.ChannelSettings\x12&\n\x04role\x18\x03 \x01(\x0e\x32\x18.meshtastic.Channel.Role\"0\n\x04Role\x12\x0c\n\x08\x44ISABLED\x10\x00\x12\x0b\n\x07PRIMARY\x10\x01\x12\r\n\tSECONDARY\x10\x02\x42\x62\n\x13\x63om.geeksville.meshB\rChannelProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3') DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x18meshtastic/channel.proto\x12\nmeshtastic\"\xb8\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\x33\n\x0fmodule_settings\x18\x07 \x01(\x0b\x32\x1a.meshtastic.ModuleSettings\"E\n\x0eModuleSettings\x12\x1a\n\x12position_precision\x18\x01 \x01(\r\x12\x17\n\x0fis_client_muted\x18\x02 \x01(\x08\"\xa1\x01\n\x07\x43hannel\x12\r\n\x05index\x18\x01 \x01(\x05\x12-\n\x08settings\x18\x02 \x01(\x0b\x32\x1b.meshtastic.ChannelSettings\x12&\n\x04role\x18\x03 \x01(\x0e\x32\x18.meshtastic.Channel.Role\"0\n\x04Role\x12\x0c\n\x08\x44ISABLED\x10\x00\x12\x0b\n\x07PRIMARY\x10\x01\x12\r\n\tSECONDARY\x10\x02\x42\x62\n\x13\x63om.geeksville.meshB\rChannelProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.channel_pb2', globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.channel_pb2', globals())
@@ -26,9 +26,9 @@ if _descriptor._USE_C_DESCRIPTORS == False:
_CHANNELSETTINGS._serialized_start=41 _CHANNELSETTINGS._serialized_start=41
_CHANNELSETTINGS._serialized_end=225 _CHANNELSETTINGS._serialized_end=225
_MODULESETTINGS._serialized_start=227 _MODULESETTINGS._serialized_start=227
_MODULESETTINGS._serialized_end=271 _MODULESETTINGS._serialized_end=296
_CHANNEL._serialized_start=274 _CHANNEL._serialized_start=299
_CHANNEL._serialized_end=435 _CHANNEL._serialized_end=460
_CHANNEL_ROLE._serialized_start=387 _CHANNEL_ROLE._serialized_start=412
_CHANNEL_ROLE._serialized_end=435 _CHANNEL_ROLE._serialized_end=460
# @@protoc_insertion_point(module_scope) # @@protoc_insertion_point(module_scope)

View File

@@ -125,16 +125,23 @@ class ModuleSettings(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor DESCRIPTOR: google.protobuf.descriptor.Descriptor
POSITION_PRECISION_FIELD_NUMBER: builtins.int POSITION_PRECISION_FIELD_NUMBER: builtins.int
IS_CLIENT_MUTED_FIELD_NUMBER: builtins.int
position_precision: builtins.int position_precision: builtins.int
""" """
Bits of precision for the location sent in position packets. Bits of precision for the location sent in position packets.
""" """
is_client_muted: builtins.bool
"""
Controls whether or not the phone / clients should mute the current channel
Useful for noisy public channels you don't necessarily want to disable
"""
def __init__( def __init__(
self, self,
*, *,
position_precision: builtins.int = ..., position_precision: builtins.int = ...,
is_client_muted: builtins.bool = ...,
) -> None: ... ) -> None: ...
def ClearField(self, field_name: typing_extensions.Literal["position_precision", b"position_precision"]) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["is_client_muted", b"is_client_muted", "position_precision", b"position_precision"]) -> None: ...
global___ModuleSettings = ModuleSettings global___ModuleSettings = ModuleSettings

View File

File diff suppressed because one or more lines are too long

View File

@@ -245,6 +245,7 @@ class Config(google.protobuf.message.Message):
IS_MANAGED_FIELD_NUMBER: builtins.int IS_MANAGED_FIELD_NUMBER: builtins.int
DISABLE_TRIPLE_CLICK_FIELD_NUMBER: builtins.int DISABLE_TRIPLE_CLICK_FIELD_NUMBER: builtins.int
TZDEF_FIELD_NUMBER: builtins.int TZDEF_FIELD_NUMBER: builtins.int
LED_HEARTBEAT_DISABLED_FIELD_NUMBER: builtins.int
role: global___Config.DeviceConfig.Role.ValueType role: global___Config.DeviceConfig.Role.ValueType
""" """
Sets the role of node Sets the role of node
@@ -294,6 +295,10 @@ class Config(google.protobuf.message.Message):
""" """
POSIX Timezone definition string from https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv. POSIX Timezone definition string from https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv.
""" """
led_heartbeat_disabled: builtins.bool
"""
If true, disable the default blinking LED (LED_PIN) behavior on the device
"""
def __init__( def __init__(
self, self,
*, *,
@@ -308,8 +313,9 @@ class Config(google.protobuf.message.Message):
is_managed: builtins.bool = ..., is_managed: builtins.bool = ...,
disable_triple_click: builtins.bool = ..., disable_triple_click: builtins.bool = ...,
tzdef: builtins.str = ..., tzdef: builtins.str = ...,
led_heartbeat_disabled: builtins.bool = ...,
) -> None: ... ) -> None: ...
def ClearField(self, field_name: typing_extensions.Literal["button_gpio", b"button_gpio", "buzzer_gpio", b"buzzer_gpio", "debug_log_enabled", b"debug_log_enabled", "disable_triple_click", b"disable_triple_click", "double_tap_as_button_press", b"double_tap_as_button_press", "is_managed", b"is_managed", "node_info_broadcast_secs", b"node_info_broadcast_secs", "rebroadcast_mode", b"rebroadcast_mode", "role", b"role", "serial_enabled", b"serial_enabled", "tzdef", b"tzdef"]) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["button_gpio", b"button_gpio", "buzzer_gpio", b"buzzer_gpio", "debug_log_enabled", b"debug_log_enabled", "disable_triple_click", b"disable_triple_click", "double_tap_as_button_press", b"double_tap_as_button_press", "is_managed", b"is_managed", "led_heartbeat_disabled", b"led_heartbeat_disabled", "node_info_broadcast_secs", b"node_info_broadcast_secs", "rebroadcast_mode", b"rebroadcast_mode", "role", b"role", "serial_enabled", b"serial_enabled", "tzdef", b"tzdef"]) -> None: ...
@typing_extensions.final @typing_extensions.final
class PositionConfig(google.protobuf.message.Message): class PositionConfig(google.protobuf.message.Message):

View File

@@ -9,6 +9,7 @@ import sys
import threading import threading
import time import time
from datetime import datetime from datetime import datetime
from decimal import Decimal
from typing import Any, Callable, Dict, List, Optional, Union from typing import Any, Callable, Dict, List, Optional, Union
@@ -25,6 +26,7 @@ from meshtastic import (
BROADCAST_ADDR, BROADCAST_ADDR,
BROADCAST_NUM, BROADCAST_NUM,
LOCAL_ADDR, LOCAL_ADDR,
NODELESS_WANT_CONFIG_ID,
ResponseHandler, ResponseHandler,
protocols, protocols,
publishingThread, publishingThread,
@@ -40,7 +42,7 @@ from meshtastic.util import (
) )
class MeshInterface: class MeshInterface: # pylint: disable=R0902
"""Interface class for meshtastic devices """Interface class for meshtastic devices
Properties: Properties:
@@ -56,12 +58,14 @@ class MeshInterface:
self.message = message self.message = message
super().__init__(self.message) super().__init__(self.message)
def __init__(self, debugOut=None, noProto: bool=False) -> None: def __init__(self, debugOut=None, noProto: bool=False, noNodes: bool=False) -> None:
"""Constructor """Constructor
Keyword Arguments: Keyword Arguments:
noProto -- If True, don't try to run our protocol on the noProto -- If True, don't try to run our protocol on the
link - just be a dumb serial client. link - just be a dumb serial client.
noNodes -- If True, instruct the node to not send its nodedb
on startup, just other configuration information.
""" """
self.debugOut = debugOut self.debugOut = debugOut
self.nodes: Optional[Dict[str,Dict]] = None # FIXME self.nodes: Optional[Dict[str,Dict]] = None # FIXME
@@ -80,11 +84,13 @@ class MeshInterface:
random.seed() # FIXME, we should not clobber the random seedval here, instead tell user they must call it random.seed() # FIXME, we should not clobber the random seedval here, instead tell user they must call it
self.currentPacketId: int = random.randint(0, 0xFFFFFFFF) self.currentPacketId: int = random.randint(0, 0xFFFFFFFF)
self.nodesByNum: Optional[Dict[int, Dict]] = None self.nodesByNum: Optional[Dict[int, Dict]] = None
self.configId: Optional[int] = None self.noNodes: bool = noNodes
self.configId: Optional[int] = NODELESS_WANT_CONFIG_ID if noNodes else None
self.gotResponse: bool = False # used in gpio read self.gotResponse: bool = False # used in gpio read
self.mask: Optional[int] = None # used in gpio read and gpio watch self.mask: Optional[int] = None # used in gpio read and gpio watch
self.queueStatus: Optional[mesh_pb2.QueueStatus] = None self.queueStatus: Optional[mesh_pb2.QueueStatus] = None
self.queue: collections.OrderedDict = collections.OrderedDict() self.queue: collections.OrderedDict = collections.OrderedDict()
self._localChannels = None
def close(self): def close(self):
"""Shutdown this interface""" """Shutdown this interface"""
@@ -133,7 +139,7 @@ class MeshInterface:
# use id as dictionary key for correct json format in list of nodes # use id as dictionary key for correct json format in list of nodes
nodeid = n2["user"]["id"] nodeid = n2["user"]["id"]
nodes[nodeid] = n2 nodes[nodeid] = n2
infos = owner + myinfo + metadata + mesh + json.dumps(nodes) infos = owner + myinfo + metadata + mesh + json.dumps(nodes, indent=2)
print(infos) print(infos)
return infos return infos
@@ -165,7 +171,8 @@ class MeshInterface:
if not includeSelf and node["num"] == self.localNode.nodeNum: if not includeSelf and node["num"] == self.localNode.nodeNum:
continue continue
row = {"N": 0, "User": f"UNK: {node['num']}", "ID": f"!{node['num']:08x}"} presumptive_id = f"!{node['num']:08x}"
row = {"N": 0, "User": f"Meshtastic {presumptive_id[-4:]}", "ID": presumptive_id}
user = node.get("user") user = node.get("user")
if user: if user:
@@ -174,6 +181,7 @@ class MeshInterface:
"User": user.get("longName", "N/A"), "User": user.get("longName", "N/A"),
"AKA": user.get("shortName", "N/A"), "AKA": user.get("shortName", "N/A"),
"ID": user["id"], "ID": user["id"],
"Hardware": user.get("hwModel", "UNSET")
} }
) )
@@ -210,8 +218,8 @@ class MeshInterface:
row.update( row.update(
{ {
"SNR": formatFloat(node.get("snr"), 2, " dB"), "SNR": formatFloat(node.get("snr"), 2, " dB"),
"Hops Away": node.get("hopsAway", "unknown"), "Hops Away": node.get("hopsAway", "0/unknown"),
"Channel": node.get("channel"), "Channel": node.get("channel", 0),
"LastHeard": getLH(node.get("lastHeard")), "LastHeard": getLH(node.get("lastHeard")),
"Since": getTimeAgo(node.get("lastHeard")), "Since": getTimeAgo(node.get("lastHeard")),
} }
@@ -247,7 +255,7 @@ class MeshInterface:
destinationId: Union[int, str]=BROADCAST_ADDR, destinationId: Union[int, str]=BROADCAST_ADDR,
wantAck: bool=False, wantAck: bool=False,
wantResponse: bool=False, wantResponse: bool=False,
onResponse: Optional[Callable[[mesh_pb2.MeshPacket], Any]]=None, onResponse: Optional[Callable[[dict], Any]]=None,
channelIndex: int=0, channelIndex: int=0,
): ):
"""Send a utf8 string to some other node, if the node has a display it """Send a utf8 string to some other node, if the node has a display it
@@ -287,7 +295,7 @@ class MeshInterface:
portNum: portnums_pb2.PortNum.ValueType=portnums_pb2.PortNum.PRIVATE_APP, portNum: portnums_pb2.PortNum.ValueType=portnums_pb2.PortNum.PRIVATE_APP,
wantAck: bool=False, wantAck: bool=False,
wantResponse: bool=False, wantResponse: bool=False,
onResponse: Optional[Callable[[mesh_pb2.MeshPacket], Any]]=None, onResponse: Optional[Callable[[dict], Any]]=None,
channelIndex: int=0, channelIndex: int=0,
): ):
"""Send a data packet to some other node """Send a data packet to some other node
@@ -351,6 +359,7 @@ class MeshInterface:
destinationId: Union[int, str]=BROADCAST_ADDR, destinationId: Union[int, str]=BROADCAST_ADDR,
wantAck: bool=False, wantAck: bool=False,
wantResponse: bool=False, wantResponse: bool=False,
channelIndex: int=0,
): ):
""" """
Send a position packet to some other node (normally a broadcast) Send a position packet to some other node (normally a broadcast)
@@ -393,6 +402,7 @@ class MeshInterface:
wantAck=wantAck, wantAck=wantAck,
wantResponse=wantResponse, wantResponse=wantResponse,
onResponse=onResponse, onResponse=onResponse,
channelIndex=channelIndex,
) )
if wantResponse: if wantResponse:
self.waitForPosition() self.waitForPosition()
@@ -426,7 +436,7 @@ class MeshInterface:
if p["decoded"]["routing"]["errorReason"] == 'NO_RESPONSE': if p["decoded"]["routing"]["errorReason"] == 'NO_RESPONSE':
our_exit("No response from node. At least firmware 2.1.22 is required on the destination node.") our_exit("No response from node. At least firmware 2.1.22 is required on the destination node.")
def sendTraceRoute(self, dest: Union[int, str], hopLimit: int): def sendTraceRoute(self, dest: Union[int, str], hopLimit: int, channelIndex: int=0):
"""Send the trace route""" """Send the trace route"""
r = mesh_pb2.RouteDiscovery() r = mesh_pb2.RouteDiscovery()
self.sendData( self.sendData(
@@ -435,12 +445,13 @@ class MeshInterface:
portNum=portnums_pb2.PortNum.TRACEROUTE_APP, portNum=portnums_pb2.PortNum.TRACEROUTE_APP,
wantResponse=True, wantResponse=True,
onResponse=self.onResponseTraceRoute, onResponse=self.onResponseTraceRoute,
channelIndex=channelIndex,
) )
# extend timeout based on number of nodes, limit by configured hopLimit # extend timeout based on number of nodes, limit by configured hopLimit
waitFactor = min(len(self.nodes) - 1 if self.nodes else 0, hopLimit) waitFactor = min(len(self.nodes) - 1 if self.nodes else 0, hopLimit)
self.waitForTraceRoute(waitFactor) self.waitForTraceRoute(waitFactor)
def onResponseTraceRoute(self, p): def onResponseTraceRoute(self, p: dict):
"""on response for trace route""" """on response for trace route"""
routeDiscovery = mesh_pb2.RouteDiscovery() routeDiscovery = mesh_pb2.RouteDiscovery()
routeDiscovery.ParseFromString(p["decoded"]["payload"]) routeDiscovery.ParseFromString(p["decoded"]["payload"])
@@ -456,26 +467,27 @@ class MeshInterface:
self._acknowledgment.receivedTraceRoute = True self._acknowledgment.receivedTraceRoute = True
def sendTelemetry(self, destinationId=BROADCAST_ADDR, wantResponse=False): def sendTelemetry(self, destinationId: Union[int,str]=BROADCAST_ADDR, wantResponse: bool=False, channelIndex: int=0):
"""Send telemetry and optionally ask for a response""" """Send telemetry and optionally ask for a response"""
r = telemetry_pb2.Telemetry() r = telemetry_pb2.Telemetry()
node = next(n for n in self.nodes.values() if n["num"] == self.localNode.nodeNum) if self.nodes is not None:
if node is not None: node = next(n for n in self.nodes.values() if n["num"] == self.localNode.nodeNum)
metrics = node.get("deviceMetrics") if node is not None:
if metrics: metrics = node.get("deviceMetrics")
batteryLevel = metrics.get("batteryLevel") if metrics:
if batteryLevel is not None: batteryLevel = metrics.get("batteryLevel")
r.device_metrics.battery_level = batteryLevel if batteryLevel is not None:
voltage = metrics.get("voltage") r.device_metrics.battery_level = batteryLevel
if voltage is not None: voltage = metrics.get("voltage")
r.device_metrics.voltage = voltage if voltage is not None:
channel_utilization = metrics.get("channelUtilization") r.device_metrics.voltage = voltage
if channel_utilization is not None: channel_utilization = metrics.get("channelUtilization")
r.device_metrics.channel_utilization = channel_utilization if channel_utilization is not None:
air_util_tx = metrics.get("airUtilTx") r.device_metrics.channel_utilization = channel_utilization
if air_util_tx is not None: air_util_tx = metrics.get("airUtilTx")
r.device_metrics.air_util_tx = air_util_tx if air_util_tx is not None:
r.device_metrics.air_util_tx = air_util_tx
if wantResponse: if wantResponse:
onResponse = self.onResponseTelemetry onResponse = self.onResponseTelemetry
@@ -488,11 +500,12 @@ class MeshInterface:
portNum=portnums_pb2.PortNum.TELEMETRY_APP, portNum=portnums_pb2.PortNum.TELEMETRY_APP,
wantResponse=wantResponse, wantResponse=wantResponse,
onResponse=onResponse, onResponse=onResponse,
channelIndex=channelIndex,
) )
if wantResponse: if wantResponse:
self.waitForTelemetry() self.waitForTelemetry()
def onResponseTelemetry(self, p): def onResponseTelemetry(self, p: dict):
"""on response for telemetry""" """on response for telemetry"""
if p["decoded"]["portnum"] == 'TELEMETRY_APP': if p["decoded"]["portnum"] == 'TELEMETRY_APP':
self._acknowledgment.receivedTelemetry = True self._acknowledgment.receivedTelemetry = True
@@ -515,7 +528,7 @@ class MeshInterface:
if p["decoded"]["routing"]["errorReason"] == 'NO_RESPONSE': if p["decoded"]["routing"]["errorReason"] == 'NO_RESPONSE':
our_exit("No response from node. At least firmware 2.1.22 is required on the destination node.") our_exit("No response from node. At least firmware 2.1.22 is required on the destination node.")
def _addResponseHandler(self, requestId: int, callback: Callable): def _addResponseHandler(self, requestId: int, callback: Callable[[dict], Any]):
self.responseHandlers[requestId] = ResponseHandler(callback) self.responseHandlers[requestId] = ResponseHandler(callback)
def _sendPacket(self, meshPacket: mesh_pb2.MeshPacket, destinationId: Union[int,str]=BROADCAST_ADDR, wantAck: bool=False): def _sendPacket(self, meshPacket: mesh_pb2.MeshPacket, destinationId: Union[int,str]=BROADCAST_ADDR, wantAck: bool=False):
@@ -677,6 +690,7 @@ class MeshInterface:
self.heartbeatTimer = threading.Timer(i, callback) self.heartbeatTimer = threading.Timer(i, callback)
self.heartbeatTimer.start() self.heartbeatTimer.start()
p = mesh_pb2.ToRadio() p = mesh_pb2.ToRadio()
p.heartbeat.CopyFrom(mesh_pb2.Heartbeat())
self._sendToRadio(p) self._sendToRadio(p)
callback() # run our periodic callback now, it will make another timer if necessary callback() # run our periodic callback now, it will make another timer if necessary
@@ -700,9 +714,11 @@ class MeshInterface:
self.myInfo = None self.myInfo = None
self.nodes = {} # nodes keyed by ID self.nodes = {} # nodes keyed by ID
self.nodesByNum = {} # nodes keyed by nodenum self.nodesByNum = {} # nodes keyed by nodenum
self._localChannels = [] # empty until we start getting channels pushed from the device (during config)
startConfig = mesh_pb2.ToRadio() startConfig = mesh_pb2.ToRadio()
self.configId = random.randint(0, 0xFFFFFFFF) if self.configId is None or not self.noNodes:
self.configId = random.randint(0, 0xFFFFFFFF)
startConfig.want_config_id = self.configId startConfig.want_config_id = self.configId
self._sendToRadio(startConfig) self._sendToRadio(startConfig)
@@ -781,7 +797,12 @@ class MeshInterface:
Done with initial config messages, now send regular MeshPackets Done with initial config messages, now send regular MeshPackets
to ask for settings and channels to ask for settings and channels
""" """
self.localNode.requestChannels() # This is no longer necessary because the current protocol statemachine has already proactively sent us the locally visible channels
# self.localNode.requestChannels()
self.localNode.setChannels(self._localChannels)
# the following should only be called after we have settings and channels
self._connected() # Tell everyone else we are ready to go
def _handleQueueStatusFromRadio(self, queueStatus) -> None: def _handleQueueStatusFromRadio(self, queueStatus) -> None:
self.queueStatus = queueStatus self.queueStatus = queueStatus
@@ -831,16 +852,18 @@ class MeshInterface:
logging.debug(f"Received device metadata: {stripnl(fromRadio.metadata)}") logging.debug(f"Received device metadata: {stripnl(fromRadio.metadata)}")
elif fromRadio.HasField("node_info"): elif fromRadio.HasField("node_info"):
node = asDict["nodeInfo"] logging.debug(f"Received nodeinfo: {asDict['nodeInfo']}")
node = self._getOrCreateByNum(asDict["nodeInfo"]["num"])
node.update(asDict["nodeInfo"])
try: try:
newpos = self._fixupPosition(node["position"]) newpos = self._fixupPosition(node["position"])
node["position"] = newpos node["position"] = newpos
except: except:
logging.debug("Node without position") logging.debug("Node without position")
logging.debug(f"Received nodeinfo: {node}") # no longer necessary since we're mutating directly in nodesByNum via _getOrCreateByNum
#self.nodesByNum[node["num"]] = node
self.nodesByNum[node["num"]] = node
if "user" in node: # Some nodes might not have user/ids assigned yet if "user" in node: # Some nodes might not have user/ids assigned yet
if "id" in node["user"]: if "id" in node["user"]:
self.nodes[node["user"]["id"]] = node self.nodes[node["user"]["id"]] = node
@@ -854,21 +877,36 @@ class MeshInterface:
# stream API fromRadio.config_complete_id # stream API fromRadio.config_complete_id
logging.debug(f"Config complete ID {self.configId}") logging.debug(f"Config complete ID {self.configId}")
self._handleConfigComplete() self._handleConfigComplete()
elif fromRadio.HasField("channel"):
self._handleChannel(fromRadio.channel)
elif fromRadio.HasField("packet"): elif fromRadio.HasField("packet"):
self._handlePacketFromRadio(fromRadio.packet) self._handlePacketFromRadio(fromRadio.packet)
elif fromRadio.HasField("queueStatus"): elif fromRadio.HasField("queueStatus"):
self._handleQueueStatusFromRadio(fromRadio.queueStatus) self._handleQueueStatusFromRadio(fromRadio.queueStatus)
elif fromRadio.rebooted: elif fromRadio.HasField("mqttClientProxyMessage"):
publishingThread.queueWork(
lambda: pub.sendMessage(
"meshtastic.mqttclientproxymessage", proxymessage=fromRadio.mqttClientProxyMessage, interface=self
)
)
elif fromRadio.HasField("xmodemPacket"):
publishingThread.queueWork(
lambda: pub.sendMessage(
"meshtastic.xmodempacket", packet=fromRadio.xmodemPacket, interface=self
)
)
elif fromRadio.HasField("rebooted") and fromRadio.rebooted:
# Tell clients the device went away. Careful not to call the overridden # Tell clients the device went away. Careful not to call the overridden
# subclass version that closes the serial port # subclass version that closes the serial port
MeshInterface._disconnected(self) MeshInterface._disconnected(self)
self._startConfig() # redownload the node db etc... self._startConfig() # redownload the node db etc...
elif fromRadio.config or fromRadio.moduleConfig: elif fromRadio.HasField("config") or fromRadio.HasField("moduleConfig"):
if fromRadio.config.HasField("device"): if fromRadio.config.HasField("device"):
self.localNode.localConfig.device.CopyFrom(fromRadio.config.device) self.localNode.localConfig.device.CopyFrom(fromRadio.config.device)
elif fromRadio.config.HasField("position"): elif fromRadio.config.HasField("position"):
@@ -896,6 +934,10 @@ class MeshInterface:
self.localNode.moduleConfig.external_notification.CopyFrom( self.localNode.moduleConfig.external_notification.CopyFrom(
fromRadio.moduleConfig.external_notification fromRadio.moduleConfig.external_notification
) )
elif fromRadio.moduleConfig.HasField("store_forward"):
self.localNode.moduleConfig.store_forward.CopyFrom(
fromRadio.moduleConfig.store_forward
)
elif fromRadio.moduleConfig.HasField("range_test"): elif fromRadio.moduleConfig.HasField("range_test"):
self.localNode.moduleConfig.range_test.CopyFrom( self.localNode.moduleConfig.range_test.CopyFrom(
fromRadio.moduleConfig.range_test fromRadio.moduleConfig.range_test
@@ -942,9 +984,9 @@ class MeshInterface:
Returns the position with the updated keys Returns the position with the updated keys
""" """
if "latitudeI" in position: if "latitudeI" in position:
position["latitude"] = position["latitudeI"] * 1e-7 position["latitude"] = float(position["latitudeI"] * Decimal("1e-7"))
if "longitudeI" in position: if "longitudeI" in position:
position["longitude"] = position["longitudeI"] * 1e-7 position["longitude"] = float(position["longitudeI"] * Decimal("1e-7"))
return position return position
def _nodeNumToId(self, num): def _nodeNumToId(self, num):
@@ -973,10 +1015,23 @@ class MeshInterface:
if nodeNum in self.nodesByNum: if nodeNum in self.nodesByNum:
return self.nodesByNum[nodeNum] return self.nodesByNum[nodeNum]
else: else:
n = {"num": nodeNum} # Create a minimal node db entry presumptive_id = f"!{nodeNum:08x}"
n = {
"num": nodeNum,
"user": {
"id": presumptive_id,
"longName": f"Meshtastic {presumptive_id[-4:]}",
"shortName": f"{presumptive_id[-4:]}",
"hwModel": "UNSET"
}
} # Create a minimal node db entry
self.nodesByNum[nodeNum] = n self.nodesByNum[nodeNum] = n
return n return n
def _handleChannel(self, channel):
"""During initial config the local node will proactively send all N (8) channels it knows"""
self._localChannels.append(channel)
def _handlePacketFromRadio(self, meshPacket, hack=False): def _handlePacketFromRadio(self, meshPacket, hack=False):
"""Handle a MeshPacket that just arrived from the radio """Handle a MeshPacket that just arrived from the radio

View File

File diff suppressed because one or more lines are too long

View File

@@ -112,6 +112,10 @@ class _HardwareModelEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._
""" """
LoRAType device: https://loratype.org/ LoRAType device: https://loratype.org/
""" """
WIPHONE: _HardwareModel.ValueType # 20
"""
wiphone https://www.wiphone.io/
"""
STATION_G1: _HardwareModel.ValueType # 25 STATION_G1: _HardwareModel.ValueType # 25
""" """
B&Q Consulting Station Edition G1: https://uniteng.com/wiki/doku.php?id=meshtastic:station B&Q Consulting Station Edition G1: https://uniteng.com/wiki/doku.php?id=meshtastic:station
@@ -271,6 +275,21 @@ class _HardwareModelEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._
""" """
CDEBYTE EoRa-S3 board using their own MM modules, clone of LILYGO T3S3 CDEBYTE EoRa-S3 board using their own MM modules, clone of LILYGO T3S3
""" """
TWC_MESH_V4: _HardwareModel.ValueType # 62
"""
TWC_MESH_V4
Adafruit NRF52840 feather express with SX1262, SSD1306 OLED and NEO6M GPS
"""
NRF52_PROMICRO_DIY: _HardwareModel.ValueType # 63
"""
NRF52_PROMICRO_DIY
Promicro NRF52840 with SX1262/LLCC68, SSD1306 OLED and NEO6M GPS
"""
RADIOMASTER_900_BANDIT_NANO: _HardwareModel.ValueType # 64
"""
RadioMaster 900 Bandit Nano, https://www.radiomasterrc.com/products/bandit-nano-expresslrs-rf-module
ESP32-D0WDQ6 With SX1276/SKY66122, SSD1306 OLED and No GPS
"""
PRIVATE_HW: _HardwareModel.ValueType # 255 PRIVATE_HW: _HardwareModel.ValueType # 255
""" """
------------------------------------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------------------------------
@@ -368,6 +387,10 @@ LORA_TYPE: HardwareModel.ValueType # 19
""" """
LoRAType device: https://loratype.org/ LoRAType device: https://loratype.org/
""" """
WIPHONE: HardwareModel.ValueType # 20
"""
wiphone https://www.wiphone.io/
"""
STATION_G1: HardwareModel.ValueType # 25 STATION_G1: HardwareModel.ValueType # 25
""" """
B&Q Consulting Station Edition G1: https://uniteng.com/wiki/doku.php?id=meshtastic:station B&Q Consulting Station Edition G1: https://uniteng.com/wiki/doku.php?id=meshtastic:station
@@ -527,6 +550,21 @@ CDEBYTE_EORA_S3: HardwareModel.ValueType # 61
""" """
CDEBYTE EoRa-S3 board using their own MM modules, clone of LILYGO T3S3 CDEBYTE EoRa-S3 board using their own MM modules, clone of LILYGO T3S3
""" """
TWC_MESH_V4: HardwareModel.ValueType # 62
"""
TWC_MESH_V4
Adafruit NRF52840 feather express with SX1262, SSD1306 OLED and NEO6M GPS
"""
NRF52_PROMICRO_DIY: HardwareModel.ValueType # 63
"""
NRF52_PROMICRO_DIY
Promicro NRF52840 with SX1262/LLCC68, SSD1306 OLED and NEO6M GPS
"""
RADIOMASTER_900_BANDIT_NANO: HardwareModel.ValueType # 64
"""
RadioMaster 900 Bandit Nano, https://www.radiomasterrc.com/products/bandit-nano-expresslrs-rf-module
ESP32-D0WDQ6 With SX1276/SKY66122, SSD1306 OLED and No GPS
"""
PRIVATE_HW: HardwareModel.ValueType # 255 PRIVATE_HW: HardwareModel.ValueType # 255
""" """
------------------------------------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------------------------------

View File

File diff suppressed because one or more lines are too long

View File

@@ -405,6 +405,8 @@ class ModuleConfig(google.protobuf.message.Message):
ENABLED_FIELD_NUMBER: builtins.int ENABLED_FIELD_NUMBER: builtins.int
PAXCOUNTER_UPDATE_INTERVAL_FIELD_NUMBER: builtins.int PAXCOUNTER_UPDATE_INTERVAL_FIELD_NUMBER: builtins.int
WIFI_THRESHOLD_FIELD_NUMBER: builtins.int
BLE_THRESHOLD_FIELD_NUMBER: builtins.int
enabled: builtins.bool enabled: builtins.bool
""" """
Enable the Paxcounter Module Enable the Paxcounter Module
@@ -414,13 +416,23 @@ class ModuleConfig(google.protobuf.message.Message):
Interval in seconds of how often we should try to send our Interval in seconds of how often we should try to send our
metrics to the mesh metrics to the mesh
""" """
wifi_threshold: builtins.int
"""
WiFi RSSI threshold. Defaults to -80
"""
ble_threshold: builtins.int
"""
BLE RSSI threshold. Defaults to -80
"""
def __init__( def __init__(
self, self,
*, *,
enabled: builtins.bool = ..., enabled: builtins.bool = ...,
paxcounter_update_interval: builtins.int = ..., paxcounter_update_interval: builtins.int = ...,
wifi_threshold: builtins.int = ...,
ble_threshold: builtins.int = ...,
) -> None: ... ) -> None: ...
def ClearField(self, field_name: typing_extensions.Literal["enabled", b"enabled", "paxcounter_update_interval", b"paxcounter_update_interval"]) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["ble_threshold", b"ble_threshold", "enabled", b"enabled", "paxcounter_update_interval", b"paxcounter_update_interval", "wifi_threshold", b"wifi_threshold"]) -> None: ...
@typing_extensions.final @typing_extensions.final
class SerialConfig(google.protobuf.message.Message): class SerialConfig(google.protobuf.message.Message):

View File

@@ -7,7 +7,7 @@ import time
from typing import Union from typing import Union
from meshtastic import admin_pb2, apponly_pb2, channel_pb2, localonly_pb2, portnums_pb2 from meshtastic import admin_pb2, apponly_pb2, channel_pb2, localonly_pb2, mesh_pb2, portnums_pb2
from meshtastic.util import ( from meshtastic.util import (
Timeout, Timeout,
camel_to_snake, camel_to_snake,
@@ -64,14 +64,19 @@ class Node:
"""Show human readable description of our node""" """Show human readable description of our node"""
prefs = "" prefs = ""
if self.localConfig: if self.localConfig:
prefs = message_to_json(self.localConfig) prefs = message_to_json(self.localConfig, multiline=True)
print(f"Preferences: {prefs}\n") print(f"Preferences: {prefs}\n")
prefs = "" prefs = ""
if self.moduleConfig: if self.moduleConfig:
prefs = message_to_json(self.moduleConfig) prefs = message_to_json(self.moduleConfig, multiline=True)
print(f"Module preferences: {prefs}\n") print(f"Module preferences: {prefs}\n")
self.showChannels() self.showChannels()
def setChannels(self, channels):
"""Set the channels for this node"""
self.channels = channels
self._fixupChannels()
def requestChannels(self): def requestChannels(self):
"""Send regular MeshPackets to ask channels.""" """Send regular MeshPackets to ask channels."""
logging.debug(f"requestChannels for nodeNum:{self.nodeNum}") logging.debug(f"requestChannels for nodeNum:{self.nodeNum}")
@@ -313,6 +318,8 @@ class Node:
): ):
channelSet.settings.append(c.settings) channelSet.settings.append(c.settings)
if len(self.localConfig.ListFields()) == 0:
self.requestConfig(self.localConfig.DESCRIPTOR.fields_by_name.get('lora'))
channelSet.lora_config.CopyFrom(self.localConfig.lora) channelSet.lora_config.CopyFrom(self.localConfig.lora)
some_bytes = channelSet.SerializeToString() some_bytes = channelSet.SerializeToString()
s = base64.urlsafe_b64encode(some_bytes).decode("ascii") s = base64.urlsafe_b64encode(some_bytes).decode("ascii")
@@ -569,6 +576,19 @@ class Node:
onResponse = self.onAckNak onResponse = self.onAckNak
return self._sendAdmin(p, onResponse=onResponse) return self._sendAdmin(p, onResponse=onResponse)
def enterDFUMode(self):
"""Tell the node to enter DFU mode (NRF52)."""
p = admin_pb2.AdminMessage()
p.enter_dfu_mode_request = True
logging.info(f"Telling node to enable DFU mode")
# If sending to a remote node, wait for ACK/NAK
if self == self.iface.localNode:
onResponse = None
else:
onResponse = self.onAckNak
return self._sendAdmin(p, onResponse=onResponse)
def shutdown(self, secs: int = 10): def shutdown(self, secs: int = 10):
"""Tell the node to shutdown.""" """Tell the node to shutdown."""
p = admin_pb2.AdminMessage() p = admin_pb2.AdminMessage()
@@ -588,9 +608,10 @@ class Node:
p.get_device_metadata_request = True p.get_device_metadata_request = True
logging.info(f"Requesting device metadata") logging.info(f"Requesting device metadata")
return self._sendAdmin( self._sendAdmin(
p, wantResponse=True, onResponse=self.onRequestGetMetadata p, wantResponse=True, onResponse=self.onRequestGetMetadata
) )
self.iface.waitForAckNak()
def factoryReset(self): def factoryReset(self):
"""Tell the node to factory reset.""" """Tell the node to factory reset."""
@@ -635,11 +656,43 @@ class Node:
onResponse = self.onAckNak onResponse = self.onAckNak
return self._sendAdmin(p, onResponse=onResponse) return self._sendAdmin(p, onResponse=onResponse)
def setFixedPosition(self, lat: Union[int, float], lon: Union[int, float], alt: int):
"""Tell the node to set fixed position to the provided value and enable the fixed position setting"""
if self != self.iface.localNode:
logging.error("Setting position of remote nodes is not supported.")
return None
p = mesh_pb2.Position()
if isinstance(lat, float) and lat != 0.0:
p.latitude_i = int(lat / 1e-7)
elif isinstance(lat, int) and lat != 0:
p.latitude_i = lat
if isinstance(lon, float) and lon != 0.0:
p.longitude_i = int(lon / 1e-7)
elif isinstance(lon, int) and lon != 0:
p.longitude_i = lon
if alt != 0:
p.altitude = alt
a = admin_pb2.AdminMessage()
a.set_fixed_position.CopyFrom(p)
return self._sendAdmin(a)
def removeFixedPosition(self):
"""Tell the node to remove the fixed position and set the fixed position setting to false"""
p = admin_pb2.AdminMessage()
p.remove_fixed_position = True
logging.info(f"Telling node to remove fixed position")
return self._sendAdmin(p)
def _fixupChannels(self): def _fixupChannels(self):
"""Fixup indexes and add disabled channels as needed""" """Fixup indexes and add disabled channels as needed"""
# Add extra disabled channels as needed # Add extra disabled channels as needed
# TODO: These 2 lines seem to not do anything. # This is needed because the protobufs will have index **missing** if the channel number is zero
for index, ch in enumerate(self.channels): for index, ch in enumerate(self.channels):
ch.index = index # fixup indexes ch.index = index # fixup indexes
@@ -661,24 +714,30 @@ class Node:
"""Handle the response packet for requesting device metadata getMetadata()""" """Handle the response packet for requesting device metadata getMetadata()"""
logging.debug(f"onRequestGetMetadata() p:{p}") logging.debug(f"onRequestGetMetadata() p:{p}")
if p["decoded"]["portnum"] == portnums_pb2.PortNum.Name( if "routing" in p["decoded"]:
portnums_pb2.PortNum.ROUTING_APP
):
if p["decoded"]["routing"]["errorReason"] != "NONE": if p["decoded"]["routing"]["errorReason"] != "NONE":
logging.warning( print(f'Error on response: {p["decoded"]["routing"]["errorReason"]}')
f'Metadata request failed, error reason: {p["decoded"]["routing"]["errorReason"]}' self.iface._acknowledgment.receivedNak = True
) else:
self._timeout.expireTime = time.time() # Do not wait any longer self.iface._acknowledgment.receivedAck = True
return # Don't try to parse this routing message if p["decoded"]["portnum"] == portnums_pb2.PortNum.Name(
logging.debug(f"Retrying metadata request.") portnums_pb2.PortNum.ROUTING_APP
self.getMetadata() ):
return if p["decoded"]["routing"]["errorReason"] != "NONE":
logging.warning(
f'Metadata request failed, error reason: {p["decoded"]["routing"]["errorReason"]}'
)
self._timeout.expireTime = time.time() # Do not wait any longer
return # Don't try to parse this routing message
logging.debug(f"Retrying metadata request.")
self.getMetadata()
return
c = p["decoded"]["admin"]["raw"].get_device_metadata_response c = p["decoded"]["admin"]["raw"].get_device_metadata_response
self._timeout.reset() # We made forward progress self._timeout.reset() # We made forward progress
logging.debug(f"Received metadata {stripnl(c)}") logging.debug(f"Received metadata {stripnl(c)}")
print(f"\nfirmware_version: {c.firmware_version}") print(f"\nfirmware_version: {c.firmware_version}")
print(f"device_state_version: {c.device_state_version}") print(f"device_state_version: {c.device_state_version}")
def onResponseRequestChannel(self, p): def onResponseRequestChannel(self, p):
"""Handle the response packet for requesting a channel _requestChannel()""" """Handle the response packet for requesting a channel _requestChannel()"""
@@ -711,9 +770,6 @@ class Node:
self.channels = self.partialChannels self.channels = self.partialChannels
self._fixupChannels() self._fixupChannels()
# FIXME, the following should only be called after we have settings and channels
self.iface._connected() # Tell everyone else we are ready to go
else: else:
self._requestChannel(index + 1) self._requestChannel(index + 1)

View File

@@ -4,6 +4,8 @@ import logging
import platform import platform
import time import time
from typing import Optional
import serial # type: ignore[import-untyped] import serial # type: ignore[import-untyped]
import meshtastic.util import meshtastic.util
@@ -16,7 +18,7 @@ if platform.system() != "Windows":
class SerialInterface(StreamInterface): class SerialInterface(StreamInterface):
"""Interface class for meshtastic devices over a serial link""" """Interface class for meshtastic devices over a serial link"""
def __init__(self, devPath=None, debugOut=None, noProto=False, connectNow=True): def __init__(self, devPath: Optional[str]=None, debugOut=None, noProto=False, connectNow=True, noNodes: bool=False):
"""Constructor, opens a connection to a specified serial port, or if unspecified try to """Constructor, opens a connection to a specified serial port, or if unspecified try to
find one Meshtastic device by probing find one Meshtastic device by probing
@@ -26,7 +28,7 @@ class SerialInterface(StreamInterface):
""" """
self.noProto = noProto self.noProto = noProto
self.devPath = devPath self.devPath: Optional[str] = devPath
if self.devPath is None: if self.devPath is None:
ports = meshtastic.util.findPorts(True) ports = meshtastic.util.findPorts(True)
@@ -60,7 +62,7 @@ class SerialInterface(StreamInterface):
time.sleep(0.1) time.sleep(0.1)
StreamInterface.__init__( StreamInterface.__init__(
self, debugOut=debugOut, noProto=noProto, connectNow=connectNow self, debugOut=debugOut, noProto=noProto, connectNow=connectNow, noNodes=noNodes
) )
def close(self): def close(self):

View File

@@ -19,7 +19,7 @@ MAX_TO_FROM_RADIO_SIZE = 512
class StreamInterface(MeshInterface): class StreamInterface(MeshInterface):
"""Interface class for meshtastic devices over a stream link (serial, TCP, etc)""" """Interface class for meshtastic devices over a stream link (serial, TCP, etc)"""
def __init__(self, debugOut=None, noProto=False, connectNow=True): def __init__(self, debugOut=None, noProto=False, connectNow=True, noNodes=False):
"""Constructor, opens a connection to self.stream """Constructor, opens a connection to self.stream
Keyword Arguments: Keyword Arguments:
@@ -43,7 +43,7 @@ class StreamInterface(MeshInterface):
# FIXME, figure out why daemon=True causes reader thread to exit too early # FIXME, figure out why daemon=True causes reader thread to exit too early
self._rxThread = threading.Thread(target=self.__reader, args=(), daemon=True) self._rxThread = threading.Thread(target=self.__reader, args=(), daemon=True)
MeshInterface.__init__(self, debugOut=debugOut, noProto=noProto) MeshInterface.__init__(self, debugOut=debugOut, noProto=noProto, noNodes=noNodes)
# Start the reader thread after superclass constructor completes init # Start the reader thread after superclass constructor completes init
if connectNow: if connectNow:

View File

@@ -17,6 +17,7 @@ class TCPInterface(StreamInterface):
noProto=False, noProto=False,
connectNow=True, connectNow=True,
portNumber=4403, portNumber=4403,
noNodes:bool=False,
): ):
"""Constructor, opens a connection to a specified IP address/hostname """Constructor, opens a connection to a specified IP address/hostname
@@ -38,7 +39,7 @@ class TCPInterface(StreamInterface):
self.socket = None self.socket = None
StreamInterface.__init__( StreamInterface.__init__(
self, debugOut=debugOut, noProto=noProto, connectNow=connectNow self, debugOut=debugOut, noProto=noProto, connectNow=connectNow, noNodes=noNodes
) )
def _socket_shutdown(self): def _socket_shutdown(self):

View File

@@ -13,7 +13,7 @@ _sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1ameshtastic/telemetry.proto\x12\nmeshtastic\"\x81\x01\n\rDeviceMetrics\x12\x15\n\rbattery_level\x18\x01 \x01(\r\x12\x0f\n\x07voltage\x18\x02 \x01(\x02\x12\x1b\n\x13\x63hannel_utilization\x18\x03 \x01(\x02\x12\x13\n\x0b\x61ir_util_tx\x18\x04 \x01(\x02\x12\x16\n\x0euptime_seconds\x18\x05 \x01(\r\"\xa8\x01\n\x12\x45nvironmentMetrics\x12\x13\n\x0btemperature\x18\x01 \x01(\x02\x12\x19\n\x11relative_humidity\x18\x02 \x01(\x02\x12\x1b\n\x13\x62\x61rometric_pressure\x18\x03 \x01(\x02\x12\x16\n\x0egas_resistance\x18\x04 \x01(\x02\x12\x0f\n\x07voltage\x18\x05 \x01(\x02\x12\x0f\n\x07\x63urrent\x18\x06 \x01(\x02\x12\x0b\n\x03iaq\x18\x07 \x01(\r\"\x8c\x01\n\x0cPowerMetrics\x12\x13\n\x0b\x63h1_voltage\x18\x01 \x01(\x02\x12\x13\n\x0b\x63h1_current\x18\x02 \x01(\x02\x12\x13\n\x0b\x63h2_voltage\x18\x03 \x01(\x02\x12\x13\n\x0b\x63h2_current\x18\x04 \x01(\x02\x12\x13\n\x0b\x63h3_voltage\x18\x05 \x01(\x02\x12\x13\n\x0b\x63h3_current\x18\x06 \x01(\x02\"\xbf\x02\n\x11\x41irQualityMetrics\x12\x15\n\rpm10_standard\x18\x01 \x01(\r\x12\x15\n\rpm25_standard\x18\x02 \x01(\r\x12\x16\n\x0epm100_standard\x18\x03 \x01(\r\x12\x1a\n\x12pm10_environmental\x18\x04 \x01(\r\x12\x1a\n\x12pm25_environmental\x18\x05 \x01(\r\x12\x1b\n\x13pm100_environmental\x18\x06 \x01(\r\x12\x16\n\x0eparticles_03um\x18\x07 \x01(\r\x12\x16\n\x0eparticles_05um\x18\x08 \x01(\r\x12\x16\n\x0eparticles_10um\x18\t \x01(\r\x12\x16\n\x0eparticles_25um\x18\n \x01(\r\x12\x16\n\x0eparticles_50um\x18\x0b \x01(\r\x12\x17\n\x0fparticles_100um\x18\x0c \x01(\r\"\x89\x02\n\tTelemetry\x12\x0c\n\x04time\x18\x01 \x01(\x07\x12\x33\n\x0e\x64\x65vice_metrics\x18\x02 \x01(\x0b\x32\x19.meshtastic.DeviceMetricsH\x00\x12=\n\x13\x65nvironment_metrics\x18\x03 \x01(\x0b\x32\x1e.meshtastic.EnvironmentMetricsH\x00\x12<\n\x13\x61ir_quality_metrics\x18\x04 \x01(\x0b\x32\x1d.meshtastic.AirQualityMetricsH\x00\x12\x31\n\rpower_metrics\x18\x05 \x01(\x0b\x32\x18.meshtastic.PowerMetricsH\x00\x42\t\n\x07variant*\xe0\x01\n\x13TelemetrySensorType\x12\x10\n\x0cSENSOR_UNSET\x10\x00\x12\n\n\x06\x42ME280\x10\x01\x12\n\n\x06\x42ME680\x10\x02\x12\x0b\n\x07MCP9808\x10\x03\x12\n\n\x06INA260\x10\x04\x12\n\n\x06INA219\x10\x05\x12\n\n\x06\x42MP280\x10\x06\x12\t\n\x05SHTC3\x10\x07\x12\t\n\x05LPS22\x10\x08\x12\x0b\n\x07QMC6310\x10\t\x12\x0b\n\x07QMI8658\x10\n\x12\x0c\n\x08QMC5883L\x10\x0b\x12\t\n\x05SHT31\x10\x0c\x12\x0c\n\x08PMSA003I\x10\r\x12\x0b\n\x07INA3221\x10\x0e\x12\n\n\x06\x42MP085\x10\x0f\x42\x64\n\x13\x63om.geeksville.meshB\x0fTelemetryProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3') DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1ameshtastic/telemetry.proto\x12\nmeshtastic\"\x81\x01\n\rDeviceMetrics\x12\x15\n\rbattery_level\x18\x01 \x01(\r\x12\x0f\n\x07voltage\x18\x02 \x01(\x02\x12\x1b\n\x13\x63hannel_utilization\x18\x03 \x01(\x02\x12\x13\n\x0b\x61ir_util_tx\x18\x04 \x01(\x02\x12\x16\n\x0euptime_seconds\x18\x05 \x01(\r\"\xda\x01\n\x12\x45nvironmentMetrics\x12\x13\n\x0btemperature\x18\x01 \x01(\x02\x12\x19\n\x11relative_humidity\x18\x02 \x01(\x02\x12\x1b\n\x13\x62\x61rometric_pressure\x18\x03 \x01(\x02\x12\x16\n\x0egas_resistance\x18\x04 \x01(\x02\x12\x0f\n\x07voltage\x18\x05 \x01(\x02\x12\x0f\n\x07\x63urrent\x18\x06 \x01(\x02\x12\x0b\n\x03iaq\x18\x07 \x01(\r\x12\x10\n\x08\x64istance\x18\x08 \x01(\x02\x12\x0b\n\x03lux\x18\t \x01(\x02\x12\x11\n\twhite_lux\x18\n \x01(\x02\"\x8c\x01\n\x0cPowerMetrics\x12\x13\n\x0b\x63h1_voltage\x18\x01 \x01(\x02\x12\x13\n\x0b\x63h1_current\x18\x02 \x01(\x02\x12\x13\n\x0b\x63h2_voltage\x18\x03 \x01(\x02\x12\x13\n\x0b\x63h2_current\x18\x04 \x01(\x02\x12\x13\n\x0b\x63h3_voltage\x18\x05 \x01(\x02\x12\x13\n\x0b\x63h3_current\x18\x06 \x01(\x02\"\xbf\x02\n\x11\x41irQualityMetrics\x12\x15\n\rpm10_standard\x18\x01 \x01(\r\x12\x15\n\rpm25_standard\x18\x02 \x01(\r\x12\x16\n\x0epm100_standard\x18\x03 \x01(\r\x12\x1a\n\x12pm10_environmental\x18\x04 \x01(\r\x12\x1a\n\x12pm25_environmental\x18\x05 \x01(\r\x12\x1b\n\x13pm100_environmental\x18\x06 \x01(\r\x12\x16\n\x0eparticles_03um\x18\x07 \x01(\r\x12\x16\n\x0eparticles_05um\x18\x08 \x01(\r\x12\x16\n\x0eparticles_10um\x18\t \x01(\r\x12\x16\n\x0eparticles_25um\x18\n \x01(\r\x12\x16\n\x0eparticles_50um\x18\x0b \x01(\r\x12\x17\n\x0fparticles_100um\x18\x0c \x01(\r\"\x89\x02\n\tTelemetry\x12\x0c\n\x04time\x18\x01 \x01(\x07\x12\x33\n\x0e\x64\x65vice_metrics\x18\x02 \x01(\x0b\x32\x19.meshtastic.DeviceMetricsH\x00\x12=\n\x13\x65nvironment_metrics\x18\x03 \x01(\x0b\x32\x1e.meshtastic.EnvironmentMetricsH\x00\x12<\n\x13\x61ir_quality_metrics\x18\x04 \x01(\x0b\x32\x1d.meshtastic.AirQualityMetricsH\x00\x12\x31\n\rpower_metrics\x18\x05 \x01(\x0b\x32\x18.meshtastic.PowerMetricsH\x00\x42\t\n\x07variant*\xcb\x02\n\x13TelemetrySensorType\x12\x10\n\x0cSENSOR_UNSET\x10\x00\x12\n\n\x06\x42ME280\x10\x01\x12\n\n\x06\x42ME680\x10\x02\x12\x0b\n\x07MCP9808\x10\x03\x12\n\n\x06INA260\x10\x04\x12\n\n\x06INA219\x10\x05\x12\n\n\x06\x42MP280\x10\x06\x12\t\n\x05SHTC3\x10\x07\x12\t\n\x05LPS22\x10\x08\x12\x0b\n\x07QMC6310\x10\t\x12\x0b\n\x07QMI8658\x10\n\x12\x0c\n\x08QMC5883L\x10\x0b\x12\t\n\x05SHT31\x10\x0c\x12\x0c\n\x08PMSA003I\x10\r\x12\x0b\n\x07INA3221\x10\x0e\x12\n\n\x06\x42MP085\x10\x0f\x12\x0c\n\x08RCWL9620\x10\x10\x12\t\n\x05SHT4X\x10\x11\x12\x0c\n\x08VEML7700\x10\x12\x12\x0c\n\x08MLX90632\x10\x13\x12\x0b\n\x07OPT3001\x10\x14\x12\x0c\n\x08LTR390UV\x10\x15\x12\x0e\n\nTSL25911FN\x10\x16\x12\t\n\x05\x41HT10\x10\x17\x42\x64\n\x13\x63om.geeksville.meshB\x0fTelemetryProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.telemetry_pb2', globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.telemetry_pb2', globals())
@@ -21,16 +21,16 @@ if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None DESCRIPTOR._options = None
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\017TelemetryProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000' DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\017TelemetryProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
_TELEMETRYSENSORTYPE._serialized_start=1079 _TELEMETRYSENSORTYPE._serialized_start=1129
_TELEMETRYSENSORTYPE._serialized_end=1303 _TELEMETRYSENSORTYPE._serialized_end=1460
_DEVICEMETRICS._serialized_start=43 _DEVICEMETRICS._serialized_start=43
_DEVICEMETRICS._serialized_end=172 _DEVICEMETRICS._serialized_end=172
_ENVIRONMENTMETRICS._serialized_start=175 _ENVIRONMENTMETRICS._serialized_start=175
_ENVIRONMENTMETRICS._serialized_end=343 _ENVIRONMENTMETRICS._serialized_end=393
_POWERMETRICS._serialized_start=346 _POWERMETRICS._serialized_start=396
_POWERMETRICS._serialized_end=486 _POWERMETRICS._serialized_end=536
_AIRQUALITYMETRICS._serialized_start=489 _AIRQUALITYMETRICS._serialized_start=539
_AIRQUALITYMETRICS._serialized_end=808 _AIRQUALITYMETRICS._serialized_end=858
_TELEMETRY._serialized_start=811 _TELEMETRY._serialized_start=861
_TELEMETRY._serialized_end=1076 _TELEMETRY._serialized_end=1126
# @@protoc_insertion_point(module_scope) # @@protoc_insertion_point(module_scope)

View File

@@ -86,6 +86,38 @@ class _TelemetrySensorTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wra
""" """
BMP085/BMP180 High accuracy temperature and pressure (older Version of BMP280) BMP085/BMP180 High accuracy temperature and pressure (older Version of BMP280)
""" """
RCWL9620: _TelemetrySensorType.ValueType # 16
"""
RCWL-9620 Doppler Radar Distance Sensor, used for water level detection
"""
SHT4X: _TelemetrySensorType.ValueType # 17
"""
Sensirion High accuracy temperature and humidity
"""
VEML7700: _TelemetrySensorType.ValueType # 18
"""
VEML7700 high accuracy ambient light(Lux) digital 16-bit resolution sensor.
"""
MLX90632: _TelemetrySensorType.ValueType # 19
"""
MLX90632 non-contact IR temperature sensor.
"""
OPT3001: _TelemetrySensorType.ValueType # 20
"""
TI OPT3001 Ambient Light Sensor
"""
LTR390UV: _TelemetrySensorType.ValueType # 21
"""
Lite On LTR-390UV-01 UV Light Sensor
"""
TSL25911FN: _TelemetrySensorType.ValueType # 22
"""
AMS TSL25911FN RGB Light Sensor
"""
AHT10: _TelemetrySensorType.ValueType # 23
"""
AHT10 Integrated temperature and humidity sensor
"""
class TelemetrySensorType(_TelemetrySensorType, metaclass=_TelemetrySensorTypeEnumTypeWrapper): class TelemetrySensorType(_TelemetrySensorType, metaclass=_TelemetrySensorTypeEnumTypeWrapper):
""" """
@@ -156,6 +188,38 @@ BMP085: TelemetrySensorType.ValueType # 15
""" """
BMP085/BMP180 High accuracy temperature and pressure (older Version of BMP280) BMP085/BMP180 High accuracy temperature and pressure (older Version of BMP280)
""" """
RCWL9620: TelemetrySensorType.ValueType # 16
"""
RCWL-9620 Doppler Radar Distance Sensor, used for water level detection
"""
SHT4X: TelemetrySensorType.ValueType # 17
"""
Sensirion High accuracy temperature and humidity
"""
VEML7700: TelemetrySensorType.ValueType # 18
"""
VEML7700 high accuracy ambient light(Lux) digital 16-bit resolution sensor.
"""
MLX90632: TelemetrySensorType.ValueType # 19
"""
MLX90632 non-contact IR temperature sensor.
"""
OPT3001: TelemetrySensorType.ValueType # 20
"""
TI OPT3001 Ambient Light Sensor
"""
LTR390UV: TelemetrySensorType.ValueType # 21
"""
Lite On LTR-390UV-01 UV Light Sensor
"""
TSL25911FN: TelemetrySensorType.ValueType # 22
"""
AMS TSL25911FN RGB Light Sensor
"""
AHT10: TelemetrySensorType.ValueType # 23
"""
AHT10 Integrated temperature and humidity sensor
"""
global___TelemetrySensorType = TelemetrySensorType global___TelemetrySensorType = TelemetrySensorType
@typing_extensions.final @typing_extensions.final
@@ -219,6 +283,9 @@ class EnvironmentMetrics(google.protobuf.message.Message):
VOLTAGE_FIELD_NUMBER: builtins.int VOLTAGE_FIELD_NUMBER: builtins.int
CURRENT_FIELD_NUMBER: builtins.int CURRENT_FIELD_NUMBER: builtins.int
IAQ_FIELD_NUMBER: builtins.int IAQ_FIELD_NUMBER: builtins.int
DISTANCE_FIELD_NUMBER: builtins.int
LUX_FIELD_NUMBER: builtins.int
WHITE_LUX_FIELD_NUMBER: builtins.int
temperature: builtins.float temperature: builtins.float
""" """
Temperature measured Temperature measured
@@ -248,6 +315,18 @@ class EnvironmentMetrics(google.protobuf.message.Message):
relative scale IAQ value as measured by Bosch BME680 . value 0-500. relative scale IAQ value as measured by Bosch BME680 . value 0-500.
Belongs to Air Quality but is not particle but VOC measurement. Other VOC values can also be put in here. Belongs to Air Quality but is not particle but VOC measurement. Other VOC values can also be put in here.
""" """
distance: builtins.float
"""
RCWL9620 Doppler Radar Distance Sensor, used for water level detection. Float value in mm.
"""
lux: builtins.float
"""
VEML7700 high accuracy ambient light(Lux) digital 16-bit resolution sensor.
"""
white_lux: builtins.float
"""
VEML7700 high accuracy white light(irradiance) not calibrated digital 16-bit resolution sensor.
"""
def __init__( def __init__(
self, self,
*, *,
@@ -258,8 +337,11 @@ class EnvironmentMetrics(google.protobuf.message.Message):
voltage: builtins.float = ..., voltage: builtins.float = ...,
current: builtins.float = ..., current: builtins.float = ...,
iaq: builtins.int = ..., iaq: builtins.int = ...,
distance: builtins.float = ...,
lux: builtins.float = ...,
white_lux: builtins.float = ...,
) -> None: ... ) -> None: ...
def ClearField(self, field_name: typing_extensions.Literal["barometric_pressure", b"barometric_pressure", "current", b"current", "gas_resistance", b"gas_resistance", "iaq", b"iaq", "relative_humidity", b"relative_humidity", "temperature", b"temperature", "voltage", b"voltage"]) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["barometric_pressure", b"barometric_pressure", "current", b"current", "distance", b"distance", "gas_resistance", b"gas_resistance", "iaq", b"iaq", "lux", b"lux", "relative_humidity", b"relative_humidity", "temperature", b"temperature", "voltage", b"voltage", "white_lux", b"white_lux"]) -> None: ...
global___EnvironmentMetrics = EnvironmentMetrics global___EnvironmentMetrics = EnvironmentMetrics

View File

@@ -44,7 +44,7 @@ def test_init_onNodeInfoReceive(caplog, iface_with_nodes):
iface = iface_with_nodes iface = iface_with_nodes
iface.myInfo.my_node_num = 2475227164 iface.myInfo.my_node_num = 2475227164
packet = { packet = {
"from": "foo", "from": 4808675309,
"decoded": { "decoded": {
"user": { "user": {
"id": "bar", "id": "bar",

View File

@@ -724,29 +724,79 @@ def test_main_sendtext_with_dest(mock_findPorts, mock_serial, mocked_open, mock_
assert re.search(r"Warning: There were no self.nodes.", caplog.text, re.MULTILINE) assert re.search(r"Warning: There were no self.nodes.", caplog.text, re.MULTILINE)
assert err == "" assert err == ""
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_main_removeposition_invalid(capsys):
"""Test --remove-position with an invalid dest"""
sys.argv = ["", "--remove-position", "--dest", "!12345678"]
mt_config.args = sys.argv
iface = MagicMock(autospec=SerialInterface)
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo:
main()
out, err = capsys.readouterr()
assert re.search(r"Connected to radio", out, re.MULTILINE)
assert re.search(r"remote nodes is not supported", out, re.MULTILINE)
assert err == ""
mo.assert_called()
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_main_setlat_invalid(capsys):
"""Test --setlat with an invalid dest"""
sys.argv = ["", "--setlat", "37.5", "--dest", "!12345678"]
mt_config.args = sys.argv
iface = MagicMock(autospec=SerialInterface)
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo:
main()
out, err = capsys.readouterr()
assert re.search(r"Connected to radio", out, re.MULTILINE)
assert re.search(r"remote nodes is not supported", out, re.MULTILINE)
assert err == ""
mo.assert_called()
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_main_removeposition(capsys):
"""Test --remove-position"""
sys.argv = ["", "--remove-position"]
mt_config.args = sys.argv
mocked_node = MagicMock(autospec=Node)
def mock_removeFixedPosition():
print("inside mocked removeFixedPosition")
mocked_node.removeFixedPosition.side_effect = mock_removeFixedPosition
iface = MagicMock(autospec=SerialInterface)
iface.localNode = mocked_node
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo:
main()
out, err = capsys.readouterr()
assert re.search(r"Connected to radio", out, re.MULTILINE)
assert re.search(r"Removing fixed position", out, re.MULTILINE)
assert re.search(r"inside mocked removeFixedPosition", out, re.MULTILINE)
assert err == ""
mo.assert_called()
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config") @pytest.mark.usefixtures("reset_mt_config")
def test_main_setlat(capsys): def test_main_setlat(capsys):
"""Test --sendlat""" """Test --setlat"""
sys.argv = ["", "--setlat", "37.5"] sys.argv = ["", "--setlat", "37.5"]
mt_config.args = sys.argv mt_config.args = sys.argv
mocked_node = MagicMock(autospec=Node) mocked_node = MagicMock(autospec=Node)
def mock_writeConfig(): def mock_setFixedPosition(lat, lon, alt):
print("inside mocked writeConfig") print("inside mocked setFixedPosition")
mocked_node.writeConfig.side_effect = mock_writeConfig
iface = MagicMock(autospec=SerialInterface)
def mock_sendPosition(lat, lon, alt):
print("inside mocked sendPosition")
print(f"{lat} {lon} {alt}") print(f"{lat} {lon} {alt}")
iface.sendPosition.side_effect = mock_sendPosition mocked_node.setFixedPosition.side_effect = mock_setFixedPosition
iface.localNode.return_value = mocked_node
iface = MagicMock(autospec=SerialInterface)
iface.localNode = mocked_node
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo:
main() main()
@@ -754,8 +804,7 @@ def test_main_setlat(capsys):
assert re.search(r"Connected to radio", out, re.MULTILINE) assert re.search(r"Connected to radio", out, re.MULTILINE)
assert re.search(r"Fixing latitude", out, re.MULTILINE) assert re.search(r"Fixing latitude", out, re.MULTILINE)
assert re.search(r"Setting device position", out, re.MULTILINE) assert re.search(r"Setting device position", out, re.MULTILINE)
assert re.search(r"inside mocked sendPosition", out, re.MULTILINE) assert re.search(r"inside mocked setFixedPosition", out, re.MULTILINE)
# TODO: Why does this not work? assert re.search(r'inside mocked writeConfig', out, re.MULTILINE)
assert err == "" assert err == ""
mo.assert_called() mo.assert_called()
@@ -769,19 +818,14 @@ def test_main_setlon(capsys):
mocked_node = MagicMock(autospec=Node) mocked_node = MagicMock(autospec=Node)
def mock_writeConfig(): def mock_setFixedPosition(lat, lon, alt):
print("inside mocked writeConfig") print("inside mocked setFixedPosition")
mocked_node.writeConfig.side_effect = mock_writeConfig
iface = MagicMock(autospec=SerialInterface)
def mock_sendPosition(lat, lon, alt):
print("inside mocked sendPosition")
print(f"{lat} {lon} {alt}") print(f"{lat} {lon} {alt}")
iface.sendPosition.side_effect = mock_sendPosition mocked_node.setFixedPosition.side_effect = mock_setFixedPosition
iface.localNode.return_value = mocked_node
iface = MagicMock(autospec=SerialInterface)
iface.localNode = mocked_node
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo:
main() main()
@@ -789,8 +833,7 @@ def test_main_setlon(capsys):
assert re.search(r"Connected to radio", out, re.MULTILINE) assert re.search(r"Connected to radio", out, re.MULTILINE)
assert re.search(r"Fixing longitude", out, re.MULTILINE) assert re.search(r"Fixing longitude", out, re.MULTILINE)
assert re.search(r"Setting device position", out, re.MULTILINE) assert re.search(r"Setting device position", out, re.MULTILINE)
assert re.search(r"inside mocked sendPosition", out, re.MULTILINE) assert re.search(r"inside mocked setFixedPosition", out, re.MULTILINE)
# TODO: Why does this not work? assert re.search(r'inside mocked writeConfig', out, re.MULTILINE)
assert err == "" assert err == ""
mo.assert_called() mo.assert_called()
@@ -804,19 +847,14 @@ def test_main_setalt(capsys):
mocked_node = MagicMock(autospec=Node) mocked_node = MagicMock(autospec=Node)
def mock_writeConfig(): def mock_setFixedPosition(lat, lon, alt):
print("inside mocked writeConfig") print("inside mocked setFixedPosition")
mocked_node.writeConfig.side_effect = mock_writeConfig
iface = MagicMock(autospec=SerialInterface)
def mock_sendPosition(lat, lon, alt):
print("inside mocked sendPosition")
print(f"{lat} {lon} {alt}") print(f"{lat} {lon} {alt}")
iface.sendPosition.side_effect = mock_sendPosition mocked_node.setFixedPosition.side_effect = mock_setFixedPosition
iface.localNode.return_value = mocked_node
iface = MagicMock(autospec=SerialInterface)
iface.localNode = mocked_node
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo:
main() main()
@@ -824,8 +862,7 @@ def test_main_setalt(capsys):
assert re.search(r"Connected to radio", out, re.MULTILINE) assert re.search(r"Connected to radio", out, re.MULTILINE)
assert re.search(r"Fixing altitude", out, re.MULTILINE) assert re.search(r"Fixing altitude", out, re.MULTILINE)
assert re.search(r"Setting device position", out, re.MULTILINE) assert re.search(r"Setting device position", out, re.MULTILINE)
assert re.search(r"inside mocked sendPosition", out, re.MULTILINE) assert re.search(r"inside mocked setFixedPosition", out, re.MULTILINE)
# TODO: Why does this not work? assert re.search(r'inside mocked writeConfig', out, re.MULTILINE)
assert err == "" assert err == ""
mo.assert_called() mo.assert_called()

View File

@@ -6,7 +6,7 @@ from unittest.mock import MagicMock, patch
import pytest import pytest
from .. import mesh_pb2, BROADCAST_ADDR, LOCAL_ADDR from .. import mesh_pb2, config_pb2, BROADCAST_ADDR, LOCAL_ADDR
from ..mesh_interface import MeshInterface from ..mesh_interface import MeshInterface
from ..node import Node from ..node import Node
@@ -36,12 +36,15 @@ def test_MeshInterface(capsys):
"lastHeard": 1640204888, "lastHeard": 1640204888,
} }
iface.nodes = {NODE_ID: node} iface.nodes = {NODE_ID: node}
iface.nodesByNum = {NODE_NUM: node} iface.nodesByNum = {NODE_NUM: node}
myInfo = MagicMock() myInfo = MagicMock()
iface.myInfo = myInfo iface.myInfo = myInfo
iface.localNode.localConfig.lora.CopyFrom(config_pb2.Config.LoRaConfig())
iface.showInfo() iface.showInfo()
iface.localNode.showInfo() iface.localNode.showInfo()
iface.showNodes() iface.showNodes()
@@ -585,7 +588,7 @@ def test_getOrCreateByNum_minimal(iface_with_nodes):
iface = iface_with_nodes iface = iface_with_nodes
iface.myInfo.my_node_num = 2475227164 iface.myInfo.my_node_num = 2475227164
tmp = iface._getOrCreateByNum(123) tmp = iface._getOrCreateByNum(123)
assert tmp == {"num": 123} assert tmp == {"num": 123, "user": {"hwModel": "UNSET", "id": "!0000007b", "shortName": "007b", "longName": "Meshtastic 007b"}}
@pytest.mark.unit @pytest.mark.unit

View File

@@ -6,7 +6,7 @@ from unittest.mock import MagicMock, patch
import pytest import pytest
# from ..admin_pb2 import AdminMessage from .. import localonly_pb2, config_pb2
from ..channel_pb2 import Channel # pylint: disable=E0611 from ..channel_pb2 import Channel # pylint: disable=E0611
from ..node import Node from ..node import Node
from ..serial_interface import SerialInterface from ..serial_interface import SerialInterface
@@ -19,21 +19,26 @@ from ..mesh_interface import MeshInterface
# from ..util import Timeout # from ..util import Timeout
# TODO @pytest.mark.unit
# @pytest.mark.unit def test_node(capsys):
# def test_node(capsys): """Test that we can instantiate a Node"""
# """Test that we can instantiate a Node""" iface = MagicMock(autospec=SerialInterface)
# anode = Node('foo', 'bar') with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo:
# radioConfig = RadioConfig() mo.localNode.getChannelByName.return_value = None
# anode.radioConfig = radioConfig mo.myInfo.max_channels = 8
# anode.showChannels() anode = Node(mo, "bar", noProto=True)
# anode.showInfo() lc = localonly_pb2.LocalConfig()
# out, err = capsys.readouterr() anode.localConfig = lc
# assert re.search(r'Preferences', out) lc.lora.CopyFrom(config_pb2.Config.LoRaConfig())
# assert re.search(r'Channels', out) anode.moduleConfig = localonly_pb2.LocalModuleConfig()
# assert re.search(r'Primary channel URL', out) anode.showInfo()
# assert err == '' out, err = capsys.readouterr()
assert re.search(r'Preferences', out)
assert re.search(r'Module preferences', out)
assert re.search(r'Channels', out)
assert re.search(r'Primary channel URL', out)
assert not re.search(r'remote node', out)
assert err == ''
# TODO # TODO
# @pytest.mark.unit # @pytest.mark.unit

View File

@@ -6,6 +6,7 @@ from unittest.mock import mock_open, patch
import pytest import pytest
from ..serial_interface import SerialInterface from ..serial_interface import SerialInterface
from .. import config_pb2
@pytest.mark.unit @pytest.mark.unit
@@ -20,6 +21,7 @@ def test_SerialInterface_single_port(
): ):
"""Test that we can instantiate a SerialInterface with a single port""" """Test that we can instantiate a SerialInterface with a single port"""
iface = SerialInterface(noProto=True) iface = SerialInterface(noProto=True)
iface.localNode.localConfig.lora.CopyFrom(config_pb2.Config.LoRaConfig())
iface.showInfo() iface.showInfo()
iface.localNode.showInfo() iface.localNode.showInfo()
iface.close() iface.close()

View File

@@ -5,6 +5,7 @@ from unittest.mock import patch
import pytest import pytest
from .. import config_pb2
from ..tcp_interface import TCPInterface from ..tcp_interface import TCPInterface
@@ -13,6 +14,7 @@ def test_TCPInterface(capsys):
"""Test that we can instantiate a TCPInterface""" """Test that we can instantiate a TCPInterface"""
with patch("socket.socket") as mock_socket: with patch("socket.socket") as mock_socket:
iface = TCPInterface(hostname="localhost", noProto=True) iface = TCPInterface(hostname="localhost", noProto=True)
iface.localNode.localConfig.lora.CopyFrom(config_pb2.Config.LoRaConfig())
iface.myConnect() iface.myConnect()
iface.showInfo() iface.showInfo()
iface.localNode.showInfo() iface.localNode.showInfo()

View File

@@ -6,6 +6,7 @@ import re
from unittest.mock import patch from unittest.mock import patch
import pytest import pytest
from hypothesis import given, strategies as st
from meshtastic.supported_device import SupportedDevice from meshtastic.supported_device import SupportedDevice
from meshtastic.mesh_pb2 import MyNodeInfo from meshtastic.mesh_pb2 import MyNodeInfo
@@ -33,6 +34,7 @@ from meshtastic.util import (
stripnl, stripnl,
support_info, support_info,
message_to_json, message_to_json,
Acknowledgment
) )
@@ -63,6 +65,7 @@ def test_fromStr():
assert fromStr("123") == 123 assert fromStr("123") == 123
assert fromStr("abc") == "abc" assert fromStr("abc") == "abc"
assert fromStr("123456789") == 123456789 assert fromStr("123456789") == 123456789
assert fromStr("base64:Zm9vIGJhciBiYXo=") == b"foo bar baz"
@pytest.mark.unitslow @pytest.mark.unitslow
@@ -555,3 +558,39 @@ def test_message_to_json_shows_all():
actual = json.loads(message_to_json(MyNodeInfo())) actual = json.loads(message_to_json(MyNodeInfo()))
expected = { "myNodeNum": 0, "rebootCount": 0, "minAppVersion": 0 } expected = { "myNodeNum": 0, "rebootCount": 0, "minAppVersion": 0 }
assert actual == expected assert actual == expected
@pytest.mark.unit
def test_acknowledgement_reset():
"""
Test that the reset method can set all fields back to False
"""
test_ack_obj = Acknowledgment()
# everything's set to False; let's set it to True to get a good test
test_ack_obj.receivedAck = True
test_ack_obj.receivedNak = True
test_ack_obj.receivedImplAck = True
test_ack_obj.receivedTraceRoute = True
test_ack_obj.receivedTelemetry = True
test_ack_obj.receivedPosition = True
test_ack_obj.reset()
assert test_ack_obj.receivedAck is False
assert test_ack_obj.receivedNak is False
assert test_ack_obj.receivedImplAck is False
assert test_ack_obj.receivedTraceRoute is False
assert test_ack_obj.receivedTelemetry is False
assert test_ack_obj.receivedPosition is False
@given(a_string=st.text(
alphabet=st.characters(
codec='ascii',
min_codepoint=0x5F,
max_codepoint=0x7A,
exclude_characters=r'`',
)).filter(
lambda x: x not in [''] and x[0] not in "_" and x[-1] not in '_' and not re.search(r'__', x)
))
def test_roundtrip_snake_to_camel_camel_to_snake(a_string):
"""Test that snake_to_camel and camel_to_snake roundtrip each other"""
value0 = snake_to_camel(a_string=a_string)
value1 = camel_to_snake(a_string=value0)
assert a_string == value1, (a_string, value1)

View File

@@ -11,9 +11,10 @@ import threading
import time import time
import traceback import traceback
from queue import Queue from queue import Queue
from typing import Union from typing import List, NoReturn, Union
from google.protobuf.json_format import MessageToJson from google.protobuf.json_format import MessageToJson
from google.protobuf.message import Message
import packaging.version as pkg_version import packaging.version as pkg_version
import requests import requests
@@ -103,7 +104,7 @@ def pskToString(psk: bytes):
return "secret" return "secret"
def stripnl(s): def stripnl(s) -> str:
"""Remove newlines from a string (and remove extra whitespace)""" """Remove newlines from a string (and remove extra whitespace)"""
s = str(s).replace("\n", " ") s = str(s).replace("\n", " ")
return " ".join(s.split()) return " ".join(s.split())
@@ -122,7 +123,7 @@ def catchAndIgnore(reason, closure):
logging.error(f"Exception thrown in {reason}: {ex}") logging.error(f"Exception thrown in {reason}: {ex}")
def findPorts(eliminate_duplicates=False): def findPorts(eliminate_duplicates: bool=False) -> List[str]:
"""Find all ports that might have meshtastic devices """Find all ports that might have meshtastic devices
eliminate_duplicates will run the eliminate_duplicate_port() on the collection eliminate_duplicates will run the eliminate_duplicate_port() on the collection
@@ -263,7 +264,7 @@ class DeferredExecution:
print(traceback.format_exc()) print(traceback.format_exc())
def our_exit(message, return_value=1): def our_exit(message, return_value=1) -> NoReturn:
"""Print the message and return a value. """Print the message and return a value.
return_value defaults to 1 (non-successful) return_value defaults to 1 (non-successful)
""" """
@@ -627,6 +628,8 @@ def check_if_newer_version():
return pypi_version return pypi_version
def message_to_json(message):
"Return protobuf message as JSON. Always print all fields, even when not present in data." def message_to_json(message: Message, multiline: bool=False) -> str:
return stripnl(MessageToJson(message, always_print_fields_with_no_presence=True)) """Return protobuf message as JSON. Always print all fields, even when not present in data."""
json = MessageToJson(message, always_print_fields_with_no_presence=True)
return stripnl(json) if not multiline else json

View File

@@ -14,6 +14,7 @@ autopep8
pylint pylint
pytest pytest
pytest-cov pytest-cov
hypothesis
pyyaml pyyaml
pytap2 pytap2
pdoc3 pdoc3
@@ -22,7 +23,7 @@ bleak
packaging packaging
mypy mypy
mypy-protobuf mypy-protobuf
types-protobuf types-protobuf>=5.26.0
types-tabulate types-tabulate
types-requests types-requests
types-setuptools types-setuptools

View File

@@ -13,7 +13,7 @@ with open("README.md", "r") as fh:
# This call to setup() does all the work # This call to setup() does all the work
setup( setup(
name="meshtastic", name="meshtastic",
version="2.3.5", version="2.3.10",
description="Python API & client shell for talking to Meshtastic devices", description="Python API & client shell for talking to Meshtastic devices",
long_description=long_description, long_description=long_description,
long_description_content_type="text/markdown", long_description_content_type="text/markdown",