Compare commits

...

22 Commits
2.3.6 ... 2.3.8

Author SHA1 Message Date
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
18 changed files with 263 additions and 132 deletions

View File

@@ -20,6 +20,7 @@ from meshtastic import mt_config
from meshtastic import channel_pb2, config_pb2, portnums_pb2, remote_hardware, BROADCAST_ADDR
from meshtastic.version import get_active_version
from meshtastic.ble_interface import BLEInterface
from meshtastic.mesh_interface import MeshInterface
def onReceive(packet, interface):
"""Callback invoked when a packet arrives"""
@@ -56,6 +57,11 @@ def onConnection(interface, topic=pub.AUTO_TOPIC): # pylint: disable=W0613
"""Callback invoked when we connect/disconnect from a radio"""
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):
"""Get a channel or preferences value"""
@@ -251,6 +257,9 @@ def onConnected(interface):
print("Connected to radio")
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
alt = 0
@@ -366,6 +375,11 @@ def onConnected(interface):
waitForAckNak = True
interface.getNode(args.dest, False).rebootOTA()
if args.enter_dfu:
closeNow = True
waitForAckNak = True
interface.getNode(args.dest, False).enterDFUMode()
if args.shutdown:
closeNow = True
waitForAckNak = True
@@ -400,12 +414,8 @@ def onConnected(interface):
if args.sendtext:
closeNow = True
channelIndex = 0
if args.ch_index is not None:
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:
channelIndex = mt_config.channel_index or 0
if checkChannel(interface, channelIndex):
print(
f"Sending text message {args.sendtext} to {args.dest} on channelIndex:{channelIndex}"
)
@@ -425,22 +435,28 @@ def onConnected(interface):
loraConfig = getattr(interface.localNode.localConfig, "lora")
hopLimit = getattr(loraConfig, "hop_limit")
dest = str(args.traceroute)
print(f"Sending traceroute request to {dest} (this could take a while)")
interface.sendTraceRoute(dest, hopLimit)
channelIndex = mt_config.channel_index or 0
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.dest == BROADCAST_ADDR:
meshtastic.util.our_exit("Warning: Must use a destination node ID.")
else:
print(f"Sending telemetry request to {args.dest} (this could take a while)")
interface.sendTelemetry(destinationId=args.dest, wantResponse=True)
channelIndex = mt_config.channel_index or 0
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:
print(f"Sending position request to {args.dest} (this could take a while)")
interface.sendPosition(destinationId=args.dest, wantResponse=True)
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.dest == BROADCAST_ADDR:
@@ -599,6 +615,9 @@ def onConnected(interface):
print("Writing modified configuration to device")
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')
closeNow = True
export_config(interface)
@@ -801,10 +820,14 @@ def onConnected(interface):
return
interface.showNodes()
if args.qr:
if args.qr or args.qr_all:
closeNow = True
url = interface.localNode.getURL(includeAll=False)
print(f"Primary channel URL {url}")
url = interface.getNode(args.dest, True).getURL(includeAll=args.qr_all)
if args.qr_all:
urldesc = "Complete URL (includes all channels)"
else:
urldesc = "Primary channel URL"
print(f"{urldesc}: {url}")
qr = pyqrcode.create(url)
print(qr.terminal())
@@ -813,6 +836,9 @@ def onConnected(interface):
have_tunnel = platform.system() == "Linux"
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
from . import tunnel
@@ -1154,7 +1180,16 @@ def initParser():
group.add_argument(
"--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",
)
@@ -1334,7 +1369,13 @@ def initParser():
group.add_argument(
"--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",
)

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.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.channel_pb2', globals())
@@ -26,9 +26,9 @@ if _descriptor._USE_C_DESCRIPTORS == False:
_CHANNELSETTINGS._serialized_start=41
_CHANNELSETTINGS._serialized_end=225
_MODULESETTINGS._serialized_start=227
_MODULESETTINGS._serialized_end=271
_CHANNEL._serialized_start=274
_CHANNEL._serialized_end=435
_CHANNEL_ROLE._serialized_start=387
_CHANNEL_ROLE._serialized_end=435
_MODULESETTINGS._serialized_end=296
_CHANNEL._serialized_start=299
_CHANNEL._serialized_end=460
_CHANNEL_ROLE._serialized_start=412
_CHANNEL_ROLE._serialized_end=460
# @@protoc_insertion_point(module_scope)

View File

@@ -125,16 +125,23 @@ class ModuleSettings(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
POSITION_PRECISION_FIELD_NUMBER: builtins.int
IS_CLIENT_MUTED_FIELD_NUMBER: builtins.int
position_precision: builtins.int
"""
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__(
self,
*,
position_precision: builtins.int = ...,
is_client_muted: builtins.bool = ...,
) -> 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

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
DISABLE_TRIPLE_CLICK_FIELD_NUMBER: builtins.int
TZDEF_FIELD_NUMBER: builtins.int
LED_HEARTBEAT_DISABLED_FIELD_NUMBER: builtins.int
role: global___Config.DeviceConfig.Role.ValueType
"""
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.
"""
led_heartbeat_disabled: builtins.bool
"""
If true, disable the default blinking LED (LED_PIN) behavior on the device
"""
def __init__(
self,
*,
@@ -308,8 +313,9 @@ class Config(google.protobuf.message.Message):
is_managed: builtins.bool = ...,
disable_triple_click: builtins.bool = ...,
tzdef: builtins.str = ...,
led_heartbeat_disabled: builtins.bool = ...,
) -> 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
class PositionConfig(google.protobuf.message.Message):

View File

@@ -85,6 +85,7 @@ class MeshInterface:
self.mask: Optional[int] = None # used in gpio read and gpio watch
self.queueStatus: Optional[mesh_pb2.QueueStatus] = None
self.queue: collections.OrderedDict = collections.OrderedDict()
self._localChannels = None
def close(self):
"""Shutdown this interface"""
@@ -133,7 +134,7 @@ class MeshInterface:
# use id as dictionary key for correct json format in list of nodes
nodeid = n2["user"]["id"]
nodes[nodeid] = n2
infos = owner + myinfo + metadata + mesh + json.dumps(nodes)
infos = owner + myinfo + metadata + mesh + json.dumps(nodes, indent=2)
print(infos)
return infos
@@ -351,6 +352,7 @@ class MeshInterface:
destinationId: Union[int, str]=BROADCAST_ADDR,
wantAck: bool=False,
wantResponse: bool=False,
channelIndex: int=0,
):
"""
Send a position packet to some other node (normally a broadcast)
@@ -393,6 +395,7 @@ class MeshInterface:
wantAck=wantAck,
wantResponse=wantResponse,
onResponse=onResponse,
channelIndex=channelIndex,
)
if wantResponse:
self.waitForPosition()
@@ -426,7 +429,7 @@ class MeshInterface:
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):
def sendTraceRoute(self, dest: Union[int, str], hopLimit: int, channelIndex: int=0):
"""Send the trace route"""
r = mesh_pb2.RouteDiscovery()
self.sendData(
@@ -435,6 +438,7 @@ class MeshInterface:
portNum=portnums_pb2.PortNum.TRACEROUTE_APP,
wantResponse=True,
onResponse=self.onResponseTraceRoute,
channelIndex=channelIndex,
)
# extend timeout based on number of nodes, limit by configured hopLimit
waitFactor = min(len(self.nodes) - 1 if self.nodes else 0, hopLimit)
@@ -456,26 +460,27 @@ class MeshInterface:
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"""
r = telemetry_pb2.Telemetry()
node = next(n for n in self.nodes.values() if n["num"] == self.localNode.nodeNum)
if node is not None:
metrics = node.get("deviceMetrics")
if metrics:
batteryLevel = metrics.get("batteryLevel")
if batteryLevel is not None:
r.device_metrics.battery_level = batteryLevel
voltage = metrics.get("voltage")
if voltage is not None:
r.device_metrics.voltage = voltage
channel_utilization = metrics.get("channelUtilization")
if channel_utilization is not None:
r.device_metrics.channel_utilization = channel_utilization
air_util_tx = metrics.get("airUtilTx")
if air_util_tx is not None:
r.device_metrics.air_util_tx = air_util_tx
if self.nodes is not None:
node = next(n for n in self.nodes.values() if n["num"] == self.localNode.nodeNum)
if node is not None:
metrics = node.get("deviceMetrics")
if metrics:
batteryLevel = metrics.get("batteryLevel")
if batteryLevel is not None:
r.device_metrics.battery_level = batteryLevel
voltage = metrics.get("voltage")
if voltage is not None:
r.device_metrics.voltage = voltage
channel_utilization = metrics.get("channelUtilization")
if channel_utilization is not None:
r.device_metrics.channel_utilization = channel_utilization
air_util_tx = metrics.get("airUtilTx")
if air_util_tx is not None:
r.device_metrics.air_util_tx = air_util_tx
if wantResponse:
onResponse = self.onResponseTelemetry
@@ -488,6 +493,7 @@ class MeshInterface:
portNum=portnums_pb2.PortNum.TELEMETRY_APP,
wantResponse=wantResponse,
onResponse=onResponse,
channelIndex=channelIndex,
)
if wantResponse:
self.waitForTelemetry()
@@ -677,6 +683,7 @@ class MeshInterface:
self.heartbeatTimer = threading.Timer(i, callback)
self.heartbeatTimer.start()
p = mesh_pb2.ToRadio()
p.heartbeat.CopyFrom(mesh_pb2.Heartbeat())
self._sendToRadio(p)
callback() # run our periodic callback now, it will make another timer if necessary
@@ -700,6 +707,7 @@ class MeshInterface:
self.myInfo = None
self.nodes = {} # nodes keyed by ID
self.nodesByNum = {} # nodes keyed by nodenum
self._localChannels = [] # empty until we start getting channels pushed from the device (during config)
startConfig = mesh_pb2.ToRadio()
self.configId = random.randint(0, 0xFFFFFFFF)
@@ -781,7 +789,12 @@ class MeshInterface:
Done with initial config messages, now send regular MeshPackets
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:
self.queueStatus = queueStatus
@@ -854,7 +867,8 @@ class MeshInterface:
# stream API fromRadio.config_complete_id
logging.debug(f"Config complete ID {self.configId}")
self._handleConfigComplete()
elif fromRadio.HasField("channel"):
self._handleChannel(fromRadio.channel)
elif fromRadio.HasField("packet"):
self._handlePacketFromRadio(fromRadio.packet)
@@ -896,6 +910,10 @@ class MeshInterface:
self.localNode.moduleConfig.external_notification.CopyFrom(
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"):
self.localNode.moduleConfig.range_test.CopyFrom(
fromRadio.moduleConfig.range_test
@@ -977,6 +995,10 @@ class MeshInterface:
self.nodesByNum[nodeNum] = 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):
"""Handle a MeshPacket that just arrived from the radio

View File

File diff suppressed because one or more lines are too long

View File

@@ -271,6 +271,11 @@ class _HardwareModelEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._
"""
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
"""
PRIVATE_HW: _HardwareModel.ValueType # 255
"""
------------------------------------------------------------------------------------------------------------------------------------------
@@ -527,6 +532,11 @@ 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
"""
PRIVATE_HW: HardwareModel.ValueType # 255
"""
------------------------------------------------------------------------------------------------------------------------------------------

View File

@@ -64,14 +64,19 @@ class Node:
"""Show human readable description of our node"""
prefs = ""
if self.localConfig:
prefs = message_to_json(self.localConfig)
prefs = message_to_json(self.localConfig, multiline=True)
print(f"Preferences: {prefs}\n")
prefs = ""
if self.moduleConfig:
prefs = message_to_json(self.moduleConfig)
prefs = message_to_json(self.moduleConfig, multiline=True)
print(f"Module preferences: {prefs}\n")
self.showChannels()
def setChannels(self, channels):
"""Set the channels for this node"""
self.channels = channels
self._fixupChannels()
def requestChannels(self):
"""Send regular MeshPackets to ask channels."""
logging.debug(f"requestChannels for nodeNum:{self.nodeNum}")
@@ -313,6 +318,8 @@ class Node:
):
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)
some_bytes = channelSet.SerializeToString()
s = base64.urlsafe_b64encode(some_bytes).decode("ascii")
@@ -569,6 +576,19 @@ class Node:
onResponse = self.onAckNak
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):
"""Tell the node to shutdown."""
p = admin_pb2.AdminMessage()
@@ -639,7 +659,7 @@ class Node:
"""Fixup indexes and add 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):
ch.index = index # fixup indexes
@@ -711,9 +731,6 @@ class Node:
self.channels = self.partialChannels
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:
self._requestChannel(index + 1)

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*\xee\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\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.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.telemetry_pb2', globals())
@@ -21,16 +21,16 @@ if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
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_end=1303
_TELEMETRYSENSORTYPE._serialized_start=1097
_TELEMETRYSENSORTYPE._serialized_end=1335
_DEVICEMETRICS._serialized_start=43
_DEVICEMETRICS._serialized_end=172
_ENVIRONMENTMETRICS._serialized_start=175
_ENVIRONMENTMETRICS._serialized_end=343
_POWERMETRICS._serialized_start=346
_POWERMETRICS._serialized_end=486
_AIRQUALITYMETRICS._serialized_start=489
_AIRQUALITYMETRICS._serialized_end=808
_TELEMETRY._serialized_start=811
_TELEMETRY._serialized_end=1076
_ENVIRONMENTMETRICS._serialized_end=361
_POWERMETRICS._serialized_start=364
_POWERMETRICS._serialized_end=504
_AIRQUALITYMETRICS._serialized_start=507
_AIRQUALITYMETRICS._serialized_end=826
_TELEMETRY._serialized_start=829
_TELEMETRY._serialized_end=1094
# @@protoc_insertion_point(module_scope)

View File

@@ -86,6 +86,10 @@ class _TelemetrySensorTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wra
"""
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
"""
class TelemetrySensorType(_TelemetrySensorType, metaclass=_TelemetrySensorTypeEnumTypeWrapper):
"""
@@ -156,6 +160,10 @@ BMP085: TelemetrySensorType.ValueType # 15
"""
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
"""
global___TelemetrySensorType = TelemetrySensorType
@typing_extensions.final
@@ -219,6 +227,7 @@ class EnvironmentMetrics(google.protobuf.message.Message):
VOLTAGE_FIELD_NUMBER: builtins.int
CURRENT_FIELD_NUMBER: builtins.int
IAQ_FIELD_NUMBER: builtins.int
DISTANCE_FIELD_NUMBER: builtins.int
temperature: builtins.float
"""
Temperature measured
@@ -248,6 +257,10 @@ class EnvironmentMetrics(google.protobuf.message.Message):
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.
"""
distance: builtins.float
"""
RCWL9620 Doppler Radar Distance Sensor, used for water level detection. Float value in mm.
"""
def __init__(
self,
*,
@@ -258,8 +271,9 @@ class EnvironmentMetrics(google.protobuf.message.Message):
voltage: builtins.float = ...,
current: builtins.float = ...,
iaq: builtins.int = ...,
distance: builtins.float = ...,
) -> 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

View File

@@ -6,7 +6,7 @@ from unittest.mock import MagicMock, patch
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 ..node import Node
@@ -36,12 +36,15 @@ def test_MeshInterface(capsys):
"lastHeard": 1640204888,
}
iface.nodes = {NODE_ID: node}
iface.nodesByNum = {NODE_NUM: node}
myInfo = MagicMock()
iface.myInfo = myInfo
iface.localNode.localConfig.lora.CopyFrom(config_pb2.Config.LoRaConfig())
iface.showInfo()
iface.localNode.showInfo()
iface.showNodes()

View File

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

View File

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

View File

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

View File

@@ -627,6 +627,8 @@ def check_if_newer_version():
return pypi_version
def message_to_json(message):
"Return protobuf message as JSON. Always print all fields, even when not present in data."
return stripnl(MessageToJson(message, always_print_fields_with_no_presence=True))
def message_to_json(message, multiline=False):
"""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
setup(
name="meshtastic",
version="2.3.5",
version="2.3.7",
description="Python API & client shell for talking to Meshtastic devices",
long_description=long_description,
long_description_content_type="text/markdown",