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

AdminMessage

  • 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

    Classes

    +
    +class ChannelFile +(*args, **kwargs) +
    +
    +

    A ProtocolMessage

    +

    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

    Field DeviceState.owner

    -
    var radio
    -
    -

    Field DeviceState.radio

    -
    var receive_queue

    Field DeviceState.receive_queue

    @@ -245,6 +382,36 @@ DESCRIPTOR._options = None
+
+class LegacyRadioConfig +(*args, **kwargs) +
+
+

A ProtocolMessage

+

Ancestors

+
    +
  • google.protobuf.pyext._message.CMessage
  • +
  • google.protobuf.message.Message
  • +
+

Class variables

+
+
var DESCRIPTOR
+
+
+
+
var LegacyPreferences
+
+

A ProtocolMessage

+
+
+

Instance variables

+
+
var preferences
+
+

Field LegacyRadioConfig.preferences

+
+
+
@@ -262,21 +429,35 @@ DESCRIPTOR._options = None
  • Classes

  • 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 = {

    Global variables

    -
    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

    +
    +

    Functions

    +
    +
    +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
    +
    +
    +

    Classes

    @@ -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

  • Global variables

    +
  • +
  • Functions

    +
  • Classes

    @@ -2802,19 +3099,29 @@ hostname {string} – Hostname/IP address of the device to connect to

    MeshInterface

    +
  • +
  • +

    Node

    +
  • 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):

    Classes

    +
    +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):
  • Classes

    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",