From eab1cdfc464797d00f78121e51e44ca84f4cd7d4 Mon Sep 17 00:00:00 2001
From: Kevin Hester
Date: Fri, 12 Mar 2021 15:42:40 +0800
Subject: [PATCH] 1.2.8
---
docs/meshtastic/admin_pb2.html | 34 +-
docs/meshtastic/channel_pb2.html | 4 +-
docs/meshtastic/deviceonly_pb2.html | 221 ++++-
docs/meshtastic/index.html | 1187 +++++++++++++++++----------
docs/meshtastic/mesh_pb2.html | 10 +-
docs/meshtastic/util.html | 80 +-
meshtastic/__init__.py | 2 -
setup.py | 2 +-
8 files changed, 1070 insertions(+), 470 deletions(-)
diff --git a/docs/meshtastic/admin_pb2.html b/docs/meshtastic/admin_pb2.html
index 46d9221..881a415 100644
--- a/docs/meshtastic/admin_pb2.html
+++ b/docs/meshtastic/admin_pb2.html
@@ -51,7 +51,7 @@ DESCRIPTOR = _descriptor.FileDescriptor(
syntax='proto3',
serialized_options=b'\n\023com.geeksville.meshB\013AdminProtosH\003',
create_key=_descriptor._internal_create_key,
- serialized_pb=b'\n\x0b\x61\x64min.proto\x1a\nmesh.proto\x1a\x11radioconfig.proto\x1a\rchannel.proto\"\x8b\x02\n\x0c\x41\x64minMessage\x12!\n\tset_radio\x18\x01 \x01(\x0b\x32\x0c.RadioConfigH\x00\x12\x1a\n\tset_owner\x18\x02 \x01(\x0b\x32\x05.UserH\x00\x12\x1f\n\x0bset_channel\x18\x03 \x01(\x0b\x32\x08.ChannelH\x00\x12\x1b\n\x11get_radio_request\x18\x04 \x01(\x08H\x00\x12*\n\x12get_radio_response\x18\x05 \x01(\x0b\x32\x0c.RadioConfigH\x00\x12\x1d\n\x13get_channel_request\x18\x06 \x01(\rH\x00\x12(\n\x14get_channel_response\x18\x07 \x01(\x0b\x32\x08.ChannelH\x00\x42\t\n\x07variantB$\n\x13\x63om.geeksville.meshB\x0b\x41\x64minProtosH\x03\x62\x06proto3'
+ serialized_pb=b'\n\x0b\x61\x64min.proto\x1a\nmesh.proto\x1a\x11radioconfig.proto\x1a\rchannel.proto\"\xc7\x02\n\x0c\x41\x64minMessage\x12!\n\tset_radio\x18\x01 \x01(\x0b\x32\x0c.RadioConfigH\x00\x12\x1a\n\tset_owner\x18\x02 \x01(\x0b\x32\x05.UserH\x00\x12\x1f\n\x0bset_channel\x18\x03 \x01(\x0b\x32\x08.ChannelH\x00\x12\x1b\n\x11get_radio_request\x18\x04 \x01(\x08H\x00\x12*\n\x12get_radio_response\x18\x05 \x01(\x0b\x32\x0c.RadioConfigH\x00\x12\x1d\n\x13get_channel_request\x18\x06 \x01(\rH\x00\x12(\n\x14get_channel_response\x18\x07 \x01(\x0b\x32\x08.ChannelH\x00\x12\x1d\n\x13\x63onfirm_set_channel\x18 \x01(\x08H\x00\x12\x1b\n\x11\x63onfirm_set_radio\x18! \x01(\x08H\x00\x42\t\n\x07variantB$\n\x13\x63om.geeksville.meshB\x0b\x41\x64minProtosH\x03\x62\x06proto3'
,
dependencies=[mesh__pb2.DESCRIPTOR,radioconfig__pb2.DESCRIPTOR,channel__pb2.DESCRIPTOR,])
@@ -115,6 +115,20 @@ _ADMINMESSAGE = _descriptor.Descriptor(
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='confirm_set_channel', full_name='AdminMessage.confirm_set_channel', index=7,
+ number=32, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='confirm_set_radio', full_name='AdminMessage.confirm_set_radio', index=8,
+ number=33, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
],
extensions=[
],
@@ -133,7 +147,7 @@ _ADMINMESSAGE = _descriptor.Descriptor(
fields=[]),
],
serialized_start=62,
- serialized_end=329,
+ serialized_end=389,
)
_ADMINMESSAGE.fields_by_name['set_radio'].message_type = radioconfig__pb2._RADIOCONFIG
@@ -162,6 +176,12 @@ _ADMINMESSAGE.fields_by_name['get_channel_request'].containing_oneof = _
_ADMINMESSAGE.oneofs_by_name['variant'].fields.append(
_ADMINMESSAGE.fields_by_name['get_channel_response'])
_ADMINMESSAGE.fields_by_name['get_channel_response'].containing_oneof = _ADMINMESSAGE.oneofs_by_name['variant']
+_ADMINMESSAGE.oneofs_by_name['variant'].fields.append(
+ _ADMINMESSAGE.fields_by_name['confirm_set_channel'])
+_ADMINMESSAGE.fields_by_name['confirm_set_channel'].containing_oneof = _ADMINMESSAGE.oneofs_by_name['variant']
+_ADMINMESSAGE.oneofs_by_name['variant'].fields.append(
+ _ADMINMESSAGE.fields_by_name['confirm_set_radio'])
+_ADMINMESSAGE.fields_by_name['confirm_set_radio'].containing_oneof = _ADMINMESSAGE.oneofs_by_name['variant']
DESCRIPTOR.message_types_by_name['AdminMessage'] = _ADMINMESSAGE
_sym_db.RegisterFileDescriptor(DESCRIPTOR)
@@ -206,6 +226,14 @@ DESCRIPTOR._options = None
Instance variables
+var confirm_set_channel
+-
+
Field AdminMessage.confirm_set_channel
+
+var confirm_set_radio
+-
+
Field AdminMessage.confirm_set_radio
+
var get_channel_request
-
Field AdminMessage.get_channel_request
@@ -256,6 +284,8 @@ DESCRIPTOR._options = None
DESCRIPTOR
+confirm_set_channel
+confirm_set_radio
get_channel_request
get_channel_response
get_radio_request
diff --git a/docs/meshtastic/channel_pb2.html b/docs/meshtastic/channel_pb2.html
index cd9b0b7..0281690 100644
--- a/docs/meshtastic/channel_pb2.html
+++ b/docs/meshtastic/channel_pb2.html
@@ -48,7 +48,7 @@ DESCRIPTOR = _descriptor.FileDescriptor(
syntax='proto3',
serialized_options=b'\n\023com.geeksville.meshB\rChannelProtosH\003',
create_key=_descriptor._internal_create_key,
- serialized_pb=b'\n\rchannel.proto\"\xe6\x02\n\x0f\x43hannelSettings\x12\x10\n\x08tx_power\x18\x01 \x01(\x05\x12\x32\n\x0cmodem_config\x18\x03 \x01(\x0e\x32\x1c.ChannelSettings.ModemConfig\x12\x11\n\tbandwidth\x18\x06 \x01(\r\x12\x15\n\rspread_factor\x18\x07 \x01(\r\x12\x13\n\x0b\x63oding_rate\x18\x08 \x01(\r\x12\x13\n\x0b\x63hannel_num\x18\t \x01(\r\x12\x0b\n\x03psk\x18\x04 \x01(\x0c\x12\x0c\n\x04name\x18\x05 \x01(\t\x12\n\n\x02id\x18\n \x01(\x07\x12\x16\n\x0euplink_enabled\x18\x10 \x01(\x08\x12\x18\n\x10\x64ownlink_enabled\x18\x11 \x01(\x08\"`\n\x0bModemConfig\x12\x12\n\x0e\x42w125Cr45Sf128\x10\x00\x12\x12\n\x0e\x42w500Cr45Sf128\x10\x01\x12\x14\n\x10\x42w31_25Cr48Sf512\x10\x02\x12\x13\n\x0f\x42w125Cr48Sf4096\x10\x03\"\x8b\x01\n\x07\x43hannel\x12\r\n\x05index\x18\x01 \x01(\r\x12\"\n\x08settings\x18\x02 \x01(\x0b\x32\x10.ChannelSettings\x12\x1b\n\x04role\x18\x03 \x01(\x0e\x32\r.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&\n\x13\x63om.geeksville.meshB\rChannelProtosH\x03\x62\x06proto3'
+ serialized_pb=b'\n\rchannel.proto\"\xe6\x02\n\x0f\x43hannelSettings\x12\x10\n\x08tx_power\x18\x01 \x01(\x05\x12\x32\n\x0cmodem_config\x18\x03 \x01(\x0e\x32\x1c.ChannelSettings.ModemConfig\x12\x11\n\tbandwidth\x18\x06 \x01(\r\x12\x15\n\rspread_factor\x18\x07 \x01(\r\x12\x13\n\x0b\x63oding_rate\x18\x08 \x01(\r\x12\x13\n\x0b\x63hannel_num\x18\t \x01(\r\x12\x0b\n\x03psk\x18\x04 \x01(\x0c\x12\x0c\n\x04name\x18\x05 \x01(\t\x12\n\n\x02id\x18\n \x01(\x07\x12\x16\n\x0euplink_enabled\x18\x10 \x01(\x08\x12\x18\n\x10\x64ownlink_enabled\x18\x11 \x01(\x08\"`\n\x0bModemConfig\x12\x12\n\x0e\x42w125Cr45Sf128\x10\x00\x12\x12\n\x0e\x42w500Cr45Sf128\x10\x01\x12\x14\n\x10\x42w31_25Cr48Sf512\x10\x02\x12\x13\n\x0f\x42w125Cr48Sf4096\x10\x03\"\x8b\x01\n\x07\x43hannel\x12\r\n\x05index\x18\x01 \x01(\x05\x12\"\n\x08settings\x18\x02 \x01(\x0b\x32\x10.ChannelSettings\x12\x1b\n\x04role\x18\x03 \x01(\x0e\x32\r.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&\n\x13\x63om.geeksville.meshB\rChannelProtosH\x03\x62\x06proto3'
)
@@ -232,7 +232,7 @@ _CHANNEL = _descriptor.Descriptor(
fields=[
_descriptor.FieldDescriptor(
name='index', full_name='Channel.index', index=0,
- number=1, type=13, cpp_type=3, label=1,
+ number=1, type=5, cpp_type=1, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
diff --git a/docs/meshtastic/deviceonly_pb2.html b/docs/meshtastic/deviceonly_pb2.html
index e9b1ec6..548c977 100644
--- a/docs/meshtastic/deviceonly_pb2.html
+++ b/docs/meshtastic/deviceonly_pb2.html
@@ -41,8 +41,8 @@ _sym_db = _symbol_database.Default()
from . import mesh_pb2 as mesh__pb2
-from . import radioconfig_pb2 as radioconfig__pb2
from . import channel_pb2 as channel__pb2
+from . import radioconfig_pb2 as radioconfig__pb2
DESCRIPTOR = _descriptor.FileDescriptor(
@@ -51,13 +51,76 @@ DESCRIPTOR = _descriptor.FileDescriptor(
syntax='proto3',
serialized_options=b'\n\023com.geeksville.meshB\nDeviceOnlyH\003',
create_key=_descriptor._internal_create_key,
- serialized_pb=b'\n\x10\x64\x65viceonly.proto\x1a\nmesh.proto\x1a\x11radioconfig.proto\x1a\rchannel.proto\"\x9f\x02\n\x0b\x44\x65viceState\x12\x1b\n\x05radio\x18\x01 \x01(\x0b\x32\x0c.RadioConfig\x12\x1c\n\x07my_node\x18\x02 \x01(\x0b\x32\x0b.MyNodeInfo\x12\x14\n\x05owner\x18\x03 \x01(\x0b\x32\x05.User\x12\x1a\n\x07node_db\x18\x04 \x03(\x0b\x32\t.NodeInfo\x12\"\n\rreceive_queue\x18\x05 \x03(\x0b\x32\x0b.MeshPacket\x12\x0f\n\x07version\x18\x08 \x01(\r\x12$\n\x0frx_text_message\x18\x07 \x01(\x0b\x32\x0b.MeshPacket\x12\x0f\n\x07no_save\x18\t \x01(\x08\x12\x15\n\rdid_gps_reset\x18\x0b \x01(\x08\x12\x1a\n\x08\x63hannels\x18\r \x03(\x0b\x32\x08.ChannelJ\x04\x08\x0c\x10\rB#\n\x13\x63om.geeksville.meshB\nDeviceOnlyH\x03\x62\x06proto3'
+ serialized_pb=b'\n\x10\x64\x65viceonly.proto\x1a\nmesh.proto\x1a\rchannel.proto\x1a\x11radioconfig.proto\"\x80\x01\n\x11LegacyRadioConfig\x12\x39\n\x0bpreferences\x18\x01 \x01(\x0b\x32$.LegacyRadioConfig.LegacyPreferences\x1a\x30\n\x11LegacyPreferences\x12\x1b\n\x06region\x18\x0f \x01(\x0e\x32\x0b.RegionCode\"\x8f\x02\n\x0b\x44\x65viceState\x12\'\n\x0blegacyRadio\x18\x01 \x01(\x0b\x32\x12.LegacyRadioConfig\x12\x1c\n\x07my_node\x18\x02 \x01(\x0b\x32\x0b.MyNodeInfo\x12\x14\n\x05owner\x18\x03 \x01(\x0b\x32\x05.User\x12\x1a\n\x07node_db\x18\x04 \x03(\x0b\x32\t.NodeInfo\x12\"\n\rreceive_queue\x18\x05 \x03(\x0b\x32\x0b.MeshPacket\x12\x0f\n\x07version\x18\x08 \x01(\r\x12$\n\x0frx_text_message\x18\x07 \x01(\x0b\x32\x0b.MeshPacket\x12\x0f\n\x07no_save\x18\t \x01(\x08\x12\x15\n\rdid_gps_reset\x18\x0b \x01(\x08J\x04\x08\x0c\x10\r\")\n\x0b\x43hannelFile\x12\x1a\n\x08\x63hannels\x18\x01 \x03(\x0b\x32\x08.ChannelB#\n\x13\x63om.geeksville.meshB\nDeviceOnlyH\x03\x62\x06proto3'
,
- dependencies=[mesh__pb2.DESCRIPTOR,radioconfig__pb2.DESCRIPTOR,channel__pb2.DESCRIPTOR,])
+ dependencies=[mesh__pb2.DESCRIPTOR,channel__pb2.DESCRIPTOR,radioconfig__pb2.DESCRIPTOR,])
+_LEGACYRADIOCONFIG_LEGACYPREFERENCES = _descriptor.Descriptor(
+ name='LegacyPreferences',
+ full_name='LegacyRadioConfig.LegacyPreferences',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='region', full_name='LegacyRadioConfig.LegacyPreferences.region', index=0,
+ number=15, type=14, cpp_type=8, label=1,
+ has_default_value=False, default_value=0,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=147,
+ serialized_end=195,
+)
+
+_LEGACYRADIOCONFIG = _descriptor.Descriptor(
+ name='LegacyRadioConfig',
+ full_name='LegacyRadioConfig',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='preferences', full_name='LegacyRadioConfig.preferences', index=0,
+ number=1, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ ],
+ extensions=[
+ ],
+ nested_types=[_LEGACYRADIOCONFIG_LEGACYPREFERENCES, ],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=67,
+ serialized_end=195,
+)
+
+
_DEVICESTATE = _descriptor.Descriptor(
name='DeviceState',
full_name='DeviceState',
@@ -67,7 +130,7 @@ _DEVICESTATE = _descriptor.Descriptor(
create_key=_descriptor._internal_create_key,
fields=[
_descriptor.FieldDescriptor(
- name='radio', full_name='DeviceState.radio', index=0,
+ name='legacyRadio', full_name='DeviceState.legacyRadio', index=0,
number=1, type=11, cpp_type=10, label=1,
has_default_value=False, default_value=None,
message_type=None, enum_type=None, containing_type=None,
@@ -129,9 +192,34 @@ _DEVICESTATE = _descriptor.Descriptor(
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=198,
+ serialized_end=469,
+)
+
+
+_CHANNELFILE = _descriptor.Descriptor(
+ name='ChannelFile',
+ full_name='ChannelFile',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
_descriptor.FieldDescriptor(
- name='channels', full_name='DeviceState.channels', index=9,
- number=13, type=11, cpp_type=10, label=3,
+ name='channels', full_name='ChannelFile.channels', index=0,
+ number=1, type=11, cpp_type=10, label=3,
has_default_value=False, default_value=[],
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
@@ -148,20 +236,40 @@ _DEVICESTATE = _descriptor.Descriptor(
extension_ranges=[],
oneofs=[
],
- serialized_start=67,
- serialized_end=354,
+ serialized_start=471,
+ serialized_end=512,
)
-_DEVICESTATE.fields_by_name['radio'].message_type = radioconfig__pb2._RADIOCONFIG
+_LEGACYRADIOCONFIG_LEGACYPREFERENCES.fields_by_name['region'].enum_type = radioconfig__pb2._REGIONCODE
+_LEGACYRADIOCONFIG_LEGACYPREFERENCES.containing_type = _LEGACYRADIOCONFIG
+_LEGACYRADIOCONFIG.fields_by_name['preferences'].message_type = _LEGACYRADIOCONFIG_LEGACYPREFERENCES
+_DEVICESTATE.fields_by_name['legacyRadio'].message_type = _LEGACYRADIOCONFIG
_DEVICESTATE.fields_by_name['my_node'].message_type = mesh__pb2._MYNODEINFO
_DEVICESTATE.fields_by_name['owner'].message_type = mesh__pb2._USER
_DEVICESTATE.fields_by_name['node_db'].message_type = mesh__pb2._NODEINFO
_DEVICESTATE.fields_by_name['receive_queue'].message_type = mesh__pb2._MESHPACKET
_DEVICESTATE.fields_by_name['rx_text_message'].message_type = mesh__pb2._MESHPACKET
-_DEVICESTATE.fields_by_name['channels'].message_type = channel__pb2._CHANNEL
+_CHANNELFILE.fields_by_name['channels'].message_type = channel__pb2._CHANNEL
+DESCRIPTOR.message_types_by_name['LegacyRadioConfig'] = _LEGACYRADIOCONFIG
DESCRIPTOR.message_types_by_name['DeviceState'] = _DEVICESTATE
+DESCRIPTOR.message_types_by_name['ChannelFile'] = _CHANNELFILE
_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+LegacyRadioConfig = _reflection.GeneratedProtocolMessageType('LegacyRadioConfig', (_message.Message,), {
+
+ 'LegacyPreferences' : _reflection.GeneratedProtocolMessageType('LegacyPreferences', (_message.Message,), {
+ 'DESCRIPTOR' : _LEGACYRADIOCONFIG_LEGACYPREFERENCES,
+ '__module__' : 'deviceonly_pb2'
+ # @@protoc_insertion_point(class_scope:LegacyRadioConfig.LegacyPreferences)
+ })
+ ,
+ 'DESCRIPTOR' : _LEGACYRADIOCONFIG,
+ '__module__' : 'deviceonly_pb2'
+ # @@protoc_insertion_point(class_scope:LegacyRadioConfig)
+ })
+_sym_db.RegisterMessage(LegacyRadioConfig)
+_sym_db.RegisterMessage(LegacyRadioConfig.LegacyPreferences)
+
DeviceState = _reflection.GeneratedProtocolMessageType('DeviceState', (_message.Message,), {
'DESCRIPTOR' : _DEVICESTATE,
'__module__' : 'deviceonly_pb2'
@@ -169,6 +277,13 @@ DeviceState = _reflection.GeneratedProtocolMessageType('DeviceState', (_
})
_sym_db.RegisterMessage(DeviceState)
+ChannelFile = _reflection.GeneratedProtocolMessageType('ChannelFile', (_message.Message,), {
+ 'DESCRIPTOR' : _CHANNELFILE,
+ '__module__' : 'deviceonly_pb2'
+ # @@protoc_insertion_point(class_scope:ChannelFile)
+ })
+_sym_db.RegisterMessage(ChannelFile)
+
DESCRIPTOR._options = None
# @@protoc_insertion_point(module_scope)
@@ -183,6 +298,32 @@ DESCRIPTOR._options = None
+
+class ChannelFile
+(*args, **kwargs)
+
+-
+
+
Ancestors
+
+- google.protobuf.pyext._message.CMessage
+- google.protobuf.message.Message
+
+Class variables
+
+var DESCRIPTOR
+-
+
+
+
+Instance variables
+
+var channels
+-
+
Field ChannelFile.channels
+
+
+
class DeviceState
(*args, **kwargs)
@@ -203,14 +344,14 @@ DESCRIPTOR._options = None
Instance variables
-var channels
--
-
Field DeviceState.channels
-
var did_gps_reset
-
Field DeviceState.did_gps_reset
+var legacyRadio
+-
+
Field DeviceState.legacyRadio
+
var my_node
-
Field DeviceState.my_node
@@ -227,10 +368,6 @@ DESCRIPTOR._options = None
-
- var radio
--
-
-
var receive_queue
-
Field DeviceState.receive_queue
@@ -245,6 +382,36 @@ DESCRIPTOR._options = None
+
+class LegacyRadioConfig
+(*args, **kwargs)
+
+-
+
+
Ancestors
+
+- google.protobuf.pyext._message.CMessage
+- google.protobuf.message.Message
+
+Class variables
+
+var DESCRIPTOR
+-
+
+
+var LegacyPreferences
+-
+
+
+
+Instance variables
+
+var preferences
+-
+
Field LegacyRadioConfig.preferences
+
+
+
@@ -262,21 +429,35 @@ DESCRIPTOR._options = None
diff --git a/docs/meshtastic/index.html b/docs/meshtastic/index.html
index 7b0601c..40aa676 100644
--- a/docs/meshtastic/index.html
+++ b/docs/meshtastic/index.html
@@ -155,10 +155,11 @@ import base64
import platform
import socket
from . import mesh_pb2, portnums_pb2, apponly_pb2, admin_pb2, environmental_measurement_pb2, remote_hardware_pb2, channel_pb2, radioconfig_pb2, util
-from .util import fixme, catchAndIgnore, stripnl
+from .util import fixme, catchAndIgnore, stripnl, DeferredExecution
from pubsub import pub
from dotmap import DotMap
from typing import *
+from google.protobuf.json_format import MessageToJson
START1 = 0x94
START2 = 0xc3
@@ -166,19 +167,22 @@ HEADER_LEN = 4
MAX_TO_FROM_RADIO_SIZE = 512
defaultHopLimit = 3
-BROADCAST_ADDR = "^all" # A special ID that means broadcast
+"""A special ID that means broadcast"""
+BROADCAST_ADDR = "^all"
+
+"""A special ID that means the local node"""
+LOCAL_ADDR = "^local"
# if using 8 bit nodenums this will be shortend on the target
BROADCAST_NUM = 0xffffffff
-MY_CONFIG_ID = 42
-
"""The numeric buildnumber (shared with android apps) specifying the level of device code we are guaranteed to understand
format is Mmmss (where M is 1+the numeric major number. i.e. 20120 means 1.1.20
"""
OUR_APP_VERSION = 20200
+publishingThread = DeferredExecution("publishing")
class ResponseHandler(NamedTuple):
"""A pending response callback, waiting for a response to one of our messages"""
@@ -197,6 +201,208 @@ class KnownProtocol(NamedTuple):
onReceive: Callable = None
+def waitForSet(target, sleep=.1, maxsecs=20, attrs=()):
+ """Block until the specified attributes are set. Returns True if config has been received."""
+ for _ in range(int(maxsecs/sleep)):
+ if all(map(lambda a: getattr(target, a, None), attrs)):
+ return True
+ time.sleep(sleep)
+ return False
+
+
+class Node:
+ """A model of a (local or remote) node in the mesh
+
+ Includes methods for radioConfig and channels
+ """
+
+ def __init__(self, iface, nodeNum):
+ """Constructor"""
+ self.iface = iface
+ self.nodeNum = nodeNum
+ self.radioConfig = None
+ self.channels = None
+
+ def showInfo(self):
+ """Show human readable description of our node"""
+ print(self.radioConfig)
+ print("Channels:")
+ for c in self.channels:
+ if c.role != channel_pb2.Channel.Role.DISABLED:
+ cStr = MessageToJson(c.settings).replace("\n", "")
+ print(f" {channel_pb2.Channel.Role.Name(c.role)} {cStr}")
+ print(f"\nChannel URL {self.channelURL}")
+
+ def requestConfig(self):
+ """
+ Send regular MeshPackets to ask for settings and channels
+ """
+ self.radioConfig = None
+ self.channels = None
+ self.partialChannels = [] # We keep our channels in a temp array until finished
+
+ self._requestSettings()
+
+ def waitForConfig(self, maxsecs=20):
+ """Block until radio config is received. Returns True if config has been received."""
+ return waitForSet(self, attrs=('radioConfig', 'channels'), maxsecs=maxsecs)
+
+ def writeConfig(self):
+ """Write the current (edited) radioConfig to the device"""
+ if self.radioConfig == None:
+ raise Exception("No RadioConfig has been read")
+
+ p = admin_pb2.AdminMessage()
+ p.set_radio.CopyFrom(self.radioConfig)
+
+ self._sendAdmin(p)
+ logging.debug("Wrote config")
+
+ def writeChannel(self, channelIndex):
+ """Write the current (edited) channel to the device"""
+
+ p = admin_pb2.AdminMessage()
+ p.set_channel.CopyFrom(self.channels[channelIndex])
+
+ self._sendAdmin(p)
+ logging.debug("Wrote channel {channelIndex}")
+
+ def setOwner(self, long_name, short_name=None):
+ """Set device owner name"""
+ nChars = 3
+ minChars = 2
+ if long_name is not None:
+ long_name = long_name.strip()
+ if short_name is None:
+ words = long_name.split()
+ if len(long_name) <= nChars:
+ short_name = long_name
+ elif len(words) >= minChars:
+ short_name = ''.join(map(lambda word: word[0], words))
+ else:
+ trans = str.maketrans(dict.fromkeys('aeiouAEIOU'))
+ short_name = long_name[0] + long_name[1:].translate(trans)
+ if len(short_name) < nChars:
+ short_name = long_name[:nChars]
+
+ p = admin_pb2.AdminMessage()
+
+ if long_name is not None:
+ p.set_owner.long_name = long_name
+ if short_name is not None:
+ short_name = short_name.strip()
+ if len(short_name) > nChars:
+ short_name = short_name[:nChars]
+ p.set_owner.short_name = short_name
+
+ return self._sendAdmin(p)
+
+ @property
+ def channelURL(self):
+ """The sharable URL that describes the current channel
+ """
+ # Only keep the primary/secondary channels, assume primary is first
+ channelSet = apponly_pb2.ChannelSet()
+ for c in self.channels:
+ if c.role != channel_pb2.Channel.Role.DISABLED:
+ channelSet.settings.append(c.settings)
+ bytes = channelSet.SerializeToString()
+ s = base64.urlsafe_b64encode(bytes).decode('ascii')
+ return f"https://www.meshtastic.org/d/#{s}".replace("=", "")
+
+ def setURL(self, url):
+ """Set mesh network URL"""
+ if self.radioConfig == None:
+ raise Exception("No RadioConfig has been read")
+
+ # URLs are of the form https://www.meshtastic.org/d/#{base64_channel_set}
+ # Split on '/#' to find the base64 encoded channel settings
+ splitURL = url.split("/#")
+ b64 = splitURL[-1]
+
+ # We normally strip padding to make for a shorter URL, but the python parser doesn't like
+ # that. So add back any missing padding
+ # per https://stackoverflow.com/a/9807138
+ missing_padding = len(b64) % 4
+ if missing_padding:
+ b64 += '=' * (4 - missing_padding)
+
+ decodedURL = base64.urlsafe_b64decode(b64)
+ channelSet = apponly_pb2.ChannelSet()
+ channelSet.ParseFromString(decodedURL)
+
+ i = 0
+ for chs in channelSet.settings:
+ ch = channel_pb2.Channel()
+ ch.role = channel_pb2.Channel.Role.PRIMARY if i == 0 else channel_pb2.Channel.Role.SECONDARY
+ ch.index = i
+ ch.settings.CopyFrom(chs)
+ self.channels[ch.index] = ch
+ self.writeChannel(ch.index)
+ i = i + 1
+
+ def _requestSettings(self):
+ """
+ Done with initial config messages, now send regular MeshPackets to ask for settings
+ """
+ p = admin_pb2.AdminMessage()
+ p.get_radio_request = True
+
+ def onResponse(p):
+ """A closure to handle the response packet"""
+ self.radioConfig = p["decoded"]["admin"]["raw"].get_radio_response
+ logging.debug("Received radio config, now fetching channels...")
+ self._requestChannel(0) # now start fetching channels
+
+ return self._sendAdmin(p,
+ wantResponse=True,
+ onResponse=onResponse)
+
+ def _requestChannel(self, channelNum: int):
+ """
+ Done with initial config messages, now send regular MeshPackets to ask for settings
+ """
+ p = admin_pb2.AdminMessage()
+ p.get_channel_request = channelNum + 1
+ logging.debug(f"Requesting channel {channelNum}")
+
+ def onResponse(p):
+ """A closure to handle the response packet"""
+ c = p["decoded"]["admin"]["raw"].get_channel_response
+ self.partialChannels.append(c)
+ logging.debug(f"Received channel {stripnl(c)}")
+ index = c.index
+
+ # for stress testing, we can always download all channels
+ fastChannelDownload = True
+
+ # Once we see a response that has NO settings, assume we are at the end of channels and stop fetching
+ quitEarly = (
+ c.role == channel_pb2.Channel.Role.DISABLED) and fastChannelDownload
+
+ if quitEarly or index >= self.iface.myInfo.max_channels - 1:
+ logging.debug("Finished downloading channels")
+ self.channels = self.partialChannels
+ # FIXME, the following should only be called after we have settings and channels
+ self.iface._connected() # Tell everone else we are ready to go
+ else:
+ self._requestChannel(index + 1)
+
+ return self._sendAdmin(p,
+ wantResponse=True,
+ onResponse=onResponse)
+
+ def _sendAdmin(self, p: admin_pb2.AdminMessage, wantResponse=False,
+ onResponse=None):
+ """Send an admin message to the specified node (or the local node if destNodeNum is zero)"""
+
+ return self.iface.sendData(p, self.nodeNum,
+ portNum=portnums_pb2.PortNum.ADMIN_APP,
+ wantAck=True,
+ wantResponse=wantResponse,
+ onResponse=onResponse)
+
+
class MeshInterface:
"""Interface class for meshtastic devices
@@ -217,6 +423,7 @@ class MeshInterface:
self.nodes = None # FIXME
self.isConnected = threading.Event()
self.noProto = noProto
+ self.localNode = Node(self, -1) # We fixup nodenum later
self.myInfo = None # We don't have device info yet
self.responseHandlers = {} # A map from request ID to the handler
self.failure = None # If we've encountered a fatal exception it will be kept here
@@ -235,6 +442,25 @@ class MeshInterface:
logging.error(f'Traceback: {traceback}')
self.close()
+ def showInfo(self):
+ """Show human readable summary about this object"""
+ print(self.myInfo)
+ print("Nodes in mesh:")
+ for n in self.nodes.values():
+ print(stripnl(n))
+
+ def getNode(self, nodeId):
+ """Return a node object which contains device settings and channel info"""
+ if nodeId == LOCAL_ADDR:
+ return self.localNode
+ else:
+ logging.info("Requesting configuration from remote node (this could take a while)")
+ n = Node(self, nodeId)
+ n.requestConfig()
+ if not n.waitForConfig(maxsecs = 60):
+ raise Exception("Timed out waiting for node config")
+ return n
+
def sendText(self, text: AnyStr,
destinationId=BROADCAST_ADDR,
wantAck=False,
@@ -285,7 +511,7 @@ class MeshInterface:
if len(data) > mesh_pb2.Constants.DATA_PAYLOAD_LEN:
raise Exception("Data payload too big")
- if portNum == portnums_pb2.PortNum.UNKNOWN_APP: # we are now more strict wrt port numbers
+ if portNum == portnums_pb2.PortNum.UNKNOWN_APP: # we are now more strict wrt port numbers
raise Exception("A non-zero port number must be specified")
meshPacket = mesh_pb2.MeshPacket()
@@ -293,7 +519,8 @@ class MeshInterface:
meshPacket.decoded.portnum = portNum
meshPacket.decoded.want_response = wantResponse
- p = self._sendPacket(meshPacket, destinationId, wantAck=wantAck, hopLimit=hopLimit)
+ p = self._sendPacket(meshPacket, destinationId,
+ wantAck=wantAck, hopLimit=hopLimit)
if onResponse is not None:
self._addResponseHandler(p.id, onResponse)
return p
@@ -353,8 +580,15 @@ class MeshInterface:
nodeNum = destinationId
elif destinationId == BROADCAST_ADDR:
nodeNum = BROADCAST_NUM
+ elif destinationId == LOCAL_ADDR:
+ nodeNum = self.myInfo.my_node_num
+ elif destinationId.startswith("!"): # A simple hex style nodeid - we can parse this without needing the DB
+ nodeNum = int(destinationId[1:], 16)
else:
- nodeNum = self.nodes[destinationId]['num']
+ node = self.nodes.get(destinationId)
+ if not node:
+ raise Exception(f"NodeId {destinationId} not found in DB")
+ nodeNum = node['num']
meshPacket.to = nodeNum
meshPacket.want_ack = wantAck
@@ -370,37 +604,11 @@ class MeshInterface:
self._sendToRadio(toRadio)
return meshPacket
- def waitForConfig(self, sleep=.1, maxsecs=20, attrs=('myInfo', 'nodes', 'radioConfig', 'channels')):
+ def waitForConfig(self):
"""Block until radio config is received. Returns True if config has been received."""
- for _ in range(int(maxsecs/sleep)):
- if all(map(lambda a: getattr(self, a, None), attrs)):
- return True
- time.sleep(sleep)
- return False
-
- def writeConfig(self):
- """Write the current (edited) radioConfig to the device"""
- if self.radioConfig == None:
- raise Exception("No RadioConfig has been read")
-
- p = admin_pb2.AdminMessage()
- p.set_radio.CopyFrom(self.radioConfig)
-
- self.sendData(p, self.myInfo.my_node_num,
- portNum=portnums_pb2.PortNum.ADMIN_APP,
- wantAck=True)
- logging.debug("Wrote config")
-
- def writeChannel(self, channelIndex):
- """Write the current (edited) channel to the device"""
-
- p = admin_pb2.AdminMessage()
- p.set_channel.CopyFrom(self.channels[channelIndex])
-
- self.sendData(p, self.myInfo.my_node_num,
- portNum=portnums_pb2.PortNum.ADMIN_APP,
- wantAck=True)
- logging.debug("Wrote channel {channelIndex}")
+ success = waitForSet(self, attrs=('myInfo', 'nodes')) and self.localNode.waitForConfig()
+ if not success:
+ raise Exception("Timed out waiting for interface config")
def getMyNodeInfo(self):
if self.myInfo is None:
@@ -425,82 +633,6 @@ class MeshInterface:
return user.get('shortName', None)
return None
- def setOwner(self, long_name, short_name=None):
- """Set device owner name"""
- nChars = 3
- minChars = 2
- if long_name is not None:
- long_name = long_name.strip()
- if short_name is None:
- words = long_name.split()
- if len(long_name) <= nChars:
- short_name = long_name
- elif len(words) >= minChars:
- short_name = ''.join(map(lambda word: word[0], words))
- else:
- trans = str.maketrans(dict.fromkeys('aeiouAEIOU'))
- short_name = long_name[0] + long_name[1:].translate(trans)
- if len(short_name) < nChars:
- short_name = long_name[:nChars]
-
- p = admin_pb2.AdminMessage()
-
- if long_name is not None:
- p.set_owner.long_name = long_name
- if short_name is not None:
- short_name = short_name.strip()
- if len(short_name) > nChars:
- short_name = short_name[:nChars]
- p.set_owner.short_name = short_name
-
- return self.sendData(p, self.myInfo.my_node_num,
- portNum=portnums_pb2.PortNum.ADMIN_APP,
- wantAck=True)
-
- @property
- def channelURL(self):
- """The sharable URL that describes the current channel
- """
- # Only keep the primary/secondary channels, assume primary is first
- channelSet = apponly_pb2.ChannelSet()
- for c in self.channels:
- if c.role != channel_pb2.Channel.Role.DISABLED:
- channelSet.settings.append(c.settings)
- bytes = channelSet.SerializeToString()
- s = base64.urlsafe_b64encode(bytes).decode('ascii')
- return f"https://www.meshtastic.org/d/#{s}".replace("=", "")
-
- def setURL(self, url):
- """Set mesh network URL"""
- if self.radioConfig == None:
- raise Exception("No RadioConfig has been read")
-
- # URLs are of the form https://www.meshtastic.org/d/#{base64_channel_set}
- # Split on '/#' to find the base64 encoded channel settings
- splitURL = url.split("/#")
- b64 = splitURL[-1]
-
- # We normally strip padding to make for a shorter URL, but the python parser doesn't like
- # that. So add back any missing padding
- # per https://stackoverflow.com/a/9807138
- missing_padding = len(b64) % 4
- if missing_padding:
- b64 += '='* (4 - missing_padding)
-
- decodedURL = base64.urlsafe_b64decode(b64)
- channelSet = apponly_pb2.ChannelSet()
- channelSet.ParseFromString(decodedURL)
-
- i = 0
- for chs in channelSet.settings:
- ch = channel_pb2.Channel()
- ch.role = channel_pb2.Channel.Role.PRIMARY if i == 0 else channel_pb2.Channel.Role.SECONDARY
- ch.index = i
- ch.settings.CopyFrom(chs)
- self.channels[ch.index] = ch
- self.writeChannel(ch.index)
- i = i + 1
-
def _waitConnected(self):
"""Block until the initial node db download is complete, or timeout
and raise an exception"""
@@ -522,27 +654,29 @@ class MeshInterface:
def _disconnected(self):
"""Called by subclasses to tell clients this interface has disconnected"""
self.isConnected.clear()
- catchAndIgnore("disconnection publish", lambda: pub.sendMessage(
+ publishingThread.queueWork(lambda: pub.sendMessage(
"meshtastic.connection.lost", interface=self))
def _connected(self):
"""Called by this class to tell clients we are now fully connected to a node
"""
- self.isConnected.set()
- catchAndIgnore("connection publish", lambda: pub.sendMessage(
- "meshtastic.connection.established", interface=self))
+ # (because I'm lazy) _connected might be called when remote Node
+ # objects complete their config reads, don't generate redundant isConnected
+ # for the local interface
+ if not self.isConnected.is_set():
+ self.isConnected.set()
+ publishingThread.queueWork(lambda: pub.sendMessage(
+ "meshtastic.connection.established", interface=self))
def _startConfig(self):
"""Start device packets flowing"""
self.myInfo = None
self.nodes = {} # nodes keyed by ID
self.nodesByNum = {} # nodes keyed by nodenum
- self.radioConfig = None
- self.channels = None
- self.partialChannels = [] # We keep our channels in a temp array until finished
startConfig = mesh_pb2.ToRadio()
- startConfig.want_config_id = MY_CONFIG_ID # we don't use this value
+ self.configId = random.randint(0, 0xffffffff)
+ startConfig.want_config_id = self.configId
self._sendToRadio(startConfig)
def _sendToRadio(self, toRadio):
@@ -562,59 +696,7 @@ class MeshInterface:
"""
Done with initial config messages, now send regular MeshPackets to ask for settings and channels
"""
- self._requestSettings()
- self._requestChannel(0)
-
- def _requestSettings(self):
- """
- Done with initial config messages, now send regular MeshPackets to ask for settings
- """
- p = admin_pb2.AdminMessage()
- p.get_radio_request = True
-
- def onResponse(p):
- """A closure to handle the response packet"""
- self.radioConfig = p["decoded"]["admin"]["raw"].get_radio_response
-
- return self.sendData(p, self.myInfo.my_node_num,
- portNum=portnums_pb2.PortNum.ADMIN_APP,
- wantAck=True,
- wantResponse=True,
- onResponse=onResponse)
-
- def _requestChannel(self, channelNum: int):
- """
- Done with initial config messages, now send regular MeshPackets to ask for settings
- """
- p = admin_pb2.AdminMessage()
- p.get_channel_request = channelNum + 1
- logging.debug(f"Requesting channel {channelNum}")
-
- def onResponse(p):
- """A closure to handle the response packet"""
- c = p["decoded"]["admin"]["raw"].get_channel_response
- self.partialChannels.append(c)
- logging.debug(f"Received channel {stripnl(c)}")
- index = c.index
-
- # for stress testing, we can always download all channels
- fastChannelDownload = True
-
- # Once we see a response that has NO settings, assume we are at the end of channels and stop fetching
- quitEarly = (c.role == channel_pb2.Channel.Role.DISABLED) and fastChannelDownload
-
- if quitEarly or index >= self.myInfo.max_channels - 1:
- self.channels = self.partialChannels
- # FIXME, the following should only be called after we have settings and channels
- self._connected() # Tell everone else we are ready to go
- else:
- self._requestChannel(index + 1)
-
- return self.sendData(p, self.myInfo.my_node_num,
- portNum=portnums_pb2.PortNum.ADMIN_APP,
- wantAck=True,
- wantResponse=True,
- onResponse=onResponse)
+ self.localNode.requestConfig()
def _handleFromRadio(self, fromRadioBytes):
"""
@@ -624,8 +706,10 @@ class MeshInterface:
fromRadio = mesh_pb2.FromRadio()
fromRadio.ParseFromString(fromRadioBytes)
asDict = google.protobuf.json_format.MessageToDict(fromRadio)
+ # logging.debug(f"Received from radio: {fromRadio}")
if fromRadio.HasField("my_info"):
self.myInfo = fromRadio.my_info
+ self.localNode.nodeNum = self.myInfo.my_node_num
logging.debug(f"Received myinfo: {stripnl(fromRadio.my_info)}")
failmsg = None
@@ -654,10 +738,11 @@ class MeshInterface:
self.nodesByNum[node["num"]] = node
if "user" in node: # Some nodes might not have user/ids assigned yet
self.nodes[node["user"]["id"]] = node
- pub.sendMessage("meshtastic.node.updated",
- node=node, interface=self)
- elif fromRadio.config_complete_id == MY_CONFIG_ID:
+ publishingThread.queueWork(lambda: pub.sendMessage("meshtastic.node.updated",
+ node=node, interface=self))
+ elif fromRadio.config_complete_id == self.configId:
# we ignore the config_complete_id, it is unneeded for our stream API fromRadio.config_complete_id
+ logging.debug(f"Config complete ID {self.configId}")
self._handleConfigComplete()
elif fromRadio.HasField("packet"):
self._handlePacketFromRadio(fromRadio.packet)
@@ -695,7 +780,7 @@ class MeshInterface:
try:
return self.nodesByNum[num]["user"]["id"]
except:
- logging.warn("Node not found for fromId")
+ logging.debug(f"Node {num} not found for fromId")
return None
def _getOrCreateByNum(self, nodeNum):
@@ -730,14 +815,21 @@ class MeshInterface:
# from might be missing if the nodenum was zero.
if not "from" in asDict:
asDict["from"] = 0
- logging.error(f"Device returned a packet we sent, ignoring: {stripnl(asDict)}")
+ logging.error(
+ f"Device returned a packet we sent, ignoring: {stripnl(asDict)}")
return
if not "to" in asDict:
asDict["to"] = 0
# /add fromId and toId fields based on the node ID
- asDict["fromId"] = self._nodeNumToId(asDict["from"])
- asDict["toId"] = self._nodeNumToId(asDict["to"])
+ try:
+ asDict["fromId"] = self._nodeNumToId(asDict["from"])
+ except Exception as ex:
+ logging.warn(f"Not populating fromId {ex}")
+ try:
+ asDict["toId"] = self._nodeNumToId(asDict["to"])
+ except Exception as ex:
+ logging.warn(f"Not populating toId {ex}")
# We could provide our objects as DotMaps - which work with . notation or as dictionaries
# asObj = DotMap(asDict)
@@ -794,7 +886,7 @@ class MeshInterface:
handler.callback(asDict)
logging.debug(f"Publishing {topic}: packet={stripnl(asDict)} ")
- catchAndIgnore(f"publishing {topic}", lambda: pub.sendMessage(
+ publishingThread.queueWork(lambda: pub.sendMessage(
topic, packet=asDict, interface=self))
@@ -841,7 +933,6 @@ class BLEInterface(MeshInterface):
if not wasEmpty:
self._handleFromRadio(b)
-
class StreamInterface(MeshInterface):
"""Interface class for meshtastic devices over a stream link (serial, TCP, etc)"""
@@ -871,7 +962,8 @@ class StreamInterface(MeshInterface):
# Start the reader thread after superclass constructor completes init
if connectNow:
self.connect()
- self.waitForConfig()
+ if not noProto:
+ self.waitForConfig()
def connect(self):
"""Connect to our radio
@@ -931,8 +1023,8 @@ class StreamInterface(MeshInterface):
# logging.debug("reading character")
b = self._readBytes(1)
# logging.debug("In reader loop")
+ # logging.debug(f"read returned {b}")
if len(b) > 0:
- # logging.debug(f"read returned {b}")
c = b[0]
ptr = len(self._rxBuf)
@@ -1183,14 +1275,43 @@ protocols = {
-var MY_CONFIG_ID
+var BROADCAST_ADDR
+-
+
A special ID that means the local node
+
+var BROADCAST_NUM
-
The numeric buildnumber (shared with android apps) specifying the level of device code we are guaranteed to understand
format is Mmmss (where M is 1+the numeric major number. i.e. 20120 means 1.1.20
+var defaultHopLimit
+-
+
A special ID that means broadcast
+
+
+
+
+def waitForSet(target, sleep=0.1, maxsecs=20, attrs=())
+
+-
+
Block until the specified attributes are set. Returns True if config has been received.
+
+
+Expand source code
+
+def waitForSet(target, sleep=.1, maxsecs=20, attrs=()):
+ """Block until the specified attributes are set. Returns True if config has been received."""
+ for _ in range(int(maxsecs/sleep)):
+ if all(map(lambda a: getattr(target, a, None), attrs)):
+ return True
+ time.sleep(sleep)
+ return False
+
+
+
@@ -1269,15 +1390,12 @@ noProto – If True, don't try to run our protocol on the link - just be a d
@@ -1358,6 +1476,7 @@ noProto – If True, don't try to run our protocol on the link - just be a d
self.nodes = None # FIXME
self.isConnected = threading.Event()
self.noProto = noProto
+ self.localNode = Node(self, -1) # We fixup nodenum later
self.myInfo = None # We don't have device info yet
self.responseHandlers = {} # A map from request ID to the handler
self.failure = None # If we've encountered a fatal exception it will be kept here
@@ -1376,6 +1495,25 @@ noProto – If True, don't try to run our protocol on the link - just be a d
logging.error(f'Traceback: {traceback}')
self.close()
+ def showInfo(self):
+ """Show human readable summary about this object"""
+ print(self.myInfo)
+ print("Nodes in mesh:")
+ for n in self.nodes.values():
+ print(stripnl(n))
+
+ def getNode(self, nodeId):
+ """Return a node object which contains device settings and channel info"""
+ if nodeId == LOCAL_ADDR:
+ return self.localNode
+ else:
+ logging.info("Requesting configuration from remote node (this could take a while)")
+ n = Node(self, nodeId)
+ n.requestConfig()
+ if not n.waitForConfig(maxsecs = 60):
+ raise Exception("Timed out waiting for node config")
+ return n
+
def sendText(self, text: AnyStr,
destinationId=BROADCAST_ADDR,
wantAck=False,
@@ -1426,7 +1564,7 @@ noProto – If True, don't try to run our protocol on the link - just be a d
if len(data) > mesh_pb2.Constants.DATA_PAYLOAD_LEN:
raise Exception("Data payload too big")
- if portNum == portnums_pb2.PortNum.UNKNOWN_APP: # we are now more strict wrt port numbers
+ if portNum == portnums_pb2.PortNum.UNKNOWN_APP: # we are now more strict wrt port numbers
raise Exception("A non-zero port number must be specified")
meshPacket = mesh_pb2.MeshPacket()
@@ -1434,7 +1572,8 @@ noProto – If True, don't try to run our protocol on the link - just be a d
meshPacket.decoded.portnum = portNum
meshPacket.decoded.want_response = wantResponse
- p = self._sendPacket(meshPacket, destinationId, wantAck=wantAck, hopLimit=hopLimit)
+ p = self._sendPacket(meshPacket, destinationId,
+ wantAck=wantAck, hopLimit=hopLimit)
if onResponse is not None:
self._addResponseHandler(p.id, onResponse)
return p
@@ -1494,8 +1633,15 @@ noProto – If True, don't try to run our protocol on the link - just be a d
nodeNum = destinationId
elif destinationId == BROADCAST_ADDR:
nodeNum = BROADCAST_NUM
+ elif destinationId == LOCAL_ADDR:
+ nodeNum = self.myInfo.my_node_num
+ elif destinationId.startswith("!"): # A simple hex style nodeid - we can parse this without needing the DB
+ nodeNum = int(destinationId[1:], 16)
else:
- nodeNum = self.nodes[destinationId]['num']
+ node = self.nodes.get(destinationId)
+ if not node:
+ raise Exception(f"NodeId {destinationId} not found in DB")
+ nodeNum = node['num']
meshPacket.to = nodeNum
meshPacket.want_ack = wantAck
@@ -1511,37 +1657,11 @@ noProto – If True, don't try to run our protocol on the link - just be a d
self._sendToRadio(toRadio)
return meshPacket
- def waitForConfig(self, sleep=.1, maxsecs=20, attrs=('myInfo', 'nodes', 'radioConfig', 'channels')):
+ def waitForConfig(self):
"""Block until radio config is received. Returns True if config has been received."""
- for _ in range(int(maxsecs/sleep)):
- if all(map(lambda a: getattr(self, a, None), attrs)):
- return True
- time.sleep(sleep)
- return False
-
- def writeConfig(self):
- """Write the current (edited) radioConfig to the device"""
- if self.radioConfig == None:
- raise Exception("No RadioConfig has been read")
-
- p = admin_pb2.AdminMessage()
- p.set_radio.CopyFrom(self.radioConfig)
-
- self.sendData(p, self.myInfo.my_node_num,
- portNum=portnums_pb2.PortNum.ADMIN_APP,
- wantAck=True)
- logging.debug("Wrote config")
-
- def writeChannel(self, channelIndex):
- """Write the current (edited) channel to the device"""
-
- p = admin_pb2.AdminMessage()
- p.set_channel.CopyFrom(self.channels[channelIndex])
-
- self.sendData(p, self.myInfo.my_node_num,
- portNum=portnums_pb2.PortNum.ADMIN_APP,
- wantAck=True)
- logging.debug("Wrote channel {channelIndex}")
+ success = waitForSet(self, attrs=('myInfo', 'nodes')) and self.localNode.waitForConfig()
+ if not success:
+ raise Exception("Timed out waiting for interface config")
def getMyNodeInfo(self):
if self.myInfo is None:
@@ -1566,82 +1686,6 @@ noProto – If True, don't try to run our protocol on the link - just be a d
return user.get('shortName', None)
return None
- def setOwner(self, long_name, short_name=None):
- """Set device owner name"""
- nChars = 3
- minChars = 2
- if long_name is not None:
- long_name = long_name.strip()
- if short_name is None:
- words = long_name.split()
- if len(long_name) <= nChars:
- short_name = long_name
- elif len(words) >= minChars:
- short_name = ''.join(map(lambda word: word[0], words))
- else:
- trans = str.maketrans(dict.fromkeys('aeiouAEIOU'))
- short_name = long_name[0] + long_name[1:].translate(trans)
- if len(short_name) < nChars:
- short_name = long_name[:nChars]
-
- p = admin_pb2.AdminMessage()
-
- if long_name is not None:
- p.set_owner.long_name = long_name
- if short_name is not None:
- short_name = short_name.strip()
- if len(short_name) > nChars:
- short_name = short_name[:nChars]
- p.set_owner.short_name = short_name
-
- return self.sendData(p, self.myInfo.my_node_num,
- portNum=portnums_pb2.PortNum.ADMIN_APP,
- wantAck=True)
-
- @property
- def channelURL(self):
- """The sharable URL that describes the current channel
- """
- # Only keep the primary/secondary channels, assume primary is first
- channelSet = apponly_pb2.ChannelSet()
- for c in self.channels:
- if c.role != channel_pb2.Channel.Role.DISABLED:
- channelSet.settings.append(c.settings)
- bytes = channelSet.SerializeToString()
- s = base64.urlsafe_b64encode(bytes).decode('ascii')
- return f"https://www.meshtastic.org/d/#{s}".replace("=", "")
-
- def setURL(self, url):
- """Set mesh network URL"""
- if self.radioConfig == None:
- raise Exception("No RadioConfig has been read")
-
- # URLs are of the form https://www.meshtastic.org/d/#{base64_channel_set}
- # Split on '/#' to find the base64 encoded channel settings
- splitURL = url.split("/#")
- b64 = splitURL[-1]
-
- # We normally strip padding to make for a shorter URL, but the python parser doesn't like
- # that. So add back any missing padding
- # per https://stackoverflow.com/a/9807138
- missing_padding = len(b64) % 4
- if missing_padding:
- b64 += '='* (4 - missing_padding)
-
- decodedURL = base64.urlsafe_b64decode(b64)
- channelSet = apponly_pb2.ChannelSet()
- channelSet.ParseFromString(decodedURL)
-
- i = 0
- for chs in channelSet.settings:
- ch = channel_pb2.Channel()
- ch.role = channel_pb2.Channel.Role.PRIMARY if i == 0 else channel_pb2.Channel.Role.SECONDARY
- ch.index = i
- ch.settings.CopyFrom(chs)
- self.channels[ch.index] = ch
- self.writeChannel(ch.index)
- i = i + 1
-
def _waitConnected(self):
"""Block until the initial node db download is complete, or timeout
and raise an exception"""
@@ -1663,27 +1707,29 @@ noProto – If True, don't try to run our protocol on the link - just be a d
def _disconnected(self):
"""Called by subclasses to tell clients this interface has disconnected"""
self.isConnected.clear()
- catchAndIgnore("disconnection publish", lambda: pub.sendMessage(
+ publishingThread.queueWork(lambda: pub.sendMessage(
"meshtastic.connection.lost", interface=self))
def _connected(self):
"""Called by this class to tell clients we are now fully connected to a node
"""
- self.isConnected.set()
- catchAndIgnore("connection publish", lambda: pub.sendMessage(
- "meshtastic.connection.established", interface=self))
+ # (because I'm lazy) _connected might be called when remote Node
+ # objects complete their config reads, don't generate redundant isConnected
+ # for the local interface
+ if not self.isConnected.is_set():
+ self.isConnected.set()
+ publishingThread.queueWork(lambda: pub.sendMessage(
+ "meshtastic.connection.established", interface=self))
def _startConfig(self):
"""Start device packets flowing"""
self.myInfo = None
self.nodes = {} # nodes keyed by ID
self.nodesByNum = {} # nodes keyed by nodenum
- self.radioConfig = None
- self.channels = None
- self.partialChannels = [] # We keep our channels in a temp array until finished
startConfig = mesh_pb2.ToRadio()
- startConfig.want_config_id = MY_CONFIG_ID # we don't use this value
+ self.configId = random.randint(0, 0xffffffff)
+ startConfig.want_config_id = self.configId
self._sendToRadio(startConfig)
def _sendToRadio(self, toRadio):
@@ -1703,59 +1749,7 @@ noProto – If True, don't try to run our protocol on the link - just be a d
"""
Done with initial config messages, now send regular MeshPackets to ask for settings and channels
"""
- self._requestSettings()
- self._requestChannel(0)
-
- def _requestSettings(self):
- """
- Done with initial config messages, now send regular MeshPackets to ask for settings
- """
- p = admin_pb2.AdminMessage()
- p.get_radio_request = True
-
- def onResponse(p):
- """A closure to handle the response packet"""
- self.radioConfig = p["decoded"]["admin"]["raw"].get_radio_response
-
- return self.sendData(p, self.myInfo.my_node_num,
- portNum=portnums_pb2.PortNum.ADMIN_APP,
- wantAck=True,
- wantResponse=True,
- onResponse=onResponse)
-
- def _requestChannel(self, channelNum: int):
- """
- Done with initial config messages, now send regular MeshPackets to ask for settings
- """
- p = admin_pb2.AdminMessage()
- p.get_channel_request = channelNum + 1
- logging.debug(f"Requesting channel {channelNum}")
-
- def onResponse(p):
- """A closure to handle the response packet"""
- c = p["decoded"]["admin"]["raw"].get_channel_response
- self.partialChannels.append(c)
- logging.debug(f"Received channel {stripnl(c)}")
- index = c.index
-
- # for stress testing, we can always download all channels
- fastChannelDownload = True
-
- # Once we see a response that has NO settings, assume we are at the end of channels and stop fetching
- quitEarly = (c.role == channel_pb2.Channel.Role.DISABLED) and fastChannelDownload
-
- if quitEarly or index >= self.myInfo.max_channels - 1:
- self.channels = self.partialChannels
- # FIXME, the following should only be called after we have settings and channels
- self._connected() # Tell everone else we are ready to go
- else:
- self._requestChannel(index + 1)
-
- return self.sendData(p, self.myInfo.my_node_num,
- portNum=portnums_pb2.PortNum.ADMIN_APP,
- wantAck=True,
- wantResponse=True,
- onResponse=onResponse)
+ self.localNode.requestConfig()
def _handleFromRadio(self, fromRadioBytes):
"""
@@ -1765,8 +1759,10 @@ noProto – If True, don't try to run our protocol on the link - just be a d
fromRadio = mesh_pb2.FromRadio()
fromRadio.ParseFromString(fromRadioBytes)
asDict = google.protobuf.json_format.MessageToDict(fromRadio)
+ # logging.debug(f"Received from radio: {fromRadio}")
if fromRadio.HasField("my_info"):
self.myInfo = fromRadio.my_info
+ self.localNode.nodeNum = self.myInfo.my_node_num
logging.debug(f"Received myinfo: {stripnl(fromRadio.my_info)}")
failmsg = None
@@ -1795,10 +1791,11 @@ noProto – If True, don't try to run our protocol on the link - just be a d
self.nodesByNum[node["num"]] = node
if "user" in node: # Some nodes might not have user/ids assigned yet
self.nodes[node["user"]["id"]] = node
- pub.sendMessage("meshtastic.node.updated",
- node=node, interface=self)
- elif fromRadio.config_complete_id == MY_CONFIG_ID:
+ publishingThread.queueWork(lambda: pub.sendMessage("meshtastic.node.updated",
+ node=node, interface=self))
+ elif fromRadio.config_complete_id == self.configId:
# we ignore the config_complete_id, it is unneeded for our stream API fromRadio.config_complete_id
+ logging.debug(f"Config complete ID {self.configId}")
self._handleConfigComplete()
elif fromRadio.HasField("packet"):
self._handlePacketFromRadio(fromRadio.packet)
@@ -1836,7 +1833,7 @@ noProto – If True, don't try to run our protocol on the link - just be a d
try:
return self.nodesByNum[num]["user"]["id"]
except:
- logging.warn("Node not found for fromId")
+ logging.debug(f"Node {num} not found for fromId")
return None
def _getOrCreateByNum(self, nodeNum):
@@ -1871,14 +1868,21 @@ noProto – If True, don't try to run our protocol on the link - just be a d
# from might be missing if the nodenum was zero.
if not "from" in asDict:
asDict["from"] = 0
- logging.error(f"Device returned a packet we sent, ignoring: {stripnl(asDict)}")
+ logging.error(
+ f"Device returned a packet we sent, ignoring: {stripnl(asDict)}")
return
if not "to" in asDict:
asDict["to"] = 0
# /add fromId and toId fields based on the node ID
- asDict["fromId"] = self._nodeNumToId(asDict["from"])
- asDict["toId"] = self._nodeNumToId(asDict["to"])
+ try:
+ asDict["fromId"] = self._nodeNumToId(asDict["from"])
+ except Exception as ex:
+ logging.warn(f"Not populating fromId {ex}")
+ try:
+ asDict["toId"] = self._nodeNumToId(asDict["to"])
+ except Exception as ex:
+ logging.warn(f"Not populating toId {ex}")
# We could provide our objects as DotMaps - which work with . notation or as dictionaries
# asObj = DotMap(asDict)
@@ -1935,7 +1939,7 @@ noProto – If True, don't try to run our protocol on the link - just be a d
handler.callback(asDict)
logging.debug(f"Publishing {topic}: packet={stripnl(asDict)} ")
- catchAndIgnore(f"publishing {topic}", lambda: pub.sendMessage(
+ publishingThread.queueWork(lambda: pub.sendMessage(
topic, packet=asDict, interface=self))
Subclasses
@@ -1943,30 +1947,6 @@ noProto – If True, don't try to run our protocol on the link - just be a d
BLEInterface
StreamInterface
-Instance variables
-
-var channelURL
--
-
The sharable URL that describes the current channel
-
-
-Expand source code
-
-@property
-def channelURL(self):
- """The sharable URL that describes the current channel
- """
- # Only keep the primary/secondary channels, assume primary is first
- channelSet = apponly_pb2.ChannelSet()
- for c in self.channels:
- if c.role != channel_pb2.Channel.Role.DISABLED:
- channelSet.settings.append(c.settings)
- bytes = channelSet.SerializeToString()
- s = base64.urlsafe_b64encode(bytes).decode('ascii')
- return f"https://www.meshtastic.org/d/#{s}".replace("=", "")
-
-
-
Methods
@@ -2016,6 +1996,28 @@ def channelURL(self):
return None
+
+def getNode(self, nodeId)
+
+-
+
Return a node object which contains device settings and channel info
+
+
+Expand source code
+
+def getNode(self, nodeId):
+ """Return a node object which contains device settings and channel info"""
+ if nodeId == LOCAL_ADDR:
+ return self.localNode
+ else:
+ logging.info("Requesting configuration from remote node (this could take a while)")
+ n = Node(self, nodeId)
+ n.requestConfig()
+ if not n.waitForConfig(maxsecs = 60):
+ raise Exception("Timed out waiting for node config")
+ return n
+
+
def getShortName(self)
@@ -2073,7 +2075,7 @@ onResponse – A closure of the form funct(packet), that will be called when
if len(data) > mesh_pb2.Constants.DATA_PAYLOAD_LEN:
raise Exception("Data payload too big")
- if portNum == portnums_pb2.PortNum.UNKNOWN_APP: # we are now more strict wrt port numbers
+ if portNum == portnums_pb2.PortNum.UNKNOWN_APP: # we are now more strict wrt port numbers
raise Exception("A non-zero port number must be specified")
meshPacket = mesh_pb2.MeshPacket()
@@ -2081,7 +2083,8 @@ onResponse – A closure of the form funct(packet), that will be called when
meshPacket.decoded.portnum = portNum
meshPacket.decoded.want_response = wantResponse
- p = self._sendPacket(meshPacket, destinationId, wantAck=wantAck, hopLimit=hopLimit)
+ p = self._sendPacket(meshPacket, destinationId,
+ wantAck=wantAck, hopLimit=hopLimit)
if onResponse is not None:
self._addResponseHandler(p.id, onResponse)
return p
@@ -2175,7 +2178,292 @@ wantResponse – True if you want the service on the other side to send an a
onResponse=onResponse)
-
+
+def showInfo(self)
+
+-
+
Show human readable summary about this object
+
+
+Expand source code
+
+def showInfo(self):
+ """Show human readable summary about this object"""
+ print(self.myInfo)
+ print("Nodes in mesh:")
+ for n in self.nodes.values():
+ print(stripnl(n))
+
+
+
+def waitForConfig(self)
+
+-
+
Block until radio config is received. Returns True if config has been received.
+
+
+Expand source code
+
+def waitForConfig(self):
+ """Block until radio config is received. Returns True if config has been received."""
+ success = waitForSet(self, attrs=('myInfo', 'nodes')) and self.localNode.waitForConfig()
+ if not success:
+ raise Exception("Timed out waiting for interface config")
+
+
+
+
+
+class Node
+(iface, nodeNum)
+
+
+A model of a (local or remote) node in the mesh
+
Includes methods for radioConfig and channels
+
Constructor
+
+
+Expand source code
+
+class Node:
+ """A model of a (local or remote) node in the mesh
+
+ Includes methods for radioConfig and channels
+ """
+
+ def __init__(self, iface, nodeNum):
+ """Constructor"""
+ self.iface = iface
+ self.nodeNum = nodeNum
+ self.radioConfig = None
+ self.channels = None
+
+ def showInfo(self):
+ """Show human readable description of our node"""
+ print(self.radioConfig)
+ print("Channels:")
+ for c in self.channels:
+ if c.role != channel_pb2.Channel.Role.DISABLED:
+ cStr = MessageToJson(c.settings).replace("\n", "")
+ print(f" {channel_pb2.Channel.Role.Name(c.role)} {cStr}")
+ print(f"\nChannel URL {self.channelURL}")
+
+ def requestConfig(self):
+ """
+ Send regular MeshPackets to ask for settings and channels
+ """
+ self.radioConfig = None
+ self.channels = None
+ self.partialChannels = [] # We keep our channels in a temp array until finished
+
+ self._requestSettings()
+
+ def waitForConfig(self, maxsecs=20):
+ """Block until radio config is received. Returns True if config has been received."""
+ return waitForSet(self, attrs=('radioConfig', 'channels'), maxsecs=maxsecs)
+
+ def writeConfig(self):
+ """Write the current (edited) radioConfig to the device"""
+ if self.radioConfig == None:
+ raise Exception("No RadioConfig has been read")
+
+ p = admin_pb2.AdminMessage()
+ p.set_radio.CopyFrom(self.radioConfig)
+
+ self._sendAdmin(p)
+ logging.debug("Wrote config")
+
+ def writeChannel(self, channelIndex):
+ """Write the current (edited) channel to the device"""
+
+ p = admin_pb2.AdminMessage()
+ p.set_channel.CopyFrom(self.channels[channelIndex])
+
+ self._sendAdmin(p)
+ logging.debug("Wrote channel {channelIndex}")
+
+ def setOwner(self, long_name, short_name=None):
+ """Set device owner name"""
+ nChars = 3
+ minChars = 2
+ if long_name is not None:
+ long_name = long_name.strip()
+ if short_name is None:
+ words = long_name.split()
+ if len(long_name) <= nChars:
+ short_name = long_name
+ elif len(words) >= minChars:
+ short_name = ''.join(map(lambda word: word[0], words))
+ else:
+ trans = str.maketrans(dict.fromkeys('aeiouAEIOU'))
+ short_name = long_name[0] + long_name[1:].translate(trans)
+ if len(short_name) < nChars:
+ short_name = long_name[:nChars]
+
+ p = admin_pb2.AdminMessage()
+
+ if long_name is not None:
+ p.set_owner.long_name = long_name
+ if short_name is not None:
+ short_name = short_name.strip()
+ if len(short_name) > nChars:
+ short_name = short_name[:nChars]
+ p.set_owner.short_name = short_name
+
+ return self._sendAdmin(p)
+
+ @property
+ def channelURL(self):
+ """The sharable URL that describes the current channel
+ """
+ # Only keep the primary/secondary channels, assume primary is first
+ channelSet = apponly_pb2.ChannelSet()
+ for c in self.channels:
+ if c.role != channel_pb2.Channel.Role.DISABLED:
+ channelSet.settings.append(c.settings)
+ bytes = channelSet.SerializeToString()
+ s = base64.urlsafe_b64encode(bytes).decode('ascii')
+ return f"https://www.meshtastic.org/d/#{s}".replace("=", "")
+
+ def setURL(self, url):
+ """Set mesh network URL"""
+ if self.radioConfig == None:
+ raise Exception("No RadioConfig has been read")
+
+ # URLs are of the form https://www.meshtastic.org/d/#{base64_channel_set}
+ # Split on '/#' to find the base64 encoded channel settings
+ splitURL = url.split("/#")
+ b64 = splitURL[-1]
+
+ # We normally strip padding to make for a shorter URL, but the python parser doesn't like
+ # that. So add back any missing padding
+ # per https://stackoverflow.com/a/9807138
+ missing_padding = len(b64) % 4
+ if missing_padding:
+ b64 += '=' * (4 - missing_padding)
+
+ decodedURL = base64.urlsafe_b64decode(b64)
+ channelSet = apponly_pb2.ChannelSet()
+ channelSet.ParseFromString(decodedURL)
+
+ i = 0
+ for chs in channelSet.settings:
+ ch = channel_pb2.Channel()
+ ch.role = channel_pb2.Channel.Role.PRIMARY if i == 0 else channel_pb2.Channel.Role.SECONDARY
+ ch.index = i
+ ch.settings.CopyFrom(chs)
+ self.channels[ch.index] = ch
+ self.writeChannel(ch.index)
+ i = i + 1
+
+ def _requestSettings(self):
+ """
+ Done with initial config messages, now send regular MeshPackets to ask for settings
+ """
+ p = admin_pb2.AdminMessage()
+ p.get_radio_request = True
+
+ def onResponse(p):
+ """A closure to handle the response packet"""
+ self.radioConfig = p["decoded"]["admin"]["raw"].get_radio_response
+ logging.debug("Received radio config, now fetching channels...")
+ self._requestChannel(0) # now start fetching channels
+
+ return self._sendAdmin(p,
+ wantResponse=True,
+ onResponse=onResponse)
+
+ def _requestChannel(self, channelNum: int):
+ """
+ Done with initial config messages, now send regular MeshPackets to ask for settings
+ """
+ p = admin_pb2.AdminMessage()
+ p.get_channel_request = channelNum + 1
+ logging.debug(f"Requesting channel {channelNum}")
+
+ def onResponse(p):
+ """A closure to handle the response packet"""
+ c = p["decoded"]["admin"]["raw"].get_channel_response
+ self.partialChannels.append(c)
+ logging.debug(f"Received channel {stripnl(c)}")
+ index = c.index
+
+ # for stress testing, we can always download all channels
+ fastChannelDownload = True
+
+ # Once we see a response that has NO settings, assume we are at the end of channels and stop fetching
+ quitEarly = (
+ c.role == channel_pb2.Channel.Role.DISABLED) and fastChannelDownload
+
+ if quitEarly or index >= self.iface.myInfo.max_channels - 1:
+ logging.debug("Finished downloading channels")
+ self.channels = self.partialChannels
+ # FIXME, the following should only be called after we have settings and channels
+ self.iface._connected() # Tell everone else we are ready to go
+ else:
+ self._requestChannel(index + 1)
+
+ return self._sendAdmin(p,
+ wantResponse=True,
+ onResponse=onResponse)
+
+ def _sendAdmin(self, p: admin_pb2.AdminMessage, wantResponse=False,
+ onResponse=None):
+ """Send an admin message to the specified node (or the local node if destNodeNum is zero)"""
+
+ return self.iface.sendData(p, self.nodeNum,
+ portNum=portnums_pb2.PortNum.ADMIN_APP,
+ wantAck=True,
+ wantResponse=wantResponse,
+ onResponse=onResponse)
+
+Instance variables
+
+var channelURL
+-
+
The sharable URL that describes the current channel
+
+
+Expand source code
+
+@property
+def channelURL(self):
+ """The sharable URL that describes the current channel
+ """
+ # Only keep the primary/secondary channels, assume primary is first
+ channelSet = apponly_pb2.ChannelSet()
+ for c in self.channels:
+ if c.role != channel_pb2.Channel.Role.DISABLED:
+ channelSet.settings.append(c.settings)
+ bytes = channelSet.SerializeToString()
+ s = base64.urlsafe_b64encode(bytes).decode('ascii')
+ return f"https://www.meshtastic.org/d/#{s}".replace("=", "")
+
+
+
+Methods
+
+
+def requestConfig(self)
+
+-
+
Send regular MeshPackets to ask for settings and channels
+
+
+Expand source code
+
+def requestConfig(self):
+ """
+ Send regular MeshPackets to ask for settings and channels
+ """
+ self.radioConfig = None
+ self.channels = None
+ self.partialChannels = [] # We keep our channels in a temp array until finished
+
+ self._requestSettings()
+
+
+
def setOwner(self, long_name, short_name=None)
-
@@ -2212,12 +2500,10 @@ wantResponse – True if you want the service on the other side to send an a
short_name = short_name[:nChars]
p.set_owner.short_name = short_name
- return self.sendData(p, self.myInfo.my_node_num,
- portNum=portnums_pb2.PortNum.ADMIN_APP,
- wantAck=True)
+ return self._sendAdmin(p)
-
+
def setURL(self, url)
-
@@ -2241,7 +2527,7 @@ wantResponse – True if you want the service on the other side to send an a
# per https://stackoverflow.com/a/9807138
missing_padding = len(b64) % 4
if missing_padding:
- b64 += '='* (4 - missing_padding)
+ b64 += '=' * (4 - missing_padding)
decodedURL = base64.urlsafe_b64decode(b64)
channelSet = apponly_pb2.ChannelSet()
@@ -2258,8 +2544,28 @@ wantResponse – True if you want the service on the other side to send an a
i = i + 1
-
-def waitForConfig(self, sleep=0.1, maxsecs=20, attrs=('myInfo', 'nodes', 'radioConfig', 'channels'))
+
+def showInfo(self)
+
+
+Show human readable description of our node
+
+
+Expand source code
+
+def showInfo(self):
+ """Show human readable description of our node"""
+ print(self.radioConfig)
+ print("Channels:")
+ for c in self.channels:
+ if c.role != channel_pb2.Channel.Role.DISABLED:
+ cStr = MessageToJson(c.settings).replace("\n", "")
+ print(f" {channel_pb2.Channel.Role.Name(c.role)} {cStr}")
+ print(f"\nChannel URL {self.channelURL}")
+
+
+
+def waitForConfig(self, maxsecs=20)
Block until radio config is received. Returns True if config has been received.
@@ -2267,16 +2573,12 @@ wantResponse – True if you want the service on the other side to send an a
Expand source code
-def waitForConfig(self, sleep=.1, maxsecs=20, attrs=('myInfo', 'nodes', 'radioConfig', 'channels')):
+def waitForConfig(self, maxsecs=20):
"""Block until radio config is received. Returns True if config has been received."""
- for _ in range(int(maxsecs/sleep)):
- if all(map(lambda a: getattr(self, a, None), attrs)):
- return True
- time.sleep(sleep)
- return False
+ return waitForSet(self, attrs=('radioConfig', 'channels'), maxsecs=maxsecs)
-
+
def writeChannel(self, channelIndex)
@@ -2291,13 +2593,11 @@ wantResponse – True if you want the service on the other side to send an a
p = admin_pb2.AdminMessage()
p.set_channel.CopyFrom(self.channels[channelIndex])
- self.sendData(p, self.myInfo.my_node_num,
- portNum=portnums_pb2.PortNum.ADMIN_APP,
- wantAck=True)
+ self._sendAdmin(p)
logging.debug("Wrote channel {channelIndex}")
-
+
def writeConfig(self)
@@ -2314,9 +2614,7 @@ wantResponse – True if you want the service on the other side to send an a
p = admin_pb2.AdminMessage()
p.set_radio.CopyFrom(self.radioConfig)
- self.sendData(p, self.myInfo.my_node_num,
- portNum=portnums_pb2.PortNum.ADMIN_APP,
- wantAck=True)
+ self._sendAdmin(p)
logging.debug("Wrote config")
@@ -2415,17 +2713,14 @@ debugOut {stream} – If a stream is provided, any debug serial output from
@@ -2480,7 +2775,8 @@ debugOut {stream} – If a stream is provided, any debug serial output from
# Start the reader thread after superclass constructor completes init
if connectNow:
self.connect()
- self.waitForConfig()
+ if not noProto:
+ self.waitForConfig()
def connect(self):
"""Connect to our radio
@@ -2540,8 +2836,8 @@ debugOut {stream} – If a stream is provided, any debug serial output from
# logging.debug("reading character")
b = self._readBytes(1)
# logging.debug("In reader loop")
+ # logging.debug(f"read returned {b}")
if len(b) > 0:
- # logging.debug(f"read returned {b}")
c = b[0]
ptr = len(self._rxBuf)
@@ -2655,15 +2951,12 @@ start the reading thread later.
@@ -2732,17 +3025,14 @@ hostname {string} – Hostname/IP address of the device to connect to
StreamInterface:
@@ -2780,7 +3070,14 @@ hostname {string} – Hostname/IP address of the device to connect to
+
+
+
@@ -2802,19 +3099,29 @@ hostname {string} – Hostname/IP address of the device to connect to
+
+
+
+
diff --git a/docs/meshtastic/mesh_pb2.html b/docs/meshtastic/mesh_pb2.html
index 7e4b856..5347a59 100644
--- a/docs/meshtastic/mesh_pb2.html
+++ b/docs/meshtastic/mesh_pb2.html
@@ -50,7 +50,7 @@ DESCRIPTOR = _descriptor.FileDescriptor(
syntax='proto3',
serialized_options=b'\n\023com.geeksville.meshB\nMeshProtosH\003',
create_key=_descriptor._internal_create_key,
- serialized_pb=b'\n\nmesh.proto\x1a\x0eportnums.proto\"v\n\x08Position\x12\x12\n\nlatitude_i\x18\x01 \x01(\x0f\x12\x13\n\x0blongitude_i\x18\x02 \x01(\x0f\x12\x10\n\x08\x61ltitude\x18\x03 \x01(\x05\x12\x15\n\rbattery_level\x18\x04 \x01(\x05\x12\x0c\n\x04time\x18\t \x01(\x07J\x04\x08\x07\x10\x08J\x04\x08\x08\x10\t\"J\n\x04User\x12\n\n\x02id\x18\x01 \x01(\t\x12\x11\n\tlong_name\x18\x02 \x01(\t\x12\x12\n\nshort_name\x18\x03 \x01(\t\x12\x0f\n\x07macaddr\x18\x04 \x01(\x0c\"\x1f\n\x0eRouteDiscovery\x12\r\n\x05route\x18\x02 \x03(\x07\"\x8e\x02\n\x07Routing\x12(\n\rroute_request\x18\x01 \x01(\x0b\x32\x0f.RouteDiscoveryH\x00\x12&\n\x0broute_reply\x18\x02 \x01(\x0b\x32\x0f.RouteDiscoveryH\x00\x12&\n\x0c\x65rror_reason\x18\x03 \x01(\x0e\x32\x0e.Routing.ErrorH\x00\"~\n\x05\x45rror\x12\x08\n\x04NONE\x10\x00\x12\x0c\n\x08NO_ROUTE\x10\x01\x12\x0b\n\x07GOT_NAK\x10\x02\x12\x0b\n\x07TIMEOUT\x10\x03\x12\x10\n\x0cNO_INTERFACE\x10\x04\x12\x12\n\x0eMAX_RETRANSMIT\x10\x05\x12\x0e\n\nNO_CHANNEL\x10\x06\x12\r\n\tTOO_LARGE\x10\x07\x42\t\n\x07variant\"{\n\x04\x44\x61ta\x12\x19\n\x07portnum\x18\x01 \x01(\x0e\x32\x08.PortNum\x12\x0f\n\x07payload\x18\x02 \x01(\x0c\x12\x15\n\rwant_response\x18\x03 \x01(\x08\x12\x0c\n\x04\x64\x65st\x18\x04 \x01(\x07\x12\x0e\n\x06source\x18\x05 \x01(\x07\x12\x12\n\nrequest_id\x18\x06 \x01(\x07\"\xcf\x02\n\nMeshPacket\x12\x0c\n\x04\x66rom\x18\x01 \x01(\x07\x12\n\n\x02to\x18\x02 \x01(\x07\x12\x0f\n\x07\x63hannel\x18\x03 \x01(\r\x12\x18\n\x07\x64\x65\x63oded\x18\x04 \x01(\x0b\x32\x05.DataH\x00\x12\x13\n\tencrypted\x18\x05 \x01(\x0cH\x00\x12\n\n\x02id\x18\x06 \x01(\x07\x12\x0f\n\x07rx_time\x18\x07 \x01(\x07\x12\x0e\n\x06rx_snr\x18\x08 \x01(\x02\x12\x11\n\thop_limit\x18\n \x01(\r\x12\x10\n\x08want_ack\x18\x0b \x01(\x08\x12&\n\x08priority\x18\x0c \x01(\x0e\x32\x14.MeshPacket.Priority\"[\n\x08Priority\x12\t\n\x05UNSET\x10\x00\x12\x07\n\x03MIN\x10\x01\x12\x0e\n\nBACKGROUND\x10\n\x12\x0b\n\x07\x44\x45\x46\x41ULT\x10@\x12\x0c\n\x08RELIABLE\x10\x46\x12\x07\n\x03\x41\x43K\x10x\x12\x07\n\x03MAX\x10\x7f\x42\x10\n\x0epayloadVariant\"h\n\x08NodeInfo\x12\x0b\n\x03num\x18\x01 \x01(\r\x12\x13\n\x04user\x18\x02 \x01(\x0b\x32\x05.User\x12\x1b\n\x08position\x18\x03 \x01(\x0b\x32\t.Position\x12\x0b\n\x03snr\x18\x07 \x01(\x02\x12\x10\n\x08next_hop\x18\x05 \x01(\r\"\xa6\x02\n\nMyNodeInfo\x12\x13\n\x0bmy_node_num\x18\x01 \x01(\r\x12\x0f\n\x07has_gps\x18\x02 \x01(\x08\x12\x11\n\tnum_bands\x18\x03 \x01(\r\x12\x14\n\x0cmax_channels\x18\x0f \x01(\r\x12\x12\n\x06region\x18\x04 \x01(\tB\x02\x18\x01\x12\x10\n\x08hw_model\x18\x05 \x01(\t\x12\x18\n\x10\x66irmware_version\x18\x06 \x01(\t\x12&\n\nerror_code\x18\x07 \x01(\x0e\x32\x12.CriticalErrorCode\x12\x15\n\rerror_address\x18\x08 \x01(\r\x12\x13\n\x0b\x65rror_count\x18\t \x01(\r\x12\x1c\n\x14message_timeout_msec\x18\r \x01(\r\x12\x17\n\x0fmin_app_version\x18\x0e \x01(\r\"\xb5\x01\n\tLogRecord\x12\x0f\n\x07message\x18\x01 \x01(\t\x12\x0c\n\x04time\x18\x02 \x01(\x07\x12\x0e\n\x06source\x18\x03 \x01(\t\x12\x1f\n\x05level\x18\x04 \x01(\x0e\x32\x10.LogRecord.Level\"X\n\x05Level\x12\t\n\x05UNSET\x10\x00\x12\x0c\n\x08\x43RITICAL\x10\x32\x12\t\n\x05\x45RROR\x10(\x12\x0b\n\x07WARNING\x10\x1e\x12\x08\n\x04INFO\x10\x14\x12\t\n\x05\x44\x45\x42UG\x10\n\x12\t\n\x05TRACE\x10\x05\"\xe9\x01\n\tFromRadio\x12\x0b\n\x03num\x18\x01 \x01(\r\x12\x1d\n\x06packet\x18\x0b \x01(\x0b\x32\x0b.MeshPacketH\x00\x12\x1e\n\x07my_info\x18\x03 \x01(\x0b\x32\x0b.MyNodeInfoH\x00\x12\x1e\n\tnode_info\x18\x04 \x01(\x0b\x32\t.NodeInfoH\x00\x12 \n\nlog_record\x18\x07 \x01(\x0b\x32\n.LogRecordH\x00\x12\x1c\n\x12\x63onfig_complete_id\x18\x08 \x01(\rH\x00\x12\x12\n\x08rebooted\x18\t \x01(\x08H\x00\x42\x10\n\x0epayloadVariantJ\x04\x08\x02\x10\x03J\x04\x08\x06\x10\x07\"l\n\x07ToRadio\x12\x1d\n\x06packet\x18\x02 \x01(\x0b\x32\x0b.MeshPacketH\x00\x12\x18\n\x0ewant_config_id\x18\x64 \x01(\rH\x00\x42\x10\n\x0epayloadVariantJ\x04\x08\x01\x10\x02J\x04\x08\x65\x10\x66J\x04\x08\x66\x10gJ\x04\x08g\x10h*.\n\tConstants\x12\n\n\x06Unused\x10\x00\x12\x15\n\x10\x44\x41TA_PAYLOAD_LEN\x10\xf0\x01*\xaf\x01\n\x11\x43riticalErrorCode\x12\x08\n\x04None\x10\x00\x12\x0e\n\nTxWatchdog\x10\x01\x12\x12\n\x0eSleepEnterWait\x10\x02\x12\x0b\n\x07NoRadio\x10\x03\x12\x0f\n\x0bUnspecified\x10\x04\x12\x13\n\x0fUBloxInitFailed\x10\x05\x12\x0c\n\x08NoAXP192\x10\x06\x12\x17\n\x13InvalidRadioSetting\x10\x07\x12\x12\n\x0eTransmitFailed\x10\x08\x42#\n\x13\x63om.geeksville.meshB\nMeshProtosH\x03\x62\x06proto3'
+ serialized_pb=b'\n\nmesh.proto\x1a\x0eportnums.proto\"v\n\x08Position\x12\x12\n\nlatitude_i\x18\x01 \x01(\x0f\x12\x13\n\x0blongitude_i\x18\x02 \x01(\x0f\x12\x10\n\x08\x61ltitude\x18\x03 \x01(\x05\x12\x15\n\rbattery_level\x18\x04 \x01(\x05\x12\x0c\n\x04time\x18\t \x01(\x07J\x04\x08\x07\x10\x08J\x04\x08\x08\x10\t\"J\n\x04User\x12\n\n\x02id\x18\x01 \x01(\t\x12\x11\n\tlong_name\x18\x02 \x01(\t\x12\x12\n\nshort_name\x18\x03 \x01(\t\x12\x0f\n\x07macaddr\x18\x04 \x01(\x0c\"\x1f\n\x0eRouteDiscovery\x12\r\n\x05route\x18\x02 \x03(\x07\"\x8e\x02\n\x07Routing\x12(\n\rroute_request\x18\x01 \x01(\x0b\x32\x0f.RouteDiscoveryH\x00\x12&\n\x0broute_reply\x18\x02 \x01(\x0b\x32\x0f.RouteDiscoveryH\x00\x12&\n\x0c\x65rror_reason\x18\x03 \x01(\x0e\x32\x0e.Routing.ErrorH\x00\"~\n\x05\x45rror\x12\x08\n\x04NONE\x10\x00\x12\x0c\n\x08NO_ROUTE\x10\x01\x12\x0b\n\x07GOT_NAK\x10\x02\x12\x0b\n\x07TIMEOUT\x10\x03\x12\x10\n\x0cNO_INTERFACE\x10\x04\x12\x12\n\x0eMAX_RETRANSMIT\x10\x05\x12\x0e\n\nNO_CHANNEL\x10\x06\x12\r\n\tTOO_LARGE\x10\x07\x42\t\n\x07variant\"{\n\x04\x44\x61ta\x12\x19\n\x07portnum\x18\x01 \x01(\x0e\x32\x08.PortNum\x12\x0f\n\x07payload\x18\x02 \x01(\x0c\x12\x15\n\rwant_response\x18\x03 \x01(\x08\x12\x0c\n\x04\x64\x65st\x18\x04 \x01(\x07\x12\x0e\n\x06source\x18\x05 \x01(\x07\x12\x12\n\nrequest_id\x18\x06 \x01(\x07\"\xcf\x02\n\nMeshPacket\x12\x0c\n\x04\x66rom\x18\x01 \x01(\x07\x12\n\n\x02to\x18\x02 \x01(\x07\x12\x0f\n\x07\x63hannel\x18\x03 \x01(\r\x12\x18\n\x07\x64\x65\x63oded\x18\x04 \x01(\x0b\x32\x05.DataH\x00\x12\x13\n\tencrypted\x18\x05 \x01(\x0cH\x00\x12\n\n\x02id\x18\x06 \x01(\x07\x12\x0f\n\x07rx_time\x18\x07 \x01(\x07\x12\x0e\n\x06rx_snr\x18\x08 \x01(\x02\x12\x11\n\thop_limit\x18\n \x01(\r\x12\x10\n\x08want_ack\x18\x0b \x01(\x08\x12&\n\x08priority\x18\x0c \x01(\x0e\x32\x14.MeshPacket.Priority\"[\n\x08Priority\x12\t\n\x05UNSET\x10\x00\x12\x07\n\x03MIN\x10\x01\x12\x0e\n\nBACKGROUND\x10\n\x12\x0b\n\x07\x44\x45\x46\x41ULT\x10@\x12\x0c\n\x08RELIABLE\x10\x46\x12\x07\n\x03\x41\x43K\x10x\x12\x07\n\x03MAX\x10\x7f\x42\x10\n\x0epayloadVariant\"h\n\x08NodeInfo\x12\x0b\n\x03num\x18\x01 \x01(\r\x12\x13\n\x04user\x18\x02 \x01(\x0b\x32\x05.User\x12\x1b\n\x08position\x18\x03 \x01(\x0b\x32\t.Position\x12\x0b\n\x03snr\x18\x07 \x01(\x02\x12\x10\n\x08next_hop\x18\x05 \x01(\r\"\xa6\x02\n\nMyNodeInfo\x12\x13\n\x0bmy_node_num\x18\x01 \x01(\r\x12\x0f\n\x07has_gps\x18\x02 \x01(\x08\x12\x11\n\tnum_bands\x18\x03 \x01(\r\x12\x14\n\x0cmax_channels\x18\x0f \x01(\r\x12\x12\n\x06region\x18\x04 \x01(\tB\x02\x18\x01\x12\x10\n\x08hw_model\x18\x05 \x01(\t\x12\x18\n\x10\x66irmware_version\x18\x06 \x01(\t\x12&\n\nerror_code\x18\x07 \x01(\x0e\x32\x12.CriticalErrorCode\x12\x15\n\rerror_address\x18\x08 \x01(\r\x12\x13\n\x0b\x65rror_count\x18\t \x01(\r\x12\x1c\n\x14message_timeout_msec\x18\r \x01(\r\x12\x17\n\x0fmin_app_version\x18\x0e \x01(\r\"\xb5\x01\n\tLogRecord\x12\x0f\n\x07message\x18\x01 \x01(\t\x12\x0c\n\x04time\x18\x02 \x01(\x07\x12\x0e\n\x06source\x18\x03 \x01(\t\x12\x1f\n\x05level\x18\x04 \x01(\x0e\x32\x10.LogRecord.Level\"X\n\x05Level\x12\t\n\x05UNSET\x10\x00\x12\x0c\n\x08\x43RITICAL\x10\x32\x12\t\n\x05\x45RROR\x10(\x12\x0b\n\x07WARNING\x10\x1e\x12\x08\n\x04INFO\x10\x14\x12\t\n\x05\x44\x45\x42UG\x10\n\x12\t\n\x05TRACE\x10\x05\"\xe9\x01\n\tFromRadio\x12\x0b\n\x03num\x18\x01 \x01(\r\x12\x1d\n\x06packet\x18\x0b \x01(\x0b\x32\x0b.MeshPacketH\x00\x12\x1e\n\x07my_info\x18\x03 \x01(\x0b\x32\x0b.MyNodeInfoH\x00\x12\x1e\n\tnode_info\x18\x04 \x01(\x0b\x32\t.NodeInfoH\x00\x12 \n\nlog_record\x18\x07 \x01(\x0b\x32\n.LogRecordH\x00\x12\x1c\n\x12\x63onfig_complete_id\x18\x08 \x01(\rH\x00\x12\x12\n\x08rebooted\x18\t \x01(\x08H\x00\x42\x10\n\x0epayloadVariantJ\x04\x08\x02\x10\x03J\x04\x08\x06\x10\x07\"l\n\x07ToRadio\x12\x1d\n\x06packet\x18\x02 \x01(\x0b\x32\x0b.MeshPacketH\x00\x12\x18\n\x0ewant_config_id\x18\x64 \x01(\rH\x00\x42\x10\n\x0epayloadVariantJ\x04\x08\x01\x10\x02J\x04\x08\x65\x10\x66J\x04\x08\x66\x10gJ\x04\x08g\x10h*.\n\tConstants\x12\n\n\x06Unused\x10\x00\x12\x15\n\x10\x44\x41TA_PAYLOAD_LEN\x10\xf0\x01*\xbd\x01\n\x11\x43riticalErrorCode\x12\x08\n\x04None\x10\x00\x12\x0e\n\nTxWatchdog\x10\x01\x12\x12\n\x0eSleepEnterWait\x10\x02\x12\x0b\n\x07NoRadio\x10\x03\x12\x0f\n\x0bUnspecified\x10\x04\x12\x13\n\x0fUBloxInitFailed\x10\x05\x12\x0c\n\x08NoAXP192\x10\x06\x12\x17\n\x13InvalidRadioSetting\x10\x07\x12\x12\n\x0eTransmitFailed\x10\x08\x12\x0c\n\x08\x42rownout\x10\tB#\n\x13\x63om.geeksville.meshB\nMeshProtosH\x03\x62\x06proto3'
,
dependencies=[portnums__pb2.DESCRIPTOR,])
@@ -132,11 +132,16 @@ _CRITICALERRORCODE = _descriptor.EnumDescriptor(
serialized_options=None,
type=None,
create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='Brownout', index=9, number=9,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
],
containing_type=None,
serialized_options=None,
serialized_start=1977,
- serialized_end=2152,
+ serialized_end=2166,
)
_sym_db.RegisterEnumDescriptor(_CRITICALERRORCODE)
@@ -152,6 +157,7 @@ UBloxInitFailed = 5
NoAXP192 = 6
InvalidRadioSetting = 7
TransmitFailed = 8
+Brownout = 9
_ROUTING_ERROR = _descriptor.EnumDescriptor(
diff --git a/docs/meshtastic/util.html b/docs/meshtastic/util.html
index cec8525..88ace01 100644
--- a/docs/meshtastic/util.html
+++ b/docs/meshtastic/util.html
@@ -29,6 +29,8 @@
from collections import defaultdict
import serial
import serial.tools.list_ports
+from queue import Queue
+import threading, sys, logging
"""Some devices such as a seger jlink we never want to accidentally open"""
blacklistVids = dict.fromkeys([0x1366])
@@ -68,7 +70,29 @@ class dotdict(dict):
"""dot.notation access to dictionary attributes"""
__getattr__ = dict.get
__setattr__ = dict.__setitem__
- __delattr__ = dict.__delitem__
+ __delattr__ = dict.__delitem__
+
+
+class DeferredExecution():
+ """A thread that accepts closures to run, and runs them as they are received"""
+
+ def __init__(self, name=None):
+ self.queue = Queue()
+ self.thread = threading.Thread(target=self._run, args=(), name=name)
+ self.thread.daemon = True
+ self.thread.start()
+
+ def queueWork(self, runnable):
+ self.queue.put(runnable)
+
+ def _run(self):
+ while True:
+ try:
+ o = self.queue.get()
+ o()
+ except:
+ logging.error(
+ f"Unexpected error in deferred execution {sys.exc_info()[0]}")
@@ -151,6 +175,54 @@ class dotdict(dict):
+
+class DeferredExecution
+(name=None)
+
+-
+
A thread that accepts closures to run, and runs them as they are received
+
+
+Expand source code
+
+class DeferredExecution():
+ """A thread that accepts closures to run, and runs them as they are received"""
+
+ def __init__(self, name=None):
+ self.queue = Queue()
+ self.thread = threading.Thread(target=self._run, args=(), name=name)
+ self.thread.daemon = True
+ self.thread.start()
+
+ def queueWork(self, runnable):
+ self.queue.put(runnable)
+
+ def _run(self):
+ while True:
+ try:
+ o = self.queue.get()
+ o()
+ except:
+ logging.error(
+ f"Unexpected error in deferred execution {sys.exc_info()[0]}")
+
+Methods
+
+
+def queueWork(self, runnable)
+
+-
+
+
+
+Expand source code
+
+def queueWork(self, runnable):
+ self.queue.put(runnable)
+
+
+
+
class dotdict
(*args, **kwargs)
@@ -197,6 +269,12 @@ class dotdict(dict):
-
diff --git a/meshtastic/__init__.py b/meshtastic/__init__.py
index 7bf3b60..cb6fee7 100644
--- a/meshtastic/__init__.py
+++ b/meshtastic/__init__.py
@@ -503,8 +503,6 @@ class MeshInterface:
raise Exception(f"NodeId {destinationId} not found in DB")
nodeNum = node['num']
- if nodeNum == -1:
- raise Exception("Badbug")
meshPacket.to = nodeNum
meshPacket.want_ack = wantAck
meshPacket.hop_limit = hopLimit
diff --git a/setup.py b/setup.py
index 449f27c..25c69bc 100644
--- a/setup.py
+++ b/setup.py
@@ -12,7 +12,7 @@ with open("README.md", "r") as fh:
# This call to setup() does all the work
setup(
name="meshtastic",
- version="1.2.7",
+ version="1.2.8",
description="Python API & client shell for talking to Meshtastic devices",
long_description=long_description,
long_description_content_type="text/markdown",