Compare commits

...

54 Commits
2.3.5 ... 2.3.9

Author SHA1 Message Date
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
Ian McEwen
70c5a30b77 protobufs: v2.3.6 2024-04-18 14:08:40 -07:00
Ian McEwen
9f0ba7aeae Merge pull request #549 from ianmcorvidae/export-import-config-altitude
Don't export null altitude, but support importing null lat/lon/alt as 0s
2024-04-18 12:32:04 -07:00
Ian McEwen
4226201423 Don't export null altitude, but support importing null lat/lon/alt as 0s. Fixes #548 2024-04-18 12:15:58 -07:00
Ian McEwen
bdf3a24be1 Merge pull request #546 from ianmcorvidae/request-position
Add a `--request-position` argument to request positions from nodes
2024-04-16 15:45:50 -07:00
Ian McEwen
e8ba5581f6 Add a --request-position argument to request positions from nodes 2024-04-16 15:43:24 -07:00
Ian McEwen
948846e0f1 Send the position the node already thinks it's at when --no-time is absent, rather than an empty position 2024-04-16 15:13:53 -07:00
Ian McEwen
6c4dbb6fe6 Fix quotes 2024-04-16 14:58:50 -07:00
Ian McEwen
afbabf9538 Merge branch 'bugfix-466-yaml-import' 2024-04-16 13:56:00 -07:00
Ian McEwen
d8107122a2 Merge remote-tracking branch 'danwelch3/bugfix-466-yaml-import' into bugfix-466-yaml-import 2024-04-16 13:53:54 -07:00
Dan Welch
03c1f08e45 Fix Lint Error
- add docstring to traverseConfig function
2024-04-16 14:46:35 -06:00
Ian McEwen
760fcfcea7 Merge pull request #544 from holdenweb/new-globals
Refactor to avoid the use of a special global object.
2024-04-16 13:17:16 -07:00
Ian McEwen
a4830f5f62 Treat a message as an ack if there is an errorReason but it's set to NONE, not just if the errorReason is absent 2024-04-16 13:10:18 -07:00
github-actions
2b1f337a41 bump version 2024-04-15 06:47:12 +00:00
Steve Holden
a07e853f69 Refactor to remove pylint issues.
Since one of pylint's complains was that the globals module was
shadowing the built-in, and since the name `config` was already
is use in several modules, globals.py was renamed as mt_config.py.
All tests now pass, and the only remaining local pylint errors
relate to the protobuf code, I'm hoping this will make the PR
valid.
2024-04-10 17:42:44 +01:00
Steve Holden
0d57449030 Begin to rationalise test data.
Also refactor to silence some CI issues.
2024-04-10 14:25:17 +01:00
Steve Holden
067cddd354 Refactor to avoid the use of a special global object.
The global object formerly used is now replaced by direct use
of the namespace opf the globals module. This eliminates the
redundant getters and setters and simplifies the code for
future maintainers.

Note that the globals module name conflicts (harmlessly at
present) with a Python built-in function. A future commit
should rename it `config` to remove this clash and better
represent its intended purpose.
2024-04-10 10:03:12 +01:00
AeroXuk
4af1b322da Also accept nested settings in module_config.
Show full config path in output when nested.
2024-04-09 12:27:08 +01:00
AeroXuk
c580df15e1 Merge 'master' into 'bugfix-466-yaml-import'. 2024-04-09 12:20:04 +01:00
Dan Welch
590b60fefb Allow 'configure' to import yaml settings nested deepr than 2 levels
Resolves meshtastic/python#466
2023-12-06 11:52:22 -07:00
30 changed files with 797 additions and 625 deletions

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

@@ -16,16 +16,15 @@ from pubsub import pub # type: ignore[import-untyped]
import meshtastic.test import meshtastic.test
import meshtastic.util import meshtastic.util
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.globals import Globals 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"""
our_globals = Globals.getInstance() args = mt_config.args
args = our_globals.get_args()
try: try:
d = packet.get("decoded") d = packet.get("decoded")
logging.debug(f"in onReceive() d:{d}") logging.debug(f"in onReceive() d:{d}")
@@ -58,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"""
@@ -69,7 +73,7 @@ def getPref(node, comp_name):
# Note: protobufs has the keys in snake_case, so snake internally # Note: protobufs has the keys in snake_case, so snake internally
snake_name = meshtastic.util.camel_to_snake(name[1]) snake_name = meshtastic.util.camel_to_snake(name[1])
logging.debug(f"snake_name:{snake_name} camel_name:{camel_name}") logging.debug(f"snake_name:{snake_name} camel_name:{camel_name}")
logging.debug(f"use camel:{Globals.getInstance().get_camel_case()}") logging.debug(f"use camel:{mt_config.camel_case}")
# First validate the input # First validate the input
localConfig = node.localConfig localConfig = node.localConfig
@@ -86,7 +90,7 @@ def getPref(node, comp_name):
break break
if not found: if not found:
if Globals.getInstance().get_camel_case(): if mt_config.camel_case:
print( print(
f"{localConfig.__class__.__name__} and {moduleConfig.__class__.__name__} do not have an attribute {snake_name}." f"{localConfig.__class__.__name__} and {moduleConfig.__class__.__name__} do not have an attribute {snake_name}."
) )
@@ -105,7 +109,7 @@ def getPref(node, comp_name):
config_values = getattr(config, config_type.name) config_values = getattr(config, config_type.name)
if not wholeField: if not wholeField:
pref_value = getattr(config_values, pref.name) pref_value = getattr(config_values, pref.name)
if Globals.getInstance().get_camel_case(): if mt_config.camel_case:
print(f"{str(config_type.name)}.{camel_name}: {str(pref_value)}") print(f"{str(config_type.name)}.{camel_name}: {str(pref_value)}")
logging.debug( logging.debug(
f"{str(config_type.name)}.{camel_name}: {str(pref_value)}" f"{str(config_type.name)}.{camel_name}: {str(pref_value)}"
@@ -127,25 +131,46 @@ def getPref(node, comp_name):
def splitCompoundName(comp_name): def splitCompoundName(comp_name):
"""Split compound (dot separated) preference name into parts""" """Split compound (dot separated) preference name into parts"""
name = comp_name.split(".", 1) name = comp_name.split(".")
if len(name) != 2: if len(name) < 2:
name[0] = comp_name name[0] = comp_name
name.append(comp_name) name.append(comp_name)
return name return name
def traverseConfig(config_root, config, interface_config):
"""Iterate through current config level preferences and either traverse deeper if preference is a dict or set preference"""
snake_name = meshtastic.util.camel_to_snake(config_root)
for pref in config:
pref_name = f"{snake_name}.{pref}"
if isinstance(config[pref], dict):
traverseConfig(pref_name, config[pref], interface_config)
else:
setPref(
interface_config,
pref_name,
str(config[pref])
)
return True
def setPref(config, comp_name, valStr) -> bool: def setPref(config, comp_name, valStr) -> bool:
"""Set a channel or preferences value""" """Set a channel or preferences value"""
name = splitCompoundName(comp_name) name = splitCompoundName(comp_name)
snake_name = meshtastic.util.camel_to_snake(name[1]) snake_name = meshtastic.util.camel_to_snake(name[-1])
camel_name = meshtastic.util.snake_to_camel(name[1]) camel_name = meshtastic.util.snake_to_camel(name[-1])
logging.debug(f"snake_name:{snake_name}") logging.debug(f"snake_name:{snake_name}")
logging.debug(f"camel_name:{camel_name}") logging.debug(f"camel_name:{camel_name}")
objDesc = config.DESCRIPTOR objDesc = config.DESCRIPTOR
config_part = config
config_type = objDesc.fields_by_name.get(name[0]) config_type = objDesc.fields_by_name.get(name[0])
if config_type and config_type.message_type is not None:
for name_part in name[1:-1]:
part_snake_name = meshtastic.util.camel_to_snake((name_part))
config_part = getattr(config, config_type.name)
config_type = config_type.message_type.fields_by_name.get(part_snake_name)
pref = None pref = None
if config_type and config_type.message_type is not None: if config_type and config_type.message_type is not None:
pref = config_type.message_type.fields_by_name.get(snake_name) pref = config_type.message_type.fields_by_name.get(snake_name)
@@ -171,7 +196,7 @@ def setPref(config, comp_name, valStr) -> bool:
if e: if e:
val = e.number val = e.number
else: else:
if Globals.getInstance().get_camel_case(): if mt_config.camel_case:
print( print(
f"{name[0]}.{camel_name} does not have an enum called {val}, so you can not set it." f"{name[0]}.{camel_name} does not have an enum called {val}, so you can not set it."
) )
@@ -192,13 +217,13 @@ def setPref(config, comp_name, valStr) -> bool:
if snake_name != "ignore_incoming": if snake_name != "ignore_incoming":
try: try:
if config_type.message_type is not None: if config_type.message_type is not None:
config_values = getattr(config, config_type.name) config_values = getattr(config_part, config_type.name)
setattr(config_values, pref.name, val) setattr(config_values, pref.name, val)
else: else:
setattr(config, snake_name, val) setattr(config_part, snake_name, val)
except TypeError: except TypeError:
# The setter didn't like our arg type guess try again as a string # The setter didn't like our arg type guess try again as a string
config_values = getattr(config, 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:
if val == 0: if val == 0:
@@ -209,8 +234,8 @@ def setPref(config, comp_name, valStr) -> bool:
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_type.message_type.ignore_incoming.extend([val])
prefix = f"{name[0]}." 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 Globals.getInstance().get_camel_case(): if mt_config.camel_case:
print(f"Set {prefix}{camel_name} to {valStr}") print(f"Set {prefix}{camel_name} to {valStr}")
else: else:
print(f"Set {prefix}{snake_name} to {valStr}") print(f"Set {prefix}{snake_name} to {valStr}")
@@ -225,20 +250,21 @@ def onConnected(interface):
False # Should we wait for an acknowledgment if we send to a remote node? False # Should we wait for an acknowledgment if we send to a remote node?
) )
try: try:
our_globals = Globals.getInstance() args = mt_config.args
args = our_globals.get_args()
# do not print this line if we are exporting the config # do not print this line if we are exporting the config
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.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.0
lon = 0.0 lon = 0.0
# TODO: use getNode(args.dest) to be able to set it for a remote node
localConfig = interface.localNode.localConfig localConfig = interface.localNode.localConfig
if args.setalt: if args.setalt:
alt = int(args.setalt) alt = int(args.setalt)
@@ -259,7 +285,12 @@ def onConnected(interface):
interface.localNode.writeConfig("position") 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
interface.sendPosition() if interface.localNode.nodeNum in interface.nodesByNum and "position" in interface.nodesByNum[interface.localNode.nodeNum]:
# send the same position the node already knows, just to update time
position = interface.nodesByNum[interface.localNode.nodeNum]["position"]
interface.sendPosition(position.get("latitude", 0.0), position.get("longitude", 0.0), position.get("altitude", 0.0))
else:
interface.sendPosition()
if args.set_owner: if args.set_owner:
closeNow = True closeNow = True
@@ -343,6 +374,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
@@ -377,12 +413,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}"
) )
@@ -402,15 +434,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.dest == BROADCAST_ADDR:
meshtastic.util.our_exit("Warning: Must use a destination node ID.")
else:
channelIndex = mt_config.channel_index or 0
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:
@@ -477,7 +522,7 @@ def onConnected(interface):
print("Writing modified preferences to device") print("Writing modified preferences to device")
node.writeConfig(field) node.writeConfig(field)
else: else:
if Globals.getInstance().get_camel_case(): if mt_config.camel_case:
print( print(
f"{node.localConfig.__class__.__name__} and {node.moduleConfig.__class__.__name__} do not have an attribute {pref[0]}." f"{node.localConfig.__class__.__name__} and {node.moduleConfig.__class__.__name__} do not have an attribute {pref[0]}."
) )
@@ -534,15 +579,15 @@ def onConnected(interface):
localConfig = interface.localNode.localConfig localConfig = interface.localNode.localConfig
if "alt" in configuration["location"]: if "alt" in configuration["location"]:
alt = int(configuration["location"]["alt"]) alt = int(configuration["location"]["alt"] or 0)
localConfig.position.fixed_position = True localConfig.position.fixed_position = True
print(f"Fixing altitude at {alt} meters") print(f"Fixing altitude at {alt} meters")
if "lat" in configuration["location"]: if "lat" in configuration["location"]:
lat = float(configuration["location"]["lat"]) lat = float(configuration["location"]["lat"] or 0)
localConfig.position.fixed_position = True localConfig.position.fixed_position = True
print(f"Fixing latitude at {lat} degrees") print(f"Fixing latitude at {lat} degrees")
if "lon" in configuration["location"]: if "lon" in configuration["location"]:
lon = float(configuration["location"]["lon"]) lon = float(configuration["location"]["lon"] or 0)
localConfig.position.fixed_position = True localConfig.position.fixed_position = True
print(f"Fixing longitude at {lon} degrees") print(f"Fixing longitude at {lon} degrees")
print("Setting device position") print("Setting device position")
@@ -552,12 +597,7 @@ def onConnected(interface):
if "config" in configuration: if "config" in configuration:
localConfig = interface.getNode(args.dest).localConfig localConfig = interface.getNode(args.dest).localConfig
for section in configuration["config"]: for section in configuration["config"]:
for pref in configuration["config"][section]: traverseConfig(section, configuration["config"][section], localConfig)
setPref(
localConfig,
f"{meshtastic.util.camel_to_snake(section)}.{pref}",
str(configuration["config"][section][pref]),
)
interface.getNode(args.dest).writeConfig( interface.getNode(args.dest).writeConfig(
meshtastic.util.camel_to_snake(section) meshtastic.util.camel_to_snake(section)
) )
@@ -565,12 +605,7 @@ def onConnected(interface):
if "module_config" in configuration: if "module_config" in configuration:
moduleConfig = interface.getNode(args.dest).moduleConfig moduleConfig = interface.getNode(args.dest).moduleConfig
for section in configuration["module_config"]: for section in configuration["module_config"]:
for pref in configuration["module_config"][section]: traverseConfig(section, configuration["module_config"][section], moduleConfig)
setPref(
moduleConfig,
f"{meshtastic.util.camel_to_snake(section)}.{pref}",
str(configuration["module_config"][section][pref]),
)
interface.getNode(args.dest).writeConfig( interface.getNode(args.dest).writeConfig(
meshtastic.util.camel_to_snake(section) meshtastic.util.camel_to_snake(section)
) )
@@ -579,6 +614,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)
@@ -590,7 +628,7 @@ def onConnected(interface):
# handle changing channels # handle changing channels
if args.ch_add: if args.ch_add:
channelIndex = our_globals.get_channel_index() channelIndex = mt_config.channel_index
if channelIndex is not None: if channelIndex is not None:
# Since we set the channel index after adding a channel, don't allow --ch-index # Since we set the channel index after adding a channel, don't allow --ch-index
meshtastic.util.our_exit( meshtastic.util.our_exit(
@@ -621,12 +659,12 @@ def onConnected(interface):
n.writeChannel(ch.index) n.writeChannel(ch.index)
if channelIndex is None: if channelIndex is None:
print(f"Setting newly-added channel's {ch.index} as '--ch-index' for further modifications") print(f"Setting newly-added channel's {ch.index} as '--ch-index' for further modifications")
our_globals.set_channel_index(ch.index) mt_config.channel_index = ch.index
if args.ch_del: if args.ch_del:
closeNow = True closeNow = True
channelIndex = our_globals.get_channel_index() channelIndex = mt_config.channel_index
if channelIndex is None: if channelIndex is None:
meshtastic.util.our_exit( meshtastic.util.our_exit(
"Warning: Need to specify '--ch-index' for '--ch-del'.", 1 "Warning: Need to specify '--ch-index' for '--ch-del'.", 1
@@ -642,7 +680,7 @@ def onConnected(interface):
def setSimpleConfig(modem_preset): def setSimpleConfig(modem_preset):
"""Set one of the simple modem_config""" """Set one of the simple modem_config"""
channelIndex = our_globals.get_channel_index() channelIndex = mt_config.channel_index
if channelIndex is not None and channelIndex > 0: if channelIndex is not None and channelIndex > 0:
meshtastic.util.our_exit( meshtastic.util.our_exit(
"Warning: Cannot set modem preset for non-primary channel", 1 "Warning: Cannot set modem preset for non-primary channel", 1
@@ -677,12 +715,16 @@ def onConnected(interface):
if args.ch_set or args.ch_enable or args.ch_disable: if args.ch_set or args.ch_enable or args.ch_disable:
closeNow = True closeNow = True
channelIndex = our_globals.get_channel_index() channelIndex = mt_config.channel_index
if channelIndex is None: if channelIndex is None:
meshtastic.util.our_exit("Warning: Need to specify '--ch-index'.", 1) meshtastic.util.our_exit("Warning: Need to specify '--ch-index'.", 1)
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."
@@ -781,10 +823,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())
@@ -793,6 +839,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,7 +881,7 @@ def printConfig(config):
names = [] names = []
for field in config.message_type.fields: for field in config.message_type.fields:
tmp_name = f"{config_section.name}.{field.name}" tmp_name = f"{config_section.name}.{field.name}"
if Globals.getInstance().get_camel_case(): if mt_config.camel_case:
tmp_name = meshtastic.util.snake_to_camel(tmp_name) tmp_name = meshtastic.util.snake_to_camel(tmp_name)
names.append(tmp_name) names.append(tmp_name)
for temp_name in sorted(names): for temp_name in sorted(names):
@@ -877,23 +926,26 @@ def export_config(interface):
if owner_short: if owner_short:
configObj["owner_short"] = owner_short configObj["owner_short"] = owner_short
if channel_url: if channel_url:
if Globals.getInstance().get_camel_case(): if mt_config.camel_case:
configObj["channelUrl"] = channel_url configObj["channelUrl"] = channel_url
else: else:
configObj["channel_url"] = channel_url configObj["channel_url"] = channel_url
if lat or lon or alt: # lat and lon don't make much sense without the other (so fill with 0s), and alt isn't meaningful without both
configObj["location"] = {"lat": lat, "lon": lon, "alt": alt} if lat or lon:
configObj["location"] = {"lat": lat or float(0), "lon": lon or float(0)}
if alt:
configObj["location"]["alt"] = alt
config = MessageToDict(interface.localNode.localConfig) config = MessageToDict(interface.localNode.localConfig)
if config: if config:
# Convert inner keys to correct snake/camelCase # Convert inner keys to correct snake/camelCase
prefs = {} prefs = {}
for pref in config: for pref in config:
if Globals.getInstance().get_camel_case(): if mt_config.camel_case:
prefs[meshtastic.util.snake_to_camel(pref)] = config[pref] prefs[meshtastic.util.snake_to_camel(pref)] = config[pref]
else: else:
prefs[pref] = config[pref] prefs[pref] = config[pref]
if Globals.getInstance().get_camel_case(): if mt_config.camel_case:
configObj["config"] = config configObj["config"] = config
else: else:
configObj["config"] = config configObj["config"] = config
@@ -905,7 +957,7 @@ def export_config(interface):
for pref in module_config: for pref in module_config:
if len(module_config[pref]) > 0: if len(module_config[pref]) > 0:
prefs[pref] = module_config[pref] prefs[pref] = module_config[pref]
if Globals.getInstance().get_camel_case(): if mt_config.camel_case:
configObj["module_config"] = prefs configObj["module_config"] = prefs
else: else:
configObj["module_config"] = prefs configObj["module_config"] = prefs
@@ -919,9 +971,8 @@ def export_config(interface):
def common(): def common():
"""Shared code for all of our command line wrappers""" """Shared code for all of our command line wrappers"""
logfile = None logfile = None
our_globals = Globals.getInstance() args = mt_config.args
args = our_globals.get_args() parser = mt_config.parser
parser = our_globals.get_parser()
logging.basicConfig( logging.basicConfig(
level=logging.DEBUG if (args.debug or args.listen) else logging.INFO, level=logging.DEBUG if (args.debug or args.listen) else logging.INFO,
format="%(levelname)s file:%(filename)s %(funcName)s line:%(lineno)s %(message)s", format="%(levelname)s file:%(filename)s %(funcName)s line:%(lineno)s %(message)s",
@@ -937,7 +988,7 @@ def common():
if args.ch_index is not None: if args.ch_index is not None:
channelIndex = int(args.ch_index) channelIndex = int(args.ch_index)
our_globals.set_channel_index(channelIndex) mt_config.channel_index = channelIndex
if not args.dest: if not args.dest:
args.dest = BROADCAST_ADDR args.dest = BROADCAST_ADDR
@@ -972,7 +1023,7 @@ def common():
# Note: using "line buffering" # Note: using "line buffering"
# pylint: disable=R1732 # pylint: disable=R1732
logfile = open(args.seriallog, "w+", buffering=1, encoding="utf8") logfile = open(args.seriallog, "w+", buffering=1, encoding="utf8")
our_globals.set_logfile(logfile) mt_config.logfile = logfile
subscribe() subscribe()
if args.ble_scan: if args.ble_scan:
@@ -1063,9 +1114,8 @@ def addConnectionArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentParse
def initParser(): def initParser():
"""Initialize the command line argument parsing.""" """Initialize the command line argument parsing."""
our_globals = Globals.getInstance() parser = mt_config.parser
parser = our_globals.get_parser() args = mt_config.args
args = our_globals.get_args()
# The "Help" group includes the help option and other informational stuff about the CLI itself # The "Help" group includes the help option and other informational stuff about the CLI itself
outerHelpGroup = parser.add_argument_group('Help') outerHelpGroup = parser.add_argument_group('Help')
@@ -1133,7 +1183,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",
) )
@@ -1288,7 +1347,15 @@ def initParser():
group.add_argument( group.add_argument(
"--request-telemetry", "--request-telemetry",
help="Request telemetry from a node. " help="Request telemetry from a node. "
"You need pass the destination ID as argument with '--dest'. " "You need to pass the destination ID as argument with '--dest'. "
"For repeaters, the nodeNum is required.",
action="store_true",
)
group.add_argument(
"--request-position",
help="Request the position from a node. "
"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",
) )
@@ -1305,7 +1372,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",
) )
@@ -1431,34 +1504,32 @@ def initParser():
args = parser.parse_args() args = parser.parse_args()
our_globals.set_args(args) mt_config.args = args
our_globals.set_parser(parser) mt_config.parser = parser
def main(): def main():
"""Perform command line meshtastic operations""" """Perform command line meshtastic operations"""
our_globals = Globals.getInstance()
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
add_help=False, add_help=False,
epilog="If no connection arguments are specified, we search for a compatible serial device, " epilog="If no connection arguments are specified, we search for a compatible serial device, "
"and if none is found, then attempt a TCP connection to localhost.") "and if none is found, then attempt a TCP connection to localhost.")
our_globals.set_parser(parser) mt_config.parser = parser
initParser() initParser()
common() common()
logfile = our_globals.get_logfile() logfile = mt_config.logfile
if logfile: if logfile:
logfile.close() logfile.close()
def tunnelMain(): def tunnelMain():
"""Run a meshtastic IP tunnel""" """Run a meshtastic IP tunnel"""
our_globals = Globals.getInstance()
parser = argparse.ArgumentParser(add_help=False) parser = argparse.ArgumentParser(add_help=False)
our_globals.set_parser(parser) mt_config.parser = parser
initParser() initParser()
args = our_globals.get_args() args = mt_config.args
args.tunnel = True args.tunnel = True
our_globals.set_args(args) mt_config.args = args
common() common()

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):
self.state = BLEInterface.BLEState() self.state = BLEInterface.BLEState()
if not address: if not address:

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):
@@ -575,27 +581,25 @@ class Config(google.protobuf.message.Message):
DEVICE_BATTERY_INA_ADDRESS_FIELD_NUMBER: builtins.int DEVICE_BATTERY_INA_ADDRESS_FIELD_NUMBER: builtins.int
is_power_saving: builtins.bool is_power_saving: builtins.bool
""" """
If set, we are powered from a low-current source (i.e. solar), so even if it looks like we have power flowing in Description: Will sleep everything as much as possible, for the tracker and sensor role this will also include the lora radio.
we should try to minimize power consumption as much as possible. Don't use this setting if you want to use your device with the phone apps or are using a device without a user button.
YOU DO NOT NEED TO SET THIS IF YOU'VE set is_router (it is implied in that case). Technical Details: Works for ESP32 devices and NRF52 devices in the Sensor or Tracker roles
Advanced Option
""" """
on_battery_shutdown_after_secs: builtins.int on_battery_shutdown_after_secs: builtins.int
""" """
If non-zero, the device will fully power off this many seconds after external power is removed. Description: If non-zero, the device will fully power off this many seconds after external power is removed.
""" """
adc_multiplier_override: builtins.float adc_multiplier_override: builtins.float
""" """
Ratio of voltage divider for battery pin eg. 3.20 (R1=100k, R2=220k) Ratio of voltage divider for battery pin eg. 3.20 (R1=100k, R2=220k)
Overrides the ADC_MULTIPLIER defined in variant for battery voltage calculation. Overrides the ADC_MULTIPLIER defined in variant for battery voltage calculation.
Should be set to floating point value between 2 and 4 https://meshtastic.org/docs/configuration/radio/power/#adc-multiplier-override
Fixes issues on Heltec v2 Should be set to floating point value between 2 and 6
""" """
wait_bluetooth_secs: builtins.int wait_bluetooth_secs: builtins.int
""" """
Wait Bluetooth Seconds Description: The number of seconds for to wait before turning off BLE in No Bluetooth states
The number of seconds for to wait before turning off BLE in No Bluetooth states Technical Details: ESP32 Only 0 for default of 1 minute
0 for default of 1 minute
""" """
sds_secs: builtins.int sds_secs: builtins.int
""" """
@@ -606,16 +610,13 @@ class Config(google.protobuf.message.Message):
""" """
ls_secs: builtins.int ls_secs: builtins.int
""" """
Light Sleep Seconds Description: In light sleep the CPU is suspended, LoRa radio is on, BLE is off an GPS is on
In light sleep the CPU is suspended, LoRa radio is on, BLE is off an GPS is on Technical Details: ESP32 Only 0 for default of 300
ESP32 Only
0 for default of 300
""" """
min_wake_secs: builtins.int min_wake_secs: builtins.int
""" """
Minimum Wake Seconds Description: While in light sleep when we receive packets on the LoRa radio we will wake and handle them and stay awake in no BLE mode for this value
While in light sleep when we receive packets on the LoRa radio we will wake and handle them and stay awake in no BLE mode for this value Technical Details: ESP32 Only 0 for default of 10 seconds
0 for default of 10 seconds
""" """
device_battery_ina_address: builtins.int device_battery_ina_address: builtins.int
""" """

View File

@@ -1,96 +0,0 @@
"""Globals singleton class.
Instead of using a global, stuff your variables in this "trash can".
This is not much better than using python's globals, but it allows
us to better test meshtastic. Plus, there are some weird python
global issues/gotcha that we can hopefully avoid by using this
class instead.
"""
class Globals:
"""Globals class is a Singleton."""
__instance = None
@staticmethod
def getInstance():
"""Get an instance of the Globals class."""
if Globals.__instance is None:
Globals()
return Globals.__instance
def __init__(self):
"""Constructor for the Globals CLass"""
if Globals.__instance is not None:
raise Exception("This class is a singleton") # pylint: disable=W0719
else:
Globals.__instance = self
self.args = None
self.parser = None
self.channel_index = None
self.logfile = None
self.tunnelInstance = None
# TODO: to migrate to camel_case for v1.3 change this value to True
self.camel_case = False
def reset(self):
"""Reset all of our globals. If you add a member, add it to this method, too."""
self.args = None
self.parser = None
self.channel_index = None
self.logfile = None
self.tunnelInstance = None
# TODO: to migrate to camel_case for v1.3 change this value to True
self.camel_case = False
# setters
def set_args(self, args):
"""Set the args"""
self.args = args
def set_parser(self, parser):
"""Set the parser"""
self.parser = parser
def set_channel_index(self, channel_index):
"""Set the channel_index"""
self.channel_index = channel_index
def set_logfile(self, logfile):
"""Set the logfile"""
self.logfile = logfile
def set_tunnelInstance(self, tunnelInstance):
"""Set the tunnelInstance"""
self.tunnelInstance = tunnelInstance
def set_camel_case(self):
"""Force using camelCase for things like prefs/set/set"""
self.camel_case = True
# getters
def get_args(self):
"""Get args"""
return self.args
def get_parser(self):
"""Get parser"""
return self.parser
def get_channel_index(self):
"""Get channel_index"""
return self.channel_index
def get_logfile(self):
"""Get logfile"""
return self.logfile
def get_tunnelInstance(self):
"""Get tunnelInstance"""
return self.tunnelInstance
def get_camel_case(self):
"""Get whether or not to use camelCase"""
return self.camel_case

View File

@@ -85,6 +85,7 @@ class MeshInterface:
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 +134,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
@@ -210,8 +211,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")),
} }
@@ -337,6 +338,7 @@ class MeshInterface:
meshPacket.id = self._generatePacketId() meshPacket.id = self._generatePacketId()
if onResponse is not None: if onResponse is not None:
logging.debug(f"Setting a response handler for requestId {meshPacket.id}")
self._addResponseHandler(meshPacket.id, onResponse) self._addResponseHandler(meshPacket.id, onResponse)
p = self._sendPacket(meshPacket, destinationId, wantAck=wantAck) p = self._sendPacket(meshPacket, destinationId, wantAck=wantAck)
return p return p
@@ -350,6 +352,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)
@@ -380,15 +383,53 @@ class MeshInterface:
p.time = timeSec p.time = timeSec
logging.debug(f"p.time:{p.time}") logging.debug(f"p.time:{p.time}")
return self.sendData( if wantResponse:
onResponse = self.onResponsePosition
else:
onResponse = None
d = self.sendData(
p, p,
destinationId, destinationId,
portNum=portnums_pb2.PortNum.POSITION_APP, portNum=portnums_pb2.PortNum.POSITION_APP,
wantAck=wantAck, wantAck=wantAck,
wantResponse=wantResponse, wantResponse=wantResponse,
onResponse=onResponse,
channelIndex=channelIndex,
) )
if wantResponse:
self.waitForPosition()
return d
def sendTraceRoute(self, dest: Union[int, str], hopLimit: int): def onResponsePosition(self, p):
"""on response for position"""
if p["decoded"]["portnum"] == 'POSITION_APP':
self._acknowledgment.receivedPosition = True
position = mesh_pb2.Position()
position.ParseFromString(p["decoded"]["payload"])
ret = "Position received: "
if position.latitude_i != 0 and position.longitude_i != 0:
ret += f"({position.latitude_i * 10**-7}, {position.longitude_i * 10**-7})"
else:
ret += "(unknown)"
if position.altitude != 0:
ret += f" {position.altitude}m"
if position.precision_bits not in [0,32]:
ret += f" precision:{position.precision_bits}"
elif position.precision_bits == 32:
ret += " full precision"
elif position.precision_bits == 0:
ret += " position disabled"
print(ret)
elif p["decoded"]["portnum"] == 'ROUTING_APP':
if p["decoded"]["routing"]["errorReason"] == 'NO_RESPONSE':
our_exit("No response from node. At least firmware 2.1.22 is required on the destination node.")
def 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(
@@ -397,6 +438,7 @@ 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)
@@ -418,43 +460,40 @@ 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
else: else:
onResponse = None onResponse = None
if destinationId.startswith("!"):
destinationId = int(destinationId[1:], 16)
else:
destinationId = int(destinationId)
self.sendData( self.sendData(
r, r,
destinationId=destinationId, destinationId=destinationId,
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()
@@ -572,6 +611,12 @@ class MeshInterface:
if not success: if not success:
raise MeshInterface.MeshInterfaceError("Timed out waiting for telemetry") raise MeshInterface.MeshInterfaceError("Timed out waiting for telemetry")
def waitForPosition(self):
"""Wait for position"""
success = self._timeout.waitForPosition(self._acknowledgment)
if not success:
raise MeshInterface.MeshInterfaceError("Timed out waiting for position")
def getMyNodeInfo(self) -> Optional[Dict]: def getMyNodeInfo(self) -> Optional[Dict]:
"""Get info about my node.""" """Get info about my node."""
if self.myInfo is None or self.nodesByNum is None: if self.myInfo is None or self.nodesByNum is None:
@@ -638,6 +683,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
@@ -661,6 +707,7 @@ 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) self.configId = random.randint(0, 0xFFFFFFFF)
@@ -742,7 +789,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
@@ -815,21 +867,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"):
@@ -857,6 +924,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
@@ -938,6 +1009,10 @@ class MeshInterface:
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
@@ -1034,14 +1109,16 @@ class MeshInterface:
# Is this message in response to a request, if so, look for a handler # Is this message in response to a request, if so, look for a handler
requestId = decoded.get("requestId") requestId = decoded.get("requestId")
if requestId is not None: if requestId is not None:
logging.debug(f"Got a response for requestId {requestId}")
# We ignore ACK packets, but send NAKs and data responses to the handlers # We ignore ACK packets, but send NAKs and data responses to the handlers
routing = decoded.get("routing") routing = decoded.get("routing")
isAck = routing is not None and ("errorReason" not in routing) isAck = routing is not None and ("errorReason" not in routing or routing["errorReason"] == "NONE")
if not isAck: if not isAck:
# we keep the responseHandler in dict until we get a non ack # we keep the responseHandler in dict until we get a non ack
handler = self.responseHandlers.pop(requestId, None) handler = self.responseHandlers.pop(requestId, None)
if handler is not None: if handler is not None:
if not isAck or (isAck and handler.__name__ == "onAckNak"): if not isAck or (isAck and handler.__name__ == "onAckNak"):
logging.debug(f"Calling response handler for requestId {requestId}")
handler.callback(asDict) handler.callback(asDict)
logging.debug(f"Publishing {topic}: packet={stripnl(asDict)} ") logging.debug(f"Publishing {topic}: packet={stripnl(asDict)} ")

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
@@ -267,6 +271,20 @@ class _HardwareModelEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._
Teledatics TD-LORAC NRF52840 based M.2 LoRA module Teledatics TD-LORAC NRF52840 based M.2 LoRA module
Compatible with the TD-WRLS development board Compatible with the TD-WRLS development board
""" """
CDEBYTE_EORA_S3: _HardwareModel.ValueType # 61
"""
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
"""
PRIVATE_HW: _HardwareModel.ValueType # 255 PRIVATE_HW: _HardwareModel.ValueType # 255
""" """
------------------------------------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------------------------------
@@ -364,6 +382,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
@@ -519,6 +541,20 @@ TD_LORAC: HardwareModel.ValueType # 60
Teledatics TD-LORAC NRF52840 based M.2 LoRA module Teledatics TD-LORAC NRF52840 based M.2 LoRA module
Compatible with the TD-WRLS development board Compatible with the TD-WRLS development board
""" """
CDEBYTE_EORA_S3: HardwareModel.ValueType # 61
"""
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
"""
PRIVATE_HW: HardwareModel.ValueType # 255 PRIVATE_HW: HardwareModel.ValueType # 255
""" """
------------------------------------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------------------------------

37
meshtastic/mt_config.py Normal file
View File

@@ -0,0 +1,37 @@
"""
Globals singleton class.
The Global object is gone, as are all its setters and getters. Instead the
module itself is the singleton namespace, which can be imported into
whichever module is used. The associated tests have also been removed,
since we now rely on built in Python mechanisms.
This is intended to make the Python read more naturally, and to make the
intention of the code clearer and more compact. It is merely a sticking
plaster over the use of shared mt_config, but the coupling issues wil be dealt
with rather more easily once the code is simplified by this change.
"""
def reset():
"""
Restore the namespace to pristine condition.
"""
# pylint: disable=W0603
global args, parser, channel_index, logfile, tunnelInstance, camel_case
args = None
parser = None
channel_index = None
logfile = None
tunnelInstance = None
# TODO: to migrate to camel_case for v1.3 change this value to True
camel_case = False
# These assignments are used instead of calling reset()
# purely to shut pylint up.
args = None
parser = None
channel_index = None
logfile = None
tunnelInstance = None
camel_case = False

View File

@@ -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()
@@ -639,7 +659,7 @@ class Node:
"""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
@@ -711,9 +731,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):
"""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)

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\"\xba\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\"\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*\xf9\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\x12\x0c\n\x08RCWL9620\x10\x10\x12\t\n\x05SHT4X\x10\x11\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=1097
_TELEMETRYSENSORTYPE._serialized_end=1303 _TELEMETRYSENSORTYPE._serialized_end=1346
_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=361
_POWERMETRICS._serialized_start=346 _POWERMETRICS._serialized_start=364
_POWERMETRICS._serialized_end=486 _POWERMETRICS._serialized_end=504
_AIRQUALITYMETRICS._serialized_start=489 _AIRQUALITYMETRICS._serialized_start=507
_AIRQUALITYMETRICS._serialized_end=808 _AIRQUALITYMETRICS._serialized_end=826
_TELEMETRY._serialized_start=811 _TELEMETRY._serialized_start=829
_TELEMETRY._serialized_end=1076 _TELEMETRY._serialized_end=1094
# @@protoc_insertion_point(module_scope) # @@protoc_insertion_point(module_scope)

View File

@@ -86,6 +86,14 @@ 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
"""
class TelemetrySensorType(_TelemetrySensorType, metaclass=_TelemetrySensorTypeEnumTypeWrapper): class TelemetrySensorType(_TelemetrySensorType, metaclass=_TelemetrySensorTypeEnumTypeWrapper):
""" """
@@ -156,6 +164,14 @@ 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
"""
global___TelemetrySensorType = TelemetrySensorType global___TelemetrySensorType = TelemetrySensorType
@typing_extensions.final @typing_extensions.final
@@ -219,6 +235,7 @@ 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
temperature: builtins.float temperature: builtins.float
""" """
Temperature measured Temperature measured
@@ -248,6 +265,10 @@ 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.
"""
def __init__( def __init__(
self, self,
*, *,
@@ -258,8 +279,9 @@ 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 = ...,
) -> 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", "relative_humidity", b"relative_humidity", "temperature", b"temperature", "voltage", b"voltage"]) -> None: ...
global___EnvironmentMetrics = EnvironmentMetrics global___EnvironmentMetrics = EnvironmentMetrics

View File

@@ -5,18 +5,18 @@ from unittest.mock import MagicMock
import pytest import pytest
from meshtastic.__main__ import Globals from meshtastic import mt_config
from ..mesh_interface import MeshInterface from ..mesh_interface import MeshInterface
@pytest.fixture @pytest.fixture
def reset_globals(): def reset_mt_config():
"""Fixture to reset globals.""" """Fixture to reset mt_config."""
parser = None parser = None
parser = argparse.ArgumentParser(add_help=False) parser = argparse.ArgumentParser(add_help=False)
Globals.getInstance().reset() mt_config.reset()
Globals.getInstance().set_parser(parser) mt_config.parser = parser
@pytest.fixture @pytest.fixture

View File

@@ -1,25 +0,0 @@
"""Meshtastic unit tests for globals.py
"""
import pytest
from ..globals import Globals
@pytest.mark.unit
def test_globals_get_instaance():
"""Test that we can instantiate a Globals instance"""
ourglobals = Globals.getInstance()
ourglobals2 = Globals.getInstance()
assert ourglobals == ourglobals2
@pytest.mark.unit
def test_globals_there_can_be_only_one():
"""Test that we can cannot create two Globals instances"""
# if we have an instance, delete it
Globals.getInstance()
with pytest.raises(Exception) as pytest_wrapped_e:
# try to create another instance
Globals()
assert pytest_wrapped_e.type == Exception

View File

@@ -6,9 +6,8 @@ from unittest.mock import MagicMock
import pytest import pytest
from meshtastic import _onNodeInfoReceive, _onPositionReceive, _onTextReceive from meshtastic import _onNodeInfoReceive, _onPositionReceive, _onTextReceive, mt_config
from ..globals import Globals
from ..serial_interface import SerialInterface from ..serial_interface import SerialInterface
@@ -16,7 +15,7 @@ from ..serial_interface import SerialInterface
def test_init_onTextReceive_with_exception(caplog): def test_init_onTextReceive_with_exception(caplog):
"""Test _onTextReceive""" """Test _onTextReceive"""
args = MagicMock() args = MagicMock()
Globals.getInstance().set_args(args) mt_config.args = args
iface = MagicMock(autospec=SerialInterface) iface = MagicMock(autospec=SerialInterface)
packet = {} packet = {}
with caplog.at_level(logging.DEBUG): with caplog.at_level(logging.DEBUG):
@@ -29,7 +28,7 @@ def test_init_onTextReceive_with_exception(caplog):
def test_init_onPositionReceive(caplog): def test_init_onPositionReceive(caplog):
"""Test _onPositionReceive""" """Test _onPositionReceive"""
args = MagicMock() args = MagicMock()
Globals.getInstance().set_args(args) mt_config.args = args
iface = MagicMock(autospec=SerialInterface) iface = MagicMock(autospec=SerialInterface)
packet = {"from": "foo", "decoded": {"position": {}}} packet = {"from": "foo", "decoded": {"position": {}}}
with caplog.at_level(logging.DEBUG): with caplog.at_level(logging.DEBUG):
@@ -41,7 +40,7 @@ def test_init_onPositionReceive(caplog):
def test_init_onNodeInfoReceive(caplog, iface_with_nodes): def test_init_onNodeInfoReceive(caplog, iface_with_nodes):
"""Test _onNodeInfoReceive""" """Test _onNodeInfoReceive"""
args = MagicMock() args = MagicMock()
Globals.getInstance().set_args(args) mt_config.args = args
iface = iface_with_nodes iface = iface_with_nodes
iface.myInfo.my_node_num = 2475227164 iface.myInfo.my_node_num = 2475227164
packet = { packet = {

View File

File diff suppressed because it is too large Load Diff

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
@@ -16,16 +16,17 @@ from ..util import Timeout
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.usefixtures("reset_globals") @pytest.mark.usefixtures("reset_mt_config")
def test_MeshInterface(capsys): def test_MeshInterface(capsys):
"""Test that we can instantiate a MeshInterface""" """Test that we can instantiate a MeshInterface"""
iface = MeshInterface(noProto=True) iface = MeshInterface(noProto=True)
nodes = { NODE_ID = "!9388f81c"
"!9388f81c": { NODE_NUM = 2475227164
"num": 2475227164, node = {
"num": NODE_NUM,
"user": { "user": {
"id": "!9388f81c", "id": NODE_ID,
"longName": "Unknown f81c", "longName": "Unknown f81c",
"shortName": "?1C", "shortName": "?1C",
"macaddr": "RBeTiPgc", "macaddr": "RBeTiPgc",
@@ -34,14 +35,16 @@ def test_MeshInterface(capsys):
"position": {}, "position": {},
"lastHeard": 1640204888, "lastHeard": 1640204888,
} }
}
iface.nodesByNum = {2475227164: nodes["!9388f81c"]}
iface.nodes = nodes iface.nodes = {NODE_ID: 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()
@@ -57,7 +60,7 @@ def test_MeshInterface(capsys):
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.usefixtures("reset_globals") @pytest.mark.usefixtures("reset_mt_config")
def test_getMyUser(iface_with_nodes): def test_getMyUser(iface_with_nodes):
"""Test getMyUser()""" """Test getMyUser()"""
iface = iface_with_nodes iface = iface_with_nodes
@@ -68,7 +71,7 @@ def test_getMyUser(iface_with_nodes):
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.usefixtures("reset_globals") @pytest.mark.usefixtures("reset_mt_config")
def test_getLongName(iface_with_nodes): def test_getLongName(iface_with_nodes):
"""Test getLongName()""" """Test getLongName()"""
iface = iface_with_nodes iface = iface_with_nodes
@@ -78,7 +81,7 @@ def test_getLongName(iface_with_nodes):
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.usefixtures("reset_globals") @pytest.mark.usefixtures("reset_mt_config")
def test_getShortName(iface_with_nodes): def test_getShortName(iface_with_nodes):
"""Test getShortName().""" """Test getShortName()."""
iface = iface_with_nodes iface = iface_with_nodes
@@ -88,7 +91,7 @@ def test_getShortName(iface_with_nodes):
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.usefixtures("reset_globals") @pytest.mark.usefixtures("reset_mt_config")
def test_handlePacketFromRadio_no_from(capsys): def test_handlePacketFromRadio_no_from(capsys):
"""Test _handlePacketFromRadio with no 'from' in the mesh packet.""" """Test _handlePacketFromRadio with no 'from' in the mesh packet."""
iface = MeshInterface(noProto=True) iface = MeshInterface(noProto=True)
@@ -100,7 +103,7 @@ def test_handlePacketFromRadio_no_from(capsys):
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.usefixtures("reset_globals") @pytest.mark.usefixtures("reset_mt_config")
def test_handlePacketFromRadio_with_a_portnum(caplog): def test_handlePacketFromRadio_with_a_portnum(caplog):
"""Test _handlePacketFromRadio with a portnum """Test _handlePacketFromRadio with a portnum
Since we have an attribute called 'from', we cannot simply 'set' it. Since we have an attribute called 'from', we cannot simply 'set' it.
@@ -116,7 +119,7 @@ def test_handlePacketFromRadio_with_a_portnum(caplog):
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.usefixtures("reset_globals") @pytest.mark.usefixtures("reset_mt_config")
def test_handlePacketFromRadio_no_portnum(caplog): def test_handlePacketFromRadio_no_portnum(caplog):
"""Test _handlePacketFromRadio without a portnum""" """Test _handlePacketFromRadio without a portnum"""
iface = MeshInterface(noProto=True) iface = MeshInterface(noProto=True)
@@ -128,7 +131,7 @@ def test_handlePacketFromRadio_no_portnum(caplog):
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.usefixtures("reset_globals") @pytest.mark.usefixtures("reset_mt_config")
def test_getNode_with_local(): def test_getNode_with_local():
"""Test getNode""" """Test getNode"""
iface = MeshInterface(noProto=True) iface = MeshInterface(noProto=True)
@@ -137,7 +140,7 @@ def test_getNode_with_local():
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.usefixtures("reset_globals") @pytest.mark.usefixtures("reset_mt_config")
def test_getNode_not_local(caplog): def test_getNode_not_local(caplog):
"""Test getNode not local""" """Test getNode not local"""
iface = MeshInterface(noProto=True) iface = MeshInterface(noProto=True)
@@ -150,7 +153,7 @@ def test_getNode_not_local(caplog):
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.usefixtures("reset_globals") @pytest.mark.usefixtures("reset_mt_config")
def test_getNode_not_local_timeout(capsys): def test_getNode_not_local_timeout(capsys):
"""Test getNode not local, simulate timeout""" """Test getNode not local, simulate timeout"""
iface = MeshInterface(noProto=True) iface = MeshInterface(noProto=True)
@@ -167,7 +170,7 @@ def test_getNode_not_local_timeout(capsys):
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.usefixtures("reset_globals") @pytest.mark.usefixtures("reset_mt_config")
def test_sendPosition(caplog): def test_sendPosition(caplog):
"""Test sendPosition""" """Test sendPosition"""
iface = MeshInterface(noProto=True) iface = MeshInterface(noProto=True)
@@ -179,7 +182,7 @@ def test_sendPosition(caplog):
# TODO # TODO
# @pytest.mark.unit # @pytest.mark.unit
# @pytest.mark.usefixtures("reset_globals") # @pytest.mark.usefixtures("reset_mt_config")
# def test_close_with_heartbeatTimer(caplog): # def test_close_with_heartbeatTimer(caplog):
# """Test close() with heartbeatTimer""" # """Test close() with heartbeatTimer"""
# iface = MeshInterface(noProto=True) # iface = MeshInterface(noProto=True)
@@ -197,7 +200,7 @@ def test_sendPosition(caplog):
# TODO # TODO
# @pytest.mark.unit # @pytest.mark.unit
# @pytest.mark.usefixtures("reset_globals") # @pytest.mark.usefixtures("reset_mt_config")
# def test_handleFromRadio_empty_payload(caplog): # def test_handleFromRadio_empty_payload(caplog):
# """Test _handleFromRadio""" # """Test _handleFromRadio"""
# iface = MeshInterface(noProto=True) # iface = MeshInterface(noProto=True)
@@ -208,7 +211,7 @@ def test_sendPosition(caplog):
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.usefixtures("reset_globals") @pytest.mark.usefixtures("reset_mt_config")
def test_handleFromRadio_with_my_info(caplog): def test_handleFromRadio_with_my_info(caplog):
"""Test _handleFromRadio with my_info""" """Test _handleFromRadio with my_info"""
# Note: I captured the '--debug --info' for the bytes below. # Note: I captured the '--debug --info' for the bytes below.
@@ -233,7 +236,7 @@ def test_handleFromRadio_with_my_info(caplog):
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.usefixtures("reset_globals") @pytest.mark.usefixtures("reset_mt_config")
def test_handleFromRadio_with_node_info(caplog, capsys): def test_handleFromRadio_with_node_info(caplog, capsys):
"""Test _handleFromRadio with node_info""" """Test _handleFromRadio with node_info"""
# Note: I captured the '--debug --info' for the bytes below. # Note: I captured the '--debug --info' for the bytes below.
@@ -269,7 +272,7 @@ def test_handleFromRadio_with_node_info(caplog, capsys):
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.usefixtures("reset_globals") @pytest.mark.usefixtures("reset_mt_config")
def test_handleFromRadio_with_node_info_tbeam1(caplog, capsys): def test_handleFromRadio_with_node_info_tbeam1(caplog, capsys):
"""Test _handleFromRadio with node_info""" """Test _handleFromRadio with node_info"""
# Note: Captured the '--debug --info' for the bytes below. # Note: Captured the '--debug --info' for the bytes below.
@@ -293,7 +296,7 @@ def test_handleFromRadio_with_node_info_tbeam1(caplog, capsys):
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.usefixtures("reset_globals") @pytest.mark.usefixtures("reset_mt_config")
def test_handleFromRadio_with_node_info_tbeam_with_bad_data(caplog): def test_handleFromRadio_with_node_info_tbeam_with_bad_data(caplog):
"""Test _handleFromRadio with node_info with some bad data (issue#172) - ensure we do not throw exception""" """Test _handleFromRadio with node_info with some bad data (issue#172) - ensure we do not throw exception"""
# Note: Captured the '--debug --info' for the bytes below. # Note: Captured the '--debug --info' for the bytes below.
@@ -305,7 +308,7 @@ def test_handleFromRadio_with_node_info_tbeam_with_bad_data(caplog):
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.usefixtures("reset_globals") @pytest.mark.usefixtures("reset_mt_config")
def test_MeshInterface_sendToRadioImpl(caplog): def test_MeshInterface_sendToRadioImpl(caplog):
"""Test _sendToRadioImp()""" """Test _sendToRadioImp()"""
iface = MeshInterface(noProto=True) iface = MeshInterface(noProto=True)
@@ -316,7 +319,7 @@ def test_MeshInterface_sendToRadioImpl(caplog):
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.usefixtures("reset_globals") @pytest.mark.usefixtures("reset_mt_config")
def test_MeshInterface_sendToRadio_no_proto(caplog): def test_MeshInterface_sendToRadio_no_proto(caplog):
"""Test sendToRadio()""" """Test sendToRadio()"""
iface = MeshInterface() iface = MeshInterface()
@@ -327,7 +330,7 @@ def test_MeshInterface_sendToRadio_no_proto(caplog):
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.usefixtures("reset_globals") @pytest.mark.usefixtures("reset_mt_config")
def test_sendData_too_long(caplog): def test_sendData_too_long(caplog):
"""Test when data payload is too big""" """Test when data payload is too big"""
iface = MeshInterface(noProto=True) iface = MeshInterface(noProto=True)
@@ -352,7 +355,7 @@ def test_sendData_too_long(caplog):
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.usefixtures("reset_globals") @pytest.mark.usefixtures("reset_mt_config")
def test_sendData_unknown_app(capsys): def test_sendData_unknown_app(capsys):
"""Test sendData when unknown app""" """Test sendData when unknown app"""
iface = MeshInterface(noProto=True) iface = MeshInterface(noProto=True)
@@ -366,7 +369,7 @@ def test_sendData_unknown_app(capsys):
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.usefixtures("reset_globals") @pytest.mark.usefixtures("reset_mt_config")
def test_sendPosition_with_a_position(caplog): def test_sendPosition_with_a_position(caplog):
"""Test sendPosition when lat/long/alt""" """Test sendPosition when lat/long/alt"""
iface = MeshInterface(noProto=True) iface = MeshInterface(noProto=True)
@@ -378,7 +381,7 @@ def test_sendPosition_with_a_position(caplog):
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.usefixtures("reset_globals") @pytest.mark.usefixtures("reset_mt_config")
def test_sendPacket_with_no_destination(capsys): def test_sendPacket_with_no_destination(capsys):
"""Test _sendPacket()""" """Test _sendPacket()"""
iface = MeshInterface(noProto=True) iface = MeshInterface(noProto=True)
@@ -392,7 +395,7 @@ def test_sendPacket_with_no_destination(capsys):
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.usefixtures("reset_globals") @pytest.mark.usefixtures("reset_mt_config")
def test_sendPacket_with_destination_as_int(caplog): def test_sendPacket_with_destination_as_int(caplog):
"""Test _sendPacket() with int as a destination""" """Test _sendPacket() with int as a destination"""
iface = MeshInterface(noProto=True) iface = MeshInterface(noProto=True)
@@ -403,7 +406,7 @@ def test_sendPacket_with_destination_as_int(caplog):
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.usefixtures("reset_globals") @pytest.mark.usefixtures("reset_mt_config")
def test_sendPacket_with_destination_starting_with_a_bang(caplog): def test_sendPacket_with_destination_starting_with_a_bang(caplog):
"""Test _sendPacket() with int as a destination""" """Test _sendPacket() with int as a destination"""
iface = MeshInterface(noProto=True) iface = MeshInterface(noProto=True)
@@ -414,7 +417,7 @@ def test_sendPacket_with_destination_starting_with_a_bang(caplog):
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.usefixtures("reset_globals") @pytest.mark.usefixtures("reset_mt_config")
def test_sendPacket_with_destination_as_BROADCAST_ADDR(caplog): def test_sendPacket_with_destination_as_BROADCAST_ADDR(caplog):
"""Test _sendPacket() with BROADCAST_ADDR as a destination""" """Test _sendPacket() with BROADCAST_ADDR as a destination"""
iface = MeshInterface(noProto=True) iface = MeshInterface(noProto=True)
@@ -425,7 +428,7 @@ def test_sendPacket_with_destination_as_BROADCAST_ADDR(caplog):
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.usefixtures("reset_globals") @pytest.mark.usefixtures("reset_mt_config")
def test_sendPacket_with_destination_as_LOCAL_ADDR_no_myInfo(capsys): def test_sendPacket_with_destination_as_LOCAL_ADDR_no_myInfo(capsys):
"""Test _sendPacket() with LOCAL_ADDR as a destination with no myInfo""" """Test _sendPacket() with LOCAL_ADDR as a destination with no myInfo"""
iface = MeshInterface(noProto=True) iface = MeshInterface(noProto=True)
@@ -440,7 +443,7 @@ def test_sendPacket_with_destination_as_LOCAL_ADDR_no_myInfo(capsys):
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.usefixtures("reset_globals") @pytest.mark.usefixtures("reset_mt_config")
def test_sendPacket_with_destination_as_LOCAL_ADDR_with_myInfo(caplog): def test_sendPacket_with_destination_as_LOCAL_ADDR_with_myInfo(caplog):
"""Test _sendPacket() with LOCAL_ADDR as a destination with myInfo""" """Test _sendPacket() with LOCAL_ADDR as a destination with myInfo"""
iface = MeshInterface(noProto=True) iface = MeshInterface(noProto=True)
@@ -454,7 +457,7 @@ def test_sendPacket_with_destination_as_LOCAL_ADDR_with_myInfo(caplog):
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.usefixtures("reset_globals") @pytest.mark.usefixtures("reset_mt_config")
def test_sendPacket_with_destination_is_blank_with_nodes(capsys, iface_with_nodes): def test_sendPacket_with_destination_is_blank_with_nodes(capsys, iface_with_nodes):
"""Test _sendPacket() with '' as a destination with myInfo""" """Test _sendPacket() with '' as a destination with myInfo"""
iface = iface_with_nodes iface = iface_with_nodes
@@ -469,7 +472,7 @@ def test_sendPacket_with_destination_is_blank_with_nodes(capsys, iface_with_node
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.usefixtures("reset_globals") @pytest.mark.usefixtures("reset_mt_config")
def test_sendPacket_with_destination_is_blank_without_nodes(caplog, iface_with_nodes): def test_sendPacket_with_destination_is_blank_without_nodes(caplog, iface_with_nodes):
"""Test _sendPacket() with '' as a destination with myInfo""" """Test _sendPacket() with '' as a destination with myInfo"""
iface = iface_with_nodes iface = iface_with_nodes
@@ -481,7 +484,7 @@ def test_sendPacket_with_destination_is_blank_without_nodes(caplog, iface_with_n
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.usefixtures("reset_globals") @pytest.mark.usefixtures("reset_mt_config")
def test_getMyNodeInfo(): def test_getMyNodeInfo():
"""Test getMyNodeInfo()""" """Test getMyNodeInfo()"""
iface = MeshInterface(noProto=True) iface = MeshInterface(noProto=True)
@@ -496,7 +499,7 @@ def test_getMyNodeInfo():
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.usefixtures("reset_globals") @pytest.mark.usefixtures("reset_mt_config")
def test_generatePacketId(capsys): def test_generatePacketId(capsys):
"""Test _generatePacketId() when no currentPacketId (not connected)""" """Test _generatePacketId() when no currentPacketId (not connected)"""
iface = MeshInterface(noProto=True) iface = MeshInterface(noProto=True)
@@ -514,7 +517,7 @@ def test_generatePacketId(capsys):
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.usefixtures("reset_globals") @pytest.mark.usefixtures("reset_mt_config")
def test_fixupPosition_empty_pos(): def test_fixupPosition_empty_pos():
"""Test _fixupPosition()""" """Test _fixupPosition()"""
iface = MeshInterface(noProto=True) iface = MeshInterface(noProto=True)
@@ -524,7 +527,7 @@ def test_fixupPosition_empty_pos():
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.usefixtures("reset_globals") @pytest.mark.usefixtures("reset_mt_config")
def test_fixupPosition_no_changes_needed(): def test_fixupPosition_no_changes_needed():
"""Test _fixupPosition()""" """Test _fixupPosition()"""
iface = MeshInterface(noProto=True) iface = MeshInterface(noProto=True)
@@ -534,7 +537,7 @@ def test_fixupPosition_no_changes_needed():
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.usefixtures("reset_globals") @pytest.mark.usefixtures("reset_mt_config")
def test_fixupPosition(): def test_fixupPosition():
"""Test _fixupPosition()""" """Test _fixupPosition()"""
iface = MeshInterface(noProto=True) iface = MeshInterface(noProto=True)
@@ -549,7 +552,7 @@ def test_fixupPosition():
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.usefixtures("reset_globals") @pytest.mark.usefixtures("reset_mt_config")
def test_nodeNumToId(iface_with_nodes): def test_nodeNumToId(iface_with_nodes):
"""Test _nodeNumToId()""" """Test _nodeNumToId()"""
iface = iface_with_nodes iface = iface_with_nodes
@@ -559,7 +562,7 @@ def test_nodeNumToId(iface_with_nodes):
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.usefixtures("reset_globals") @pytest.mark.usefixtures("reset_mt_config")
def test_nodeNumToId_not_found(iface_with_nodes): def test_nodeNumToId_not_found(iface_with_nodes):
"""Test _nodeNumToId()""" """Test _nodeNumToId()"""
iface = iface_with_nodes iface = iface_with_nodes
@@ -569,7 +572,7 @@ def test_nodeNumToId_not_found(iface_with_nodes):
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.usefixtures("reset_globals") @pytest.mark.usefixtures("reset_mt_config")
def test_nodeNumToId_to_all(iface_with_nodes): def test_nodeNumToId_to_all(iface_with_nodes):
"""Test _nodeNumToId()""" """Test _nodeNumToId()"""
iface = iface_with_nodes iface = iface_with_nodes
@@ -579,7 +582,7 @@ def test_nodeNumToId_to_all(iface_with_nodes):
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.usefixtures("reset_globals") @pytest.mark.usefixtures("reset_mt_config")
def test_getOrCreateByNum_minimal(iface_with_nodes): def test_getOrCreateByNum_minimal(iface_with_nodes):
"""Test _getOrCreateByNum()""" """Test _getOrCreateByNum()"""
iface = iface_with_nodes iface = iface_with_nodes
@@ -589,7 +592,7 @@ def test_getOrCreateByNum_minimal(iface_with_nodes):
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.usefixtures("reset_globals") @pytest.mark.usefixtures("reset_mt_config")
def test_getOrCreateByNum_not_found(iface_with_nodes): def test_getOrCreateByNum_not_found(iface_with_nodes):
"""Test _getOrCreateByNum()""" """Test _getOrCreateByNum()"""
iface = iface_with_nodes iface = iface_with_nodes
@@ -600,7 +603,7 @@ def test_getOrCreateByNum_not_found(iface_with_nodes):
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.usefixtures("reset_globals") @pytest.mark.usefixtures("reset_mt_config")
def test_getOrCreateByNum(iface_with_nodes): def test_getOrCreateByNum(iface_with_nodes):
"""Test _getOrCreateByNum()""" """Test _getOrCreateByNum()"""
iface = iface_with_nodes iface = iface_with_nodes

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

@@ -20,7 +20,7 @@ def test_StreamInterface():
# Note: This takes a bit, so moving from unit to slow # Note: This takes a bit, so moving from unit to slow
@pytest.mark.unitslow @pytest.mark.unitslow
@pytest.mark.usefixtures("reset_globals") @pytest.mark.usefixtures("reset_mt_config")
def test_StreamInterface_with_noProto(caplog): def test_StreamInterface_with_noProto(caplog):
"""Test that we can instantiate a StreamInterface based on nonProto """Test that we can instantiate a StreamInterface based on nonProto
and we can read/write bytes from a mocked stream and we can read/write bytes from a mocked stream
@@ -41,7 +41,7 @@ def test_StreamInterface_with_noProto(caplog):
### Tip: If you want to see the print output, run with '-s' flag: ### Tip: If you want to see the print output, run with '-s' flag:
### pytest -s meshtastic/tests/test_stream_interface.py::test_sendToRadioImpl ### pytest -s meshtastic/tests/test_stream_interface.py::test_sendToRadioImpl
# @pytest.mark.unitslow # @pytest.mark.unitslow
# @pytest.mark.usefixtures("reset_globals") # @pytest.mark.usefixtures("reset_mt_config")
# def test_sendToRadioImpl(caplog): # def test_sendToRadioImpl(caplog):
# """Test _sendToRadioImpl()""" # """Test _sendToRadioImpl()"""
# #

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

@@ -1,5 +1,4 @@
"""Meshtastic unit tests for tunnel.py""" """Meshtastic unit tests for tunnel.py"""
import logging import logging
import re import re
import sys import sys
@@ -7,7 +6,8 @@ from unittest.mock import MagicMock, patch
import pytest import pytest
from ..globals import Globals from meshtastic import mt_config
from ..tcp_interface import TCPInterface from ..tcp_interface import TCPInterface
from ..tunnel import Tunnel, onTunnelReceive from ..tunnel import Tunnel, onTunnelReceive
@@ -51,7 +51,7 @@ def test_Tunnel_with_interface(mock_platform_system, caplog, iface_with_nodes):
with caplog.at_level(logging.WARNING): with caplog.at_level(logging.WARNING):
with patch("socket.socket"): with patch("socket.socket"):
tun = Tunnel(iface) tun = Tunnel(iface)
assert tun == Globals.getInstance().get_tunnelInstance() assert tun == mt_config.tunnelInstance
iface.close() iface.close()
assert re.search(r"Not creating a TapDevice()", caplog.text, re.MULTILINE) assert re.search(r"Not creating a TapDevice()", caplog.text, re.MULTILINE)
assert re.search(r"Not starting TUN reader", caplog.text, re.MULTILINE) assert re.search(r"Not starting TUN reader", caplog.text, re.MULTILINE)
@@ -65,7 +65,7 @@ def test_onTunnelReceive_from_ourselves(mock_platform_system, caplog, iface_with
iface = iface_with_nodes iface = iface_with_nodes
iface.myInfo.my_node_num = 2475227164 iface.myInfo.my_node_num = 2475227164
sys.argv = [""] sys.argv = [""]
Globals.getInstance().set_args(sys.argv) mt_config.args = sys.argv
packet = {"decoded": {"payload": "foo"}, "from": 2475227164} packet = {"decoded": {"payload": "foo"}, "from": 2475227164}
a_mock = MagicMock() a_mock = MagicMock()
a_mock.return_value = "Linux" a_mock.return_value = "Linux"
@@ -73,7 +73,7 @@ def test_onTunnelReceive_from_ourselves(mock_platform_system, caplog, iface_with
with caplog.at_level(logging.DEBUG): with caplog.at_level(logging.DEBUG):
with patch("socket.socket"): with patch("socket.socket"):
tun = Tunnel(iface) tun = Tunnel(iface)
Globals.getInstance().set_tunnelInstance(tun) mt_config.tunnelInstance = tun
onTunnelReceive(packet, iface) onTunnelReceive(packet, iface)
assert re.search(r"in onTunnelReceive", caplog.text, re.MULTILINE) assert re.search(r"in onTunnelReceive", caplog.text, re.MULTILINE)
assert re.search(r"Ignoring message we sent", caplog.text, re.MULTILINE) assert re.search(r"Ignoring message we sent", caplog.text, re.MULTILINE)
@@ -88,7 +88,7 @@ def test_onTunnelReceive_from_someone_else(
iface = iface_with_nodes iface = iface_with_nodes
iface.myInfo.my_node_num = 2475227164 iface.myInfo.my_node_num = 2475227164
sys.argv = [""] sys.argv = [""]
Globals.getInstance().set_args(sys.argv) mt_config.args = sys.argv
packet = {"decoded": {"payload": "foo"}, "from": 123} packet = {"decoded": {"payload": "foo"}, "from": 123}
a_mock = MagicMock() a_mock = MagicMock()
a_mock.return_value = "Linux" a_mock.return_value = "Linux"
@@ -96,7 +96,7 @@ def test_onTunnelReceive_from_someone_else(
with caplog.at_level(logging.DEBUG): with caplog.at_level(logging.DEBUG):
with patch("socket.socket"): with patch("socket.socket"):
tun = Tunnel(iface) tun = Tunnel(iface)
Globals.getInstance().set_tunnelInstance(tun) mt_config.tunnelInstance = tun
onTunnelReceive(packet, iface) onTunnelReceive(packet, iface)
assert re.search(r"in onTunnelReceive", caplog.text, re.MULTILINE) assert re.search(r"in onTunnelReceive", caplog.text, re.MULTILINE)

View File

@@ -22,16 +22,14 @@ import threading
from pubsub import pub # type: ignore[import-untyped] from pubsub import pub # type: ignore[import-untyped]
from pytap2 import TapDevice from pytap2 import TapDevice
from meshtastic import portnums_pb2 from meshtastic import portnums_pb2, mt_config
from meshtastic.globals import Globals
from meshtastic.util import ipstr, readnet_u16 from meshtastic.util import ipstr, readnet_u16
def onTunnelReceive(packet, interface): # pylint: disable=W0613 def onTunnelReceive(packet, interface): # pylint: disable=W0613
"""Callback for received tunneled messages from mesh.""" """Callback for received tunneled messages from mesh."""
logging.debug(f"in onTunnelReceive()") logging.debug(f"in onTunnelReceive()")
our_globals = Globals.getInstance() tunnelInstance = mt_config.tunnelInstance
tunnelInstance = our_globals.get_tunnelInstance()
tunnelInstance.onReceive(packet) tunnelInstance.onReceive(packet)
@@ -67,8 +65,7 @@ class Tunnel:
if platform.system() != "Linux": if platform.system() != "Linux":
raise Tunnel.TunnelError("Tunnel() can only be run instantiated on a Linux system") raise Tunnel.TunnelError("Tunnel() can only be run instantiated on a Linux system")
our_globals = Globals.getInstance() mt_config.tunnelInstance = self
our_globals.set_tunnelInstance(self)
"""A list of chatty UDP services we should never accidentally """A list of chatty UDP services we should never accidentally
forward to our slow network""" forward to our slow network"""

View File

@@ -11,7 +11,7 @@ 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
@@ -122,7 +122,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
@@ -206,6 +206,16 @@ class Timeout:
time.sleep(self.sleepInterval) time.sleep(self.sleepInterval)
return False return False
def waitForPosition(self, acknowledgment) -> bool:
"""Block until position response is received. Returns True if position response has been received."""
self.reset()
while time.time() < self.expireTime:
if getattr(acknowledgment, "receivedPosition", None):
acknowledgment.reset()
return True
time.sleep(self.sleepInterval)
return False
class Acknowledgment: class Acknowledgment:
"A class that records which type of acknowledgment was just received, if any." "A class that records which type of acknowledgment was just received, if any."
@@ -216,6 +226,7 @@ class Acknowledgment:
self.receivedImplAck = False self.receivedImplAck = False
self.receivedTraceRoute = False self.receivedTraceRoute = False
self.receivedTelemetry = False self.receivedTelemetry = False
self.receivedPosition = False
def reset(self): def reset(self):
"""reset""" """reset"""
@@ -224,6 +235,7 @@ class Acknowledgment:
self.receivedImplAck = False self.receivedImplAck = False
self.receivedTraceRoute = False self.receivedTraceRoute = False
self.receivedTelemetry = False self.receivedTelemetry = False
self.receivedPosition = False
class DeferredExecution: class DeferredExecution:
@@ -251,7 +263,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)
""" """
@@ -615,6 +627,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, multiline=False):
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

@@ -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.4", version="2.3.8",
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",