mirror of
https://github.com/meshtastic/python.git
synced 2025-12-26 01:17:51 -05:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c703aff1d | ||
|
|
5441266565 | ||
|
|
890557fa5d | ||
|
|
e27d210a71 | ||
|
|
16c08b8b47 | ||
|
|
ebd3c7f5e8 | ||
|
|
da0312a5b0 | ||
|
|
919ae8c40f | ||
|
|
dd4fccbc77 | ||
|
|
32682b5230 | ||
|
|
9dab76bb64 | ||
|
|
e6d61c6603 | ||
|
|
ee857c5128 | ||
|
|
87a4bb0888 | ||
|
|
d72cc0e201 | ||
|
|
b350b9eab9 | ||
|
|
dc112f2f3a | ||
|
|
14ae4eeac1 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -15,3 +15,4 @@ venv/
|
||||
__pycache__
|
||||
examples/__pycache__
|
||||
meshtastic.spec
|
||||
.hypothesis/
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
"""
|
||||
# an API for Meshtastic devices
|
||||
# A library for the Meshtastic Client API
|
||||
|
||||
Primary class: SerialInterface
|
||||
Primary interfaces: SerialInterface, TCPInterface, BLEInterface
|
||||
Install with pip: "[pip3 install meshtastic](https://pypi.org/project/meshtastic/)"
|
||||
Source code on [github](https://github.com/meshtastic/python)
|
||||
|
||||
properties of SerialInterface:
|
||||
notable properties of interface classes:
|
||||
|
||||
- localConfig - Current radio configuration and device settings, if you write to this the new settings will be applied to
|
||||
the device.
|
||||
- nodes - The database of received nodes. Includes always up-to-date location and username information for each
|
||||
node in the mesh. This is a read-only datastructure.
|
||||
- nodesByNum - like "nodes" but keyed by nodeNum instead of nodeId
|
||||
- myInfo - Contains read-only information about the local radio device (software version, hardware version, etc)
|
||||
- myInfo & metadata - Contain read-only information about the local radio device (software version, hardware version, etc)
|
||||
- localNode - Pointer to a node object for the local node
|
||||
|
||||
notable properties of nodes:
|
||||
- localConfig - Current radio settings, can be written to the radio with the `writeConfig` method.
|
||||
- moduleConfig - Current module settings, can be written to the radio with the `writeConfig` method.
|
||||
- channels - The node's channels, keyed by index.
|
||||
|
||||
# Published PubSub topics
|
||||
|
||||
@@ -113,6 +117,9 @@ OUR_APP_VERSION = 20300
|
||||
format is Mmmss (where M is 1+the numeric major number. i.e. 20120 means 1.1.20
|
||||
"""
|
||||
|
||||
NODELESS_WANT_CONFIG_ID = 69420
|
||||
"""A special thing to pass for want_config_id that instructs nodes to skip sending nodeinfos other than its own."""
|
||||
|
||||
publishingThread = DeferredExecution("publishing")
|
||||
|
||||
|
||||
|
||||
@@ -1050,11 +1050,11 @@ def common():
|
||||
meshtastic.util.our_exit("BLE scan finished", 0)
|
||||
return
|
||||
elif args.ble:
|
||||
client = BLEInterface(args.ble, debugOut=logfile, noProto=args.noproto)
|
||||
client = BLEInterface(args.ble, debugOut=logfile, noProto=args.noproto, noNodes=args.no_nodes)
|
||||
elif args.host:
|
||||
try:
|
||||
client = meshtastic.tcp_interface.TCPInterface(
|
||||
args.host, debugOut=logfile, noProto=args.noproto
|
||||
args.host, debugOut=logfile, noProto=args.noproto, noNodes=args.no_nodes
|
||||
)
|
||||
except Exception as ex:
|
||||
meshtastic.util.our_exit(
|
||||
@@ -1063,7 +1063,7 @@ def common():
|
||||
else:
|
||||
try:
|
||||
client = meshtastic.serial_interface.SerialInterface(
|
||||
args.port, debugOut=logfile, noProto=args.noproto
|
||||
args.port, debugOut=logfile, noProto=args.noproto, noNodes=args.no_nodes
|
||||
)
|
||||
except PermissionError as ex:
|
||||
username = os.getlogin()
|
||||
@@ -1078,7 +1078,7 @@ def common():
|
||||
if client.devPath is None:
|
||||
try:
|
||||
client = meshtastic.tcp_interface.TCPInterface(
|
||||
"localhost", debugOut=logfile, noProto=args.noproto
|
||||
"localhost", debugOut=logfile, noProto=args.noproto, noNodes=args.no_nodes
|
||||
)
|
||||
except Exception as ex:
|
||||
meshtastic.util.our_exit(
|
||||
@@ -1453,6 +1453,13 @@ def initParser():
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
group.add_argument(
|
||||
"--no-nodes",
|
||||
help="Request that the node not send node info to the client. "
|
||||
"Will break things that depend on the nodedb, but will speed up startup. Requires 2.3.11+ firmware.",
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
group.add_argument(
|
||||
"--setalt",
|
||||
help="Set device altitude in meters (allows use without GPS), and enable fixed position.",
|
||||
|
||||
@@ -13,7 +13,7 @@ _sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15meshtastic/atak.proto\x12\nmeshtastic\"\xe6\x01\n\tTAKPacket\x12\x15\n\ris_compressed\x18\x01 \x01(\x08\x12$\n\x07\x63ontact\x18\x02 \x01(\x0b\x32\x13.meshtastic.Contact\x12 \n\x05group\x18\x03 \x01(\x0b\x32\x11.meshtastic.Group\x12\"\n\x06status\x18\x04 \x01(\x0b\x32\x12.meshtastic.Status\x12\x1e\n\x03pli\x18\x05 \x01(\x0b\x32\x0f.meshtastic.PLIH\x00\x12#\n\x04\x63hat\x18\x06 \x01(\x0b\x32\x13.meshtastic.GeoChatH\x00\x42\x11\n\x0fpayload_variant\"2\n\x07GeoChat\x12\x0f\n\x07message\x18\x01 \x01(\t\x12\x0f\n\x02to\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\x05\n\x03_to\"M\n\x05Group\x12$\n\x04role\x18\x01 \x01(\x0e\x32\x16.meshtastic.MemberRole\x12\x1e\n\x04team\x18\x02 \x01(\x0e\x32\x10.meshtastic.Team\"\x19\n\x06Status\x12\x0f\n\x07\x62\x61ttery\x18\x01 \x01(\r\"4\n\x07\x43ontact\x12\x10\n\x08\x63\x61llsign\x18\x01 \x01(\t\x12\x17\n\x0f\x64\x65vice_callsign\x18\x02 \x01(\t\"_\n\x03PLI\x12\x12\n\nlatitude_i\x18\x01 \x01(\x0f\x12\x13\n\x0blongitude_i\x18\x02 \x01(\x0f\x12\x10\n\x08\x61ltitude\x18\x03 \x01(\x05\x12\r\n\x05speed\x18\x04 \x01(\r\x12\x0e\n\x06\x63ourse\x18\x05 \x01(\r*\xc0\x01\n\x04Team\x12\x14\n\x10Unspecifed_Color\x10\x00\x12\t\n\x05White\x10\x01\x12\n\n\x06Yellow\x10\x02\x12\n\n\x06Orange\x10\x03\x12\x0b\n\x07Magenta\x10\x04\x12\x07\n\x03Red\x10\x05\x12\n\n\x06Maroon\x10\x06\x12\n\n\x06Purple\x10\x07\x12\r\n\tDark_Blue\x10\x08\x12\x08\n\x04\x42lue\x10\t\x12\x08\n\x04\x43yan\x10\n\x12\x08\n\x04Teal\x10\x0b\x12\t\n\x05Green\x10\x0c\x12\x0e\n\nDark_Green\x10\r\x12\t\n\x05\x42rown\x10\x0e*\x7f\n\nMemberRole\x12\x0e\n\nUnspecifed\x10\x00\x12\x0e\n\nTeamMember\x10\x01\x12\x0c\n\x08TeamLead\x10\x02\x12\x06\n\x02HQ\x10\x03\x12\n\n\x06Sniper\x10\x04\x12\t\n\x05Medic\x10\x05\x12\x13\n\x0f\x46orwardObserver\x10\x06\x12\x07\n\x03RTO\x10\x07\x12\x06\n\x02K9\x10\x08\x42_\n\x13\x63om.geeksville.meshB\nATAKProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15meshtastic/atak.proto\x12\nmeshtastic\"\xe6\x01\n\tTAKPacket\x12\x15\n\ris_compressed\x18\x01 \x01(\x08\x12$\n\x07\x63ontact\x18\x02 \x01(\x0b\x32\x13.meshtastic.Contact\x12 \n\x05group\x18\x03 \x01(\x0b\x32\x11.meshtastic.Group\x12\"\n\x06status\x18\x04 \x01(\x0b\x32\x12.meshtastic.Status\x12\x1e\n\x03pli\x18\x05 \x01(\x0b\x32\x0f.meshtastic.PLIH\x00\x12#\n\x04\x63hat\x18\x06 \x01(\x0b\x32\x13.meshtastic.GeoChatH\x00\x42\x11\n\x0fpayload_variant\"\\\n\x07GeoChat\x12\x0f\n\x07message\x18\x01 \x01(\t\x12\x0f\n\x02to\x18\x02 \x01(\tH\x00\x88\x01\x01\x12\x18\n\x0bto_callsign\x18\x03 \x01(\tH\x01\x88\x01\x01\x42\x05\n\x03_toB\x0e\n\x0c_to_callsign\"M\n\x05Group\x12$\n\x04role\x18\x01 \x01(\x0e\x32\x16.meshtastic.MemberRole\x12\x1e\n\x04team\x18\x02 \x01(\x0e\x32\x10.meshtastic.Team\"\x19\n\x06Status\x12\x0f\n\x07\x62\x61ttery\x18\x01 \x01(\r\"4\n\x07\x43ontact\x12\x10\n\x08\x63\x61llsign\x18\x01 \x01(\t\x12\x17\n\x0f\x64\x65vice_callsign\x18\x02 \x01(\t\"_\n\x03PLI\x12\x12\n\nlatitude_i\x18\x01 \x01(\x0f\x12\x13\n\x0blongitude_i\x18\x02 \x01(\x0f\x12\x10\n\x08\x61ltitude\x18\x03 \x01(\x05\x12\r\n\x05speed\x18\x04 \x01(\r\x12\x0e\n\x06\x63ourse\x18\x05 \x01(\r*\xc0\x01\n\x04Team\x12\x14\n\x10Unspecifed_Color\x10\x00\x12\t\n\x05White\x10\x01\x12\n\n\x06Yellow\x10\x02\x12\n\n\x06Orange\x10\x03\x12\x0b\n\x07Magenta\x10\x04\x12\x07\n\x03Red\x10\x05\x12\n\n\x06Maroon\x10\x06\x12\n\n\x06Purple\x10\x07\x12\r\n\tDark_Blue\x10\x08\x12\x08\n\x04\x42lue\x10\t\x12\x08\n\x04\x43yan\x10\n\x12\x08\n\x04Teal\x10\x0b\x12\t\n\x05Green\x10\x0c\x12\x0e\n\nDark_Green\x10\r\x12\t\n\x05\x42rown\x10\x0e*\x7f\n\nMemberRole\x12\x0e\n\nUnspecifed\x10\x00\x12\x0e\n\nTeamMember\x10\x01\x12\x0c\n\x08TeamLead\x10\x02\x12\x06\n\x02HQ\x10\x03\x12\n\n\x06Sniper\x10\x04\x12\t\n\x05Medic\x10\x05\x12\x13\n\x0f\x46orwardObserver\x10\x06\x12\x07\n\x03RTO\x10\x07\x12\x06\n\x02K9\x10\x08\x42_\n\x13\x63om.geeksville.meshB\nATAKProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.atak_pb2', globals())
|
||||
@@ -21,20 +21,20 @@ if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
|
||||
DESCRIPTOR._options = None
|
||||
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\nATAKProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||
_TEAM._serialized_start=580
|
||||
_TEAM._serialized_end=772
|
||||
_MEMBERROLE._serialized_start=774
|
||||
_MEMBERROLE._serialized_end=901
|
||||
_TEAM._serialized_start=622
|
||||
_TEAM._serialized_end=814
|
||||
_MEMBERROLE._serialized_start=816
|
||||
_MEMBERROLE._serialized_end=943
|
||||
_TAKPACKET._serialized_start=38
|
||||
_TAKPACKET._serialized_end=268
|
||||
_GEOCHAT._serialized_start=270
|
||||
_GEOCHAT._serialized_end=320
|
||||
_GROUP._serialized_start=322
|
||||
_GROUP._serialized_end=399
|
||||
_STATUS._serialized_start=401
|
||||
_STATUS._serialized_end=426
|
||||
_CONTACT._serialized_start=428
|
||||
_CONTACT._serialized_end=480
|
||||
_PLI._serialized_start=482
|
||||
_PLI._serialized_end=577
|
||||
_GEOCHAT._serialized_end=362
|
||||
_GROUP._serialized_start=364
|
||||
_GROUP._serialized_end=441
|
||||
_STATUS._serialized_start=443
|
||||
_STATUS._serialized_end=468
|
||||
_CONTACT._serialized_start=470
|
||||
_CONTACT._serialized_end=522
|
||||
_PLI._serialized_start=524
|
||||
_PLI._serialized_end=619
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
|
||||
@@ -302,6 +302,7 @@ class GeoChat(google.protobuf.message.Message):
|
||||
|
||||
MESSAGE_FIELD_NUMBER: builtins.int
|
||||
TO_FIELD_NUMBER: builtins.int
|
||||
TO_CALLSIGN_FIELD_NUMBER: builtins.int
|
||||
message: builtins.str
|
||||
"""
|
||||
The text message
|
||||
@@ -310,15 +311,23 @@ class GeoChat(google.protobuf.message.Message):
|
||||
"""
|
||||
Uid recipient of the message
|
||||
"""
|
||||
to_callsign: builtins.str
|
||||
"""
|
||||
Callsign of the recipient for the message
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
message: builtins.str = ...,
|
||||
to: builtins.str | None = ...,
|
||||
to_callsign: builtins.str | None = ...,
|
||||
) -> None: ...
|
||||
def HasField(self, field_name: typing_extensions.Literal["_to", b"_to", "to", b"to"]) -> builtins.bool: ...
|
||||
def ClearField(self, field_name: typing_extensions.Literal["_to", b"_to", "message", b"message", "to", b"to"]) -> None: ...
|
||||
def HasField(self, field_name: typing_extensions.Literal["_to", b"_to", "_to_callsign", b"_to_callsign", "to", b"to", "to_callsign", b"to_callsign"]) -> builtins.bool: ...
|
||||
def ClearField(self, field_name: typing_extensions.Literal["_to", b"_to", "_to_callsign", b"_to_callsign", "message", b"message", "to", b"to", "to_callsign", b"to_callsign"]) -> None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing_extensions.Literal["_to", b"_to"]) -> typing_extensions.Literal["to"] | None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing_extensions.Literal["_to_callsign", b"_to_callsign"]) -> typing_extensions.Literal["to_callsign"] | None: ...
|
||||
|
||||
global___GeoChat = GeoChat
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ class BLEInterface(MeshInterface):
|
||||
MESH = False
|
||||
|
||||
|
||||
def __init__(self, address: Optional[str], noProto: bool = False, debugOut = None):
|
||||
def __init__(self, address: Optional[str], noProto: bool = False, debugOut = None, noNodes: bool = False):
|
||||
self.state = BLEInterface.BLEState()
|
||||
|
||||
if not address:
|
||||
@@ -60,7 +60,7 @@ class BLEInterface(MeshInterface):
|
||||
return
|
||||
|
||||
logging.debug("Mesh init starting")
|
||||
MeshInterface.__init__(self, debugOut = debugOut, noProto = noProto)
|
||||
MeshInterface.__init__(self, debugOut = debugOut, noProto = noProto, noNodes = noNodes)
|
||||
self._startConfig()
|
||||
if not self.noProto:
|
||||
self._waitConnected(timeout = 60.0)
|
||||
|
||||
@@ -26,6 +26,7 @@ from meshtastic import (
|
||||
BROADCAST_ADDR,
|
||||
BROADCAST_NUM,
|
||||
LOCAL_ADDR,
|
||||
NODELESS_WANT_CONFIG_ID,
|
||||
ResponseHandler,
|
||||
protocols,
|
||||
publishingThread,
|
||||
@@ -41,7 +42,7 @@ from meshtastic.util import (
|
||||
)
|
||||
|
||||
|
||||
class MeshInterface:
|
||||
class MeshInterface: # pylint: disable=R0902
|
||||
"""Interface class for meshtastic devices
|
||||
|
||||
Properties:
|
||||
@@ -57,12 +58,14 @@ class MeshInterface:
|
||||
self.message = message
|
||||
super().__init__(self.message)
|
||||
|
||||
def __init__(self, debugOut=None, noProto: bool=False) -> None:
|
||||
def __init__(self, debugOut=None, noProto: bool=False, noNodes: bool=False) -> None:
|
||||
"""Constructor
|
||||
|
||||
Keyword Arguments:
|
||||
noProto -- If True, don't try to run our protocol on the
|
||||
link - just be a dumb serial client.
|
||||
noNodes -- If True, instruct the node to not send its nodedb
|
||||
on startup, just other configuration information.
|
||||
"""
|
||||
self.debugOut = debugOut
|
||||
self.nodes: Optional[Dict[str,Dict]] = None # FIXME
|
||||
@@ -81,7 +84,8 @@ class MeshInterface:
|
||||
random.seed() # FIXME, we should not clobber the random seedval here, instead tell user they must call it
|
||||
self.currentPacketId: int = random.randint(0, 0xFFFFFFFF)
|
||||
self.nodesByNum: Optional[Dict[int, Dict]] = None
|
||||
self.configId: Optional[int] = None
|
||||
self.noNodes: bool = noNodes
|
||||
self.configId: Optional[int] = NODELESS_WANT_CONFIG_ID if noNodes else None
|
||||
self.gotResponse: bool = False # used in gpio read
|
||||
self.mask: Optional[int] = None # used in gpio read and gpio watch
|
||||
self.queueStatus: Optional[mesh_pb2.QueueStatus] = None
|
||||
@@ -713,7 +717,8 @@ class MeshInterface:
|
||||
self._localChannels = [] # empty until we start getting channels pushed from the device (during config)
|
||||
|
||||
startConfig = mesh_pb2.ToRadio()
|
||||
self.configId = random.randint(0, 0xFFFFFFFF)
|
||||
if self.configId is None or not self.noNodes:
|
||||
self.configId = random.randint(0, 0xFFFFFFFF)
|
||||
startConfig.want_config_id = self.configId
|
||||
self._sendToRadio(startConfig)
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -285,6 +285,11 @@ class _HardwareModelEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._
|
||||
NRF52_PROMICRO_DIY
|
||||
Promicro NRF52840 with SX1262/LLCC68, SSD1306 OLED and NEO6M GPS
|
||||
"""
|
||||
RADIOMASTER_900_BANDIT_NANO: _HardwareModel.ValueType # 64
|
||||
"""
|
||||
RadioMaster 900 Bandit Nano, https://www.radiomasterrc.com/products/bandit-nano-expresslrs-rf-module
|
||||
ESP32-D0WDQ6 With SX1276/SKY66122, SSD1306 OLED and No GPS
|
||||
"""
|
||||
PRIVATE_HW: _HardwareModel.ValueType # 255
|
||||
"""
|
||||
------------------------------------------------------------------------------------------------------------------------------------------
|
||||
@@ -555,6 +560,11 @@ NRF52_PROMICRO_DIY: HardwareModel.ValueType # 63
|
||||
NRF52_PROMICRO_DIY
|
||||
Promicro NRF52840 with SX1262/LLCC68, SSD1306 OLED and NEO6M GPS
|
||||
"""
|
||||
RADIOMASTER_900_BANDIT_NANO: HardwareModel.ValueType # 64
|
||||
"""
|
||||
RadioMaster 900 Bandit Nano, https://www.radiomasterrc.com/products/bandit-nano-expresslrs-rf-module
|
||||
ESP32-D0WDQ6 With SX1276/SKY66122, SSD1306 OLED and No GPS
|
||||
"""
|
||||
PRIVATE_HW: HardwareModel.ValueType # 255
|
||||
"""
|
||||
------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
@@ -608,9 +608,10 @@ class Node:
|
||||
p.get_device_metadata_request = True
|
||||
logging.info(f"Requesting device metadata")
|
||||
|
||||
return self._sendAdmin(
|
||||
self._sendAdmin(
|
||||
p, wantResponse=True, onResponse=self.onRequestGetMetadata
|
||||
)
|
||||
self.iface.waitForAckNak()
|
||||
|
||||
def factoryReset(self):
|
||||
"""Tell the node to factory reset."""
|
||||
@@ -713,24 +714,30 @@ class Node:
|
||||
"""Handle the response packet for requesting device metadata getMetadata()"""
|
||||
logging.debug(f"onRequestGetMetadata() p:{p}")
|
||||
|
||||
if p["decoded"]["portnum"] == portnums_pb2.PortNum.Name(
|
||||
portnums_pb2.PortNum.ROUTING_APP
|
||||
):
|
||||
if "routing" in p["decoded"]:
|
||||
if p["decoded"]["routing"]["errorReason"] != "NONE":
|
||||
logging.warning(
|
||||
f'Metadata request failed, error reason: {p["decoded"]["routing"]["errorReason"]}'
|
||||
)
|
||||
self._timeout.expireTime = time.time() # Do not wait any longer
|
||||
return # Don't try to parse this routing message
|
||||
logging.debug(f"Retrying metadata request.")
|
||||
self.getMetadata()
|
||||
return
|
||||
print(f'Error on response: {p["decoded"]["routing"]["errorReason"]}')
|
||||
self.iface._acknowledgment.receivedNak = True
|
||||
else:
|
||||
self.iface._acknowledgment.receivedAck = True
|
||||
if p["decoded"]["portnum"] == portnums_pb2.PortNum.Name(
|
||||
portnums_pb2.PortNum.ROUTING_APP
|
||||
):
|
||||
if p["decoded"]["routing"]["errorReason"] != "NONE":
|
||||
logging.warning(
|
||||
f'Metadata request failed, error reason: {p["decoded"]["routing"]["errorReason"]}'
|
||||
)
|
||||
self._timeout.expireTime = time.time() # Do not wait any longer
|
||||
return # Don't try to parse this routing message
|
||||
logging.debug(f"Retrying metadata request.")
|
||||
self.getMetadata()
|
||||
return
|
||||
|
||||
c = p["decoded"]["admin"]["raw"].get_device_metadata_response
|
||||
self._timeout.reset() # We made forward progress
|
||||
logging.debug(f"Received metadata {stripnl(c)}")
|
||||
print(f"\nfirmware_version: {c.firmware_version}")
|
||||
print(f"device_state_version: {c.device_state_version}")
|
||||
c = p["decoded"]["admin"]["raw"].get_device_metadata_response
|
||||
self._timeout.reset() # We made forward progress
|
||||
logging.debug(f"Received metadata {stripnl(c)}")
|
||||
print(f"\nfirmware_version: {c.firmware_version}")
|
||||
print(f"device_state_version: {c.device_state_version}")
|
||||
|
||||
def onResponseRequestChannel(self, p):
|
||||
"""Handle the response packet for requesting a channel _requestChannel()"""
|
||||
|
||||
@@ -18,7 +18,7 @@ if platform.system() != "Windows":
|
||||
class SerialInterface(StreamInterface):
|
||||
"""Interface class for meshtastic devices over a serial link"""
|
||||
|
||||
def __init__(self, devPath: Optional[str]=None, debugOut=None, noProto=False, connectNow=True):
|
||||
def __init__(self, devPath: Optional[str]=None, debugOut=None, noProto=False, connectNow=True, noNodes: bool=False):
|
||||
"""Constructor, opens a connection to a specified serial port, or if unspecified try to
|
||||
find one Meshtastic device by probing
|
||||
|
||||
@@ -62,7 +62,7 @@ class SerialInterface(StreamInterface):
|
||||
time.sleep(0.1)
|
||||
|
||||
StreamInterface.__init__(
|
||||
self, debugOut=debugOut, noProto=noProto, connectNow=connectNow
|
||||
self, debugOut=debugOut, noProto=noProto, connectNow=connectNow, noNodes=noNodes
|
||||
)
|
||||
|
||||
def close(self):
|
||||
|
||||
@@ -19,7 +19,7 @@ MAX_TO_FROM_RADIO_SIZE = 512
|
||||
class StreamInterface(MeshInterface):
|
||||
"""Interface class for meshtastic devices over a stream link (serial, TCP, etc)"""
|
||||
|
||||
def __init__(self, debugOut=None, noProto=False, connectNow=True):
|
||||
def __init__(self, debugOut=None, noProto=False, connectNow=True, noNodes=False):
|
||||
"""Constructor, opens a connection to self.stream
|
||||
|
||||
Keyword Arguments:
|
||||
@@ -43,7 +43,7 @@ class StreamInterface(MeshInterface):
|
||||
# FIXME, figure out why daemon=True causes reader thread to exit too early
|
||||
self._rxThread = threading.Thread(target=self.__reader, args=(), daemon=True)
|
||||
|
||||
MeshInterface.__init__(self, debugOut=debugOut, noProto=noProto)
|
||||
MeshInterface.__init__(self, debugOut=debugOut, noProto=noProto, noNodes=noNodes)
|
||||
|
||||
# Start the reader thread after superclass constructor completes init
|
||||
if connectNow:
|
||||
|
||||
@@ -17,6 +17,7 @@ class TCPInterface(StreamInterface):
|
||||
noProto=False,
|
||||
connectNow=True,
|
||||
portNumber=4403,
|
||||
noNodes:bool=False,
|
||||
):
|
||||
"""Constructor, opens a connection to a specified IP address/hostname
|
||||
|
||||
@@ -38,7 +39,7 @@ class TCPInterface(StreamInterface):
|
||||
self.socket = None
|
||||
|
||||
StreamInterface.__init__(
|
||||
self, debugOut=debugOut, noProto=noProto, connectNow=connectNow
|
||||
self, debugOut=debugOut, noProto=noProto, connectNow=connectNow, noNodes=noNodes
|
||||
)
|
||||
|
||||
def _socket_shutdown(self):
|
||||
|
||||
@@ -13,7 +13,7 @@ _sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1ameshtastic/telemetry.proto\x12\nmeshtastic\"\x81\x01\n\rDeviceMetrics\x12\x15\n\rbattery_level\x18\x01 \x01(\r\x12\x0f\n\x07voltage\x18\x02 \x01(\x02\x12\x1b\n\x13\x63hannel_utilization\x18\x03 \x01(\x02\x12\x13\n\x0b\x61ir_util_tx\x18\x04 \x01(\x02\x12\x16\n\x0euptime_seconds\x18\x05 \x01(\r\"\xda\x01\n\x12\x45nvironmentMetrics\x12\x13\n\x0btemperature\x18\x01 \x01(\x02\x12\x19\n\x11relative_humidity\x18\x02 \x01(\x02\x12\x1b\n\x13\x62\x61rometric_pressure\x18\x03 \x01(\x02\x12\x16\n\x0egas_resistance\x18\x04 \x01(\x02\x12\x0f\n\x07voltage\x18\x05 \x01(\x02\x12\x0f\n\x07\x63urrent\x18\x06 \x01(\x02\x12\x0b\n\x03iaq\x18\x07 \x01(\r\x12\x10\n\x08\x64istance\x18\x08 \x01(\x02\x12\x0b\n\x03lux\x18\t \x01(\x02\x12\x11\n\twhite_lux\x18\n \x01(\x02\"\x8c\x01\n\x0cPowerMetrics\x12\x13\n\x0b\x63h1_voltage\x18\x01 \x01(\x02\x12\x13\n\x0b\x63h1_current\x18\x02 \x01(\x02\x12\x13\n\x0b\x63h2_voltage\x18\x03 \x01(\x02\x12\x13\n\x0b\x63h2_current\x18\x04 \x01(\x02\x12\x13\n\x0b\x63h3_voltage\x18\x05 \x01(\x02\x12\x13\n\x0b\x63h3_current\x18\x06 \x01(\x02\"\xbf\x02\n\x11\x41irQualityMetrics\x12\x15\n\rpm10_standard\x18\x01 \x01(\r\x12\x15\n\rpm25_standard\x18\x02 \x01(\r\x12\x16\n\x0epm100_standard\x18\x03 \x01(\r\x12\x1a\n\x12pm10_environmental\x18\x04 \x01(\r\x12\x1a\n\x12pm25_environmental\x18\x05 \x01(\r\x12\x1b\n\x13pm100_environmental\x18\x06 \x01(\r\x12\x16\n\x0eparticles_03um\x18\x07 \x01(\r\x12\x16\n\x0eparticles_05um\x18\x08 \x01(\r\x12\x16\n\x0eparticles_10um\x18\t \x01(\r\x12\x16\n\x0eparticles_25um\x18\n \x01(\r\x12\x16\n\x0eparticles_50um\x18\x0b \x01(\r\x12\x17\n\x0fparticles_100um\x18\x0c \x01(\r\"\x89\x02\n\tTelemetry\x12\x0c\n\x04time\x18\x01 \x01(\x07\x12\x33\n\x0e\x64\x65vice_metrics\x18\x02 \x01(\x0b\x32\x19.meshtastic.DeviceMetricsH\x00\x12=\n\x13\x65nvironment_metrics\x18\x03 \x01(\x0b\x32\x1e.meshtastic.EnvironmentMetricsH\x00\x12<\n\x13\x61ir_quality_metrics\x18\x04 \x01(\x0b\x32\x1d.meshtastic.AirQualityMetricsH\x00\x12\x31\n\rpower_metrics\x18\x05 \x01(\x0b\x32\x18.meshtastic.PowerMetricsH\x00\x42\t\n\x07variant*\xc0\x02\n\x13TelemetrySensorType\x12\x10\n\x0cSENSOR_UNSET\x10\x00\x12\n\n\x06\x42ME280\x10\x01\x12\n\n\x06\x42ME680\x10\x02\x12\x0b\n\x07MCP9808\x10\x03\x12\n\n\x06INA260\x10\x04\x12\n\n\x06INA219\x10\x05\x12\n\n\x06\x42MP280\x10\x06\x12\t\n\x05SHTC3\x10\x07\x12\t\n\x05LPS22\x10\x08\x12\x0b\n\x07QMC6310\x10\t\x12\x0b\n\x07QMI8658\x10\n\x12\x0c\n\x08QMC5883L\x10\x0b\x12\t\n\x05SHT31\x10\x0c\x12\x0c\n\x08PMSA003I\x10\r\x12\x0b\n\x07INA3221\x10\x0e\x12\n\n\x06\x42MP085\x10\x0f\x12\x0c\n\x08RCWL9620\x10\x10\x12\t\n\x05SHT4X\x10\x11\x12\x0c\n\x08VEML7700\x10\x12\x12\x0c\n\x08MLX90632\x10\x13\x12\x0b\n\x07OPT3001\x10\x14\x12\x0c\n\x08LTR390UV\x10\x15\x12\x0e\n\nTSL25911FN\x10\x16\x42\x64\n\x13\x63om.geeksville.meshB\x0fTelemetryProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1ameshtastic/telemetry.proto\x12\nmeshtastic\"\x81\x01\n\rDeviceMetrics\x12\x15\n\rbattery_level\x18\x01 \x01(\r\x12\x0f\n\x07voltage\x18\x02 \x01(\x02\x12\x1b\n\x13\x63hannel_utilization\x18\x03 \x01(\x02\x12\x13\n\x0b\x61ir_util_tx\x18\x04 \x01(\x02\x12\x16\n\x0euptime_seconds\x18\x05 \x01(\r\"\xda\x01\n\x12\x45nvironmentMetrics\x12\x13\n\x0btemperature\x18\x01 \x01(\x02\x12\x19\n\x11relative_humidity\x18\x02 \x01(\x02\x12\x1b\n\x13\x62\x61rometric_pressure\x18\x03 \x01(\x02\x12\x16\n\x0egas_resistance\x18\x04 \x01(\x02\x12\x0f\n\x07voltage\x18\x05 \x01(\x02\x12\x0f\n\x07\x63urrent\x18\x06 \x01(\x02\x12\x0b\n\x03iaq\x18\x07 \x01(\r\x12\x10\n\x08\x64istance\x18\x08 \x01(\x02\x12\x0b\n\x03lux\x18\t \x01(\x02\x12\x11\n\twhite_lux\x18\n \x01(\x02\"\x8c\x01\n\x0cPowerMetrics\x12\x13\n\x0b\x63h1_voltage\x18\x01 \x01(\x02\x12\x13\n\x0b\x63h1_current\x18\x02 \x01(\x02\x12\x13\n\x0b\x63h2_voltage\x18\x03 \x01(\x02\x12\x13\n\x0b\x63h2_current\x18\x04 \x01(\x02\x12\x13\n\x0b\x63h3_voltage\x18\x05 \x01(\x02\x12\x13\n\x0b\x63h3_current\x18\x06 \x01(\x02\"\xbf\x02\n\x11\x41irQualityMetrics\x12\x15\n\rpm10_standard\x18\x01 \x01(\r\x12\x15\n\rpm25_standard\x18\x02 \x01(\r\x12\x16\n\x0epm100_standard\x18\x03 \x01(\r\x12\x1a\n\x12pm10_environmental\x18\x04 \x01(\r\x12\x1a\n\x12pm25_environmental\x18\x05 \x01(\r\x12\x1b\n\x13pm100_environmental\x18\x06 \x01(\r\x12\x16\n\x0eparticles_03um\x18\x07 \x01(\r\x12\x16\n\x0eparticles_05um\x18\x08 \x01(\r\x12\x16\n\x0eparticles_10um\x18\t \x01(\r\x12\x16\n\x0eparticles_25um\x18\n \x01(\r\x12\x16\n\x0eparticles_50um\x18\x0b \x01(\r\x12\x17\n\x0fparticles_100um\x18\x0c \x01(\r\"\x89\x02\n\tTelemetry\x12\x0c\n\x04time\x18\x01 \x01(\x07\x12\x33\n\x0e\x64\x65vice_metrics\x18\x02 \x01(\x0b\x32\x19.meshtastic.DeviceMetricsH\x00\x12=\n\x13\x65nvironment_metrics\x18\x03 \x01(\x0b\x32\x1e.meshtastic.EnvironmentMetricsH\x00\x12<\n\x13\x61ir_quality_metrics\x18\x04 \x01(\x0b\x32\x1d.meshtastic.AirQualityMetricsH\x00\x12\x31\n\rpower_metrics\x18\x05 \x01(\x0b\x32\x18.meshtastic.PowerMetricsH\x00\x42\t\n\x07variant*\xcb\x02\n\x13TelemetrySensorType\x12\x10\n\x0cSENSOR_UNSET\x10\x00\x12\n\n\x06\x42ME280\x10\x01\x12\n\n\x06\x42ME680\x10\x02\x12\x0b\n\x07MCP9808\x10\x03\x12\n\n\x06INA260\x10\x04\x12\n\n\x06INA219\x10\x05\x12\n\n\x06\x42MP280\x10\x06\x12\t\n\x05SHTC3\x10\x07\x12\t\n\x05LPS22\x10\x08\x12\x0b\n\x07QMC6310\x10\t\x12\x0b\n\x07QMI8658\x10\n\x12\x0c\n\x08QMC5883L\x10\x0b\x12\t\n\x05SHT31\x10\x0c\x12\x0c\n\x08PMSA003I\x10\r\x12\x0b\n\x07INA3221\x10\x0e\x12\n\n\x06\x42MP085\x10\x0f\x12\x0c\n\x08RCWL9620\x10\x10\x12\t\n\x05SHT4X\x10\x11\x12\x0c\n\x08VEML7700\x10\x12\x12\x0c\n\x08MLX90632\x10\x13\x12\x0b\n\x07OPT3001\x10\x14\x12\x0c\n\x08LTR390UV\x10\x15\x12\x0e\n\nTSL25911FN\x10\x16\x12\t\n\x05\x41HT10\x10\x17\x42\x64\n\x13\x63om.geeksville.meshB\x0fTelemetryProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.telemetry_pb2', globals())
|
||||
@@ -22,7 +22,7 @@ if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
DESCRIPTOR._options = None
|
||||
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\017TelemetryProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||
_TELEMETRYSENSORTYPE._serialized_start=1129
|
||||
_TELEMETRYSENSORTYPE._serialized_end=1449
|
||||
_TELEMETRYSENSORTYPE._serialized_end=1460
|
||||
_DEVICEMETRICS._serialized_start=43
|
||||
_DEVICEMETRICS._serialized_end=172
|
||||
_ENVIRONMENTMETRICS._serialized_start=175
|
||||
|
||||
@@ -114,6 +114,10 @@ class _TelemetrySensorTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wra
|
||||
"""
|
||||
AMS TSL25911FN RGB Light Sensor
|
||||
"""
|
||||
AHT10: _TelemetrySensorType.ValueType # 23
|
||||
"""
|
||||
AHT10 Integrated temperature and humidity sensor
|
||||
"""
|
||||
|
||||
class TelemetrySensorType(_TelemetrySensorType, metaclass=_TelemetrySensorTypeEnumTypeWrapper):
|
||||
"""
|
||||
@@ -212,6 +216,10 @@ TSL25911FN: TelemetrySensorType.ValueType # 22
|
||||
"""
|
||||
AMS TSL25911FN RGB Light Sensor
|
||||
"""
|
||||
AHT10: TelemetrySensorType.ValueType # 23
|
||||
"""
|
||||
AHT10 Integrated temperature and humidity sensor
|
||||
"""
|
||||
global___TelemetrySensorType = TelemetrySensorType
|
||||
|
||||
@typing_extensions.final
|
||||
|
||||
@@ -724,11 +724,66 @@ def test_main_sendtext_with_dest(mock_findPorts, mock_serial, mocked_open, mock_
|
||||
assert re.search(r"Warning: There were no self.nodes.", caplog.text, re.MULTILINE)
|
||||
assert err == ""
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_main_removeposition_invalid(capsys):
|
||||
"""Test --remove-position with an invalid dest"""
|
||||
sys.argv = ["", "--remove-position", "--dest", "!12345678"]
|
||||
mt_config.args = sys.argv
|
||||
iface = MagicMock(autospec=SerialInterface)
|
||||
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo:
|
||||
main()
|
||||
out, err = capsys.readouterr()
|
||||
assert re.search(r"Connected to radio", out, re.MULTILINE)
|
||||
assert re.search(r"remote nodes is not supported", out, re.MULTILINE)
|
||||
assert err == ""
|
||||
mo.assert_called()
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_main_setlat_invalid(capsys):
|
||||
"""Test --setlat with an invalid dest"""
|
||||
sys.argv = ["", "--setlat", "37.5", "--dest", "!12345678"]
|
||||
mt_config.args = sys.argv
|
||||
iface = MagicMock(autospec=SerialInterface)
|
||||
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo:
|
||||
main()
|
||||
out, err = capsys.readouterr()
|
||||
assert re.search(r"Connected to radio", out, re.MULTILINE)
|
||||
assert re.search(r"remote nodes is not supported", out, re.MULTILINE)
|
||||
assert err == ""
|
||||
mo.assert_called()
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_main_removeposition(capsys):
|
||||
"""Test --remove-position"""
|
||||
sys.argv = ["", "--remove-position"]
|
||||
mt_config.args = sys.argv
|
||||
|
||||
mocked_node = MagicMock(autospec=Node)
|
||||
|
||||
def mock_removeFixedPosition():
|
||||
print("inside mocked removeFixedPosition")
|
||||
|
||||
mocked_node.removeFixedPosition.side_effect = mock_removeFixedPosition
|
||||
|
||||
iface = MagicMock(autospec=SerialInterface)
|
||||
iface.localNode = mocked_node
|
||||
|
||||
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo:
|
||||
main()
|
||||
out, err = capsys.readouterr()
|
||||
assert re.search(r"Connected to radio", out, re.MULTILINE)
|
||||
assert re.search(r"Removing fixed position", out, re.MULTILINE)
|
||||
assert re.search(r"inside mocked removeFixedPosition", out, re.MULTILINE)
|
||||
assert err == ""
|
||||
mo.assert_called()
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_main_setlat(capsys):
|
||||
"""Test --sendlat"""
|
||||
"""Test --setlat"""
|
||||
sys.argv = ["", "--setlat", "37.5"]
|
||||
mt_config.args = sys.argv
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import re
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from hypothesis import given, strategies as st
|
||||
|
||||
from meshtastic.supported_device import SupportedDevice
|
||||
from meshtastic.mesh_pb2 import MyNodeInfo
|
||||
@@ -33,6 +34,7 @@ from meshtastic.util import (
|
||||
stripnl,
|
||||
support_info,
|
||||
message_to_json,
|
||||
Acknowledgment
|
||||
)
|
||||
|
||||
|
||||
@@ -63,6 +65,7 @@ def test_fromStr():
|
||||
assert fromStr("123") == 123
|
||||
assert fromStr("abc") == "abc"
|
||||
assert fromStr("123456789") == 123456789
|
||||
assert fromStr("base64:Zm9vIGJhciBiYXo=") == b"foo bar baz"
|
||||
|
||||
|
||||
@pytest.mark.unitslow
|
||||
@@ -555,3 +558,39 @@ def test_message_to_json_shows_all():
|
||||
actual = json.loads(message_to_json(MyNodeInfo()))
|
||||
expected = { "myNodeNum": 0, "rebootCount": 0, "minAppVersion": 0 }
|
||||
assert actual == expected
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_acknowledgement_reset():
|
||||
"""
|
||||
Test that the reset method can set all fields back to False
|
||||
"""
|
||||
test_ack_obj = Acknowledgment()
|
||||
# everything's set to False; let's set it to True to get a good test
|
||||
test_ack_obj.receivedAck = True
|
||||
test_ack_obj.receivedNak = True
|
||||
test_ack_obj.receivedImplAck = True
|
||||
test_ack_obj.receivedTraceRoute = True
|
||||
test_ack_obj.receivedTelemetry = True
|
||||
test_ack_obj.receivedPosition = True
|
||||
test_ack_obj.reset()
|
||||
assert test_ack_obj.receivedAck is False
|
||||
assert test_ack_obj.receivedNak is False
|
||||
assert test_ack_obj.receivedImplAck is False
|
||||
assert test_ack_obj.receivedTraceRoute is False
|
||||
assert test_ack_obj.receivedTelemetry is False
|
||||
assert test_ack_obj.receivedPosition is False
|
||||
|
||||
@given(a_string=st.text(
|
||||
alphabet=st.characters(
|
||||
codec='ascii',
|
||||
min_codepoint=0x5F,
|
||||
max_codepoint=0x7A,
|
||||
exclude_characters=r'`',
|
||||
)).filter(
|
||||
lambda x: x not in [''] and x[0] not in "_" and x[-1] not in '_' and not re.search(r'__', x)
|
||||
))
|
||||
def test_roundtrip_snake_to_camel_camel_to_snake(a_string):
|
||||
"""Test that snake_to_camel and camel_to_snake roundtrip each other"""
|
||||
value0 = snake_to_camel(a_string=a_string)
|
||||
value1 = camel_to_snake(a_string=value0)
|
||||
assert a_string == value1, (a_string, value1)
|
||||
|
||||
@@ -14,6 +14,7 @@ from queue import Queue
|
||||
from typing import List, NoReturn, Union
|
||||
|
||||
from google.protobuf.json_format import MessageToJson
|
||||
from google.protobuf.message import Message
|
||||
|
||||
import packaging.version as pkg_version
|
||||
import requests
|
||||
@@ -103,7 +104,7 @@ def pskToString(psk: bytes):
|
||||
return "secret"
|
||||
|
||||
|
||||
def stripnl(s):
|
||||
def stripnl(s) -> str:
|
||||
"""Remove newlines from a string (and remove extra whitespace)"""
|
||||
s = str(s).replace("\n", " ")
|
||||
return " ".join(s.split())
|
||||
@@ -628,7 +629,7 @@ def check_if_newer_version():
|
||||
return pypi_version
|
||||
|
||||
|
||||
def message_to_json(message, multiline=False):
|
||||
def message_to_json(message: Message, multiline: bool=False) -> str:
|
||||
"""Return protobuf message as JSON. Always print all fields, even when not present in data."""
|
||||
json = MessageToJson(message, always_print_fields_with_no_presence=True)
|
||||
return stripnl(json) if not multiline else json
|
||||
|
||||
Submodule protobufs updated: 5cfadd1489...a45a6154d0
@@ -14,6 +14,7 @@ autopep8
|
||||
pylint
|
||||
pytest
|
||||
pytest-cov
|
||||
hypothesis
|
||||
pyyaml
|
||||
pytap2
|
||||
pdoc3
|
||||
@@ -22,7 +23,7 @@ bleak
|
||||
packaging
|
||||
mypy
|
||||
mypy-protobuf
|
||||
types-protobuf
|
||||
types-protobuf>=5.26.0
|
||||
types-tabulate
|
||||
types-requests
|
||||
types-setuptools
|
||||
|
||||
2
setup.py
2
setup.py
@@ -13,7 +13,7 @@ with open("README.md", "r") as fh:
|
||||
# This call to setup() does all the work
|
||||
setup(
|
||||
name="meshtastic",
|
||||
version="2.3.9",
|
||||
version="2.3.10",
|
||||
description="Python API & client shell for talking to Meshtastic devices",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
|
||||
Reference in New Issue
Block a user