Compare commits

..

11 Commits
2.3.4 ... 2.3.5

Author SHA1 Message Date
Ian McEwen
ddad5f08b3 protobufs: v2.3.5 2024-04-14 12:30:14 -07:00
Ian McEwen
6e7933a3ce Fix my own pylint mistakes 2024-04-11 18:40:10 -07:00
Ian McEwen
f449ff9506 Add a variety of type annotations, primarily in mesh_interface 2024-04-11 18:28:01 -07:00
Ian McEwen
b280d0ba23 Merge pull request #541 from ianmcorvidae/removenode
Add --remove-node (fixes #514)
2024-04-08 15:05:11 -07:00
Ian McEwen
439b1ade2e Add --remove-node (fixes #514) 2024-04-08 14:58:15 -07:00
Ian McEwen
9f2b54eb98 Merge pull request #540 from ianmcorvidae/hopsaway
Show hops away when present in nodeDB (fixes #539)
2024-04-08 11:28:39 -07:00
Ian McEwen
278ca74a70 Show hops away when present in nodeDB (fixes #539) 2024-04-08 11:24:21 -07:00
Ian McEwen
1c93b7bd52 Revert "disable bump for post1 version"
This reverts commit 2d4be347e9.
2024-04-08 10:54:10 -07:00
Ian McEwen
2d4be347e9 disable bump for post1 version 2024-04-08 10:49:13 -07:00
Ian McEwen
26f024dc11 Set minimum version for protobuf to ensure presence of always_print_fields_with_no_presence 2024-04-08 10:47:01 -07:00
github-actions
2b8348ea05 bump version 2024-04-08 17:14:00 +00:00
9 changed files with 139 additions and 92 deletions

View File

@@ -365,6 +365,11 @@ def onConnected(interface):
waitForAckNak = True waitForAckNak = True
interface.getNode(args.dest, False).factoryReset() interface.getNode(args.dest, False).factoryReset()
if args.remove_node:
closeNow = True
waitForAckNak = True
interface.getNode(args.dest, False).removeNode(args.remove_node)
if args.reset_nodedb: if args.reset_nodedb:
closeNow = True closeNow = True
waitForAckNak = True waitForAckNak = True
@@ -1332,9 +1337,13 @@ def initParser():
action="store_true", action="store_true",
) )
group.add_argument(
"--remove-node",
help="Tell the destination node to remove a specific node from its DB, by node number or ID"
)
group.add_argument( group.add_argument(
"--reset-nodedb", "--reset-nodedb",
help="Tell the destination node clear its list of nodes", help="Tell the destination node to clear its list of nodes",
action="store_true", action="store_true",
) )

View File

@@ -10,6 +10,8 @@ import threading
import time import time
from datetime import datetime from datetime import datetime
from typing import Any, Callable, Dict, List, Optional, Union
import google.protobuf.json_format import google.protobuf.json_format
import timeago # type: ignore[import-untyped] import timeago # type: ignore[import-untyped]
from pubsub import pub # type: ignore[import-untyped] from pubsub import pub # type: ignore[import-untyped]
@@ -54,7 +56,7 @@ class MeshInterface:
self.message = message self.message = message
super().__init__(self.message) super().__init__(self.message)
def __init__(self, debugOut=None, noProto=False): def __init__(self, debugOut=None, noProto: bool=False) -> None:
"""Constructor """Constructor
Keyword Arguments: Keyword Arguments:
@@ -62,27 +64,27 @@ class MeshInterface:
link - just be a dumb serial client. link - just be a dumb serial client.
""" """
self.debugOut = debugOut self.debugOut = debugOut
self.nodes = None # FIXME self.nodes: Optional[Dict[str,Dict]] = None # FIXME
self.isConnected = threading.Event() self.isConnected: threading.Event = threading.Event()
self.noProto = noProto self.noProto: bool = noProto
self.localNode = meshtastic.node.Node(self, -1) # We fixup nodenum later self.localNode: meshtastic.node.Node = meshtastic.node.Node(self, -1) # We fixup nodenum later
self.myInfo = None # We don't have device info yet self.myInfo: Optional[mesh_pb2.MyNodeInfo] = None # We don't have device info yet
self.metadata = None # We don't have device metadata yet self.metadata: Optional[mesh_pb2.DeviceMetadata] = None # We don't have device metadata yet
self.responseHandlers = {} # A map from request ID to the handler self.responseHandlers: Dict[int,ResponseHandler] = {} # A map from request ID to the handler
self.failure = ( self.failure = (
None # If we've encountered a fatal exception it will be kept here None # If we've encountered a fatal exception it will be kept here
) )
self._timeout = Timeout() self._timeout: Timeout = Timeout()
self._acknowledgment = Acknowledgment() self._acknowledgment: Acknowledgment = Acknowledgment()
self.heartbeatTimer = None self.heartbeatTimer: Optional[threading.Timer] = None
random.seed() # FIXME, we should not clobber the random seedval here, instead tell user they must call it random.seed() # FIXME, we should not clobber the random seedval here, instead tell user they must call it
self.currentPacketId = random.randint(0, 0xFFFFFFFF) self.currentPacketId: int = random.randint(0, 0xFFFFFFFF)
self.nodesByNum = None self.nodesByNum: Optional[Dict[int, Dict]] = None
self.configId = None self.configId: Optional[int] = None
self.gotResponse = False # used in gpio read self.gotResponse: bool = False # used in gpio read
self.mask = None # used in gpio read and gpio watch self.mask: Optional[int] = None # used in gpio read and gpio watch
self.queueStatus = None self.queueStatus: Optional[mesh_pb2.QueueStatus] = None
self.queue = collections.OrderedDict() self.queue: collections.OrderedDict = collections.OrderedDict()
def close(self): def close(self):
"""Shutdown this interface""" """Shutdown this interface"""
@@ -103,7 +105,7 @@ class MeshInterface:
logging.error(f"Traceback: {traceback}") logging.error(f"Traceback: {traceback}")
self.close() self.close()
def showInfo(self, file=sys.stdout): # pylint: disable=W0613 def showInfo(self, file=sys.stdout) -> str: # pylint: disable=W0613
"""Show human readable summary about this object""" """Show human readable summary about this object"""
owner = f"Owner: {self.getLongName()} ({self.getShortName()})" owner = f"Owner: {self.getLongName()} ({self.getShortName()})"
myinfo = "" myinfo = ""
@@ -135,20 +137,20 @@ class MeshInterface:
print(infos) print(infos)
return infos return infos
def showNodes(self, includeSelf=True, file=sys.stdout): # pylint: disable=W0613 def showNodes(self, includeSelf: bool=True, file=sys.stdout) -> str: # pylint: disable=W0613
"""Show table summary of nodes in mesh""" """Show table summary of nodes in mesh"""
def formatFloat(value, precision=2, unit=""): def formatFloat(value, precision=2, unit="") -> Optional[str]:
"""Format a float value with precision.""" """Format a float value with precision."""
return f"{value:.{precision}f}{unit}" if value else None return f"{value:.{precision}f}{unit}" if value else None
def getLH(ts): def getLH(ts) -> Optional[str]:
"""Format last heard""" """Format last heard"""
return ( return (
datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S") if ts else None datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S") if ts else None
) )
def getTimeAgo(ts): def getTimeAgo(ts) -> Optional[str]:
"""Format how long ago have we heard from this node (aka timeago).""" """Format how long ago have we heard from this node (aka timeago)."""
return ( return (
timeago.format(datetime.fromtimestamp(ts), datetime.now()) timeago.format(datetime.fromtimestamp(ts), datetime.now())
@@ -156,7 +158,7 @@ class MeshInterface:
else None else None
) )
rows = [] rows: List[Dict[str, Any]] = []
if self.nodesByNum: if self.nodesByNum:
logging.debug(f"self.nodes:{self.nodes}") logging.debug(f"self.nodes:{self.nodes}")
for node in self.nodesByNum.values(): for node in self.nodesByNum.values():
@@ -208,6 +210,7 @@ class MeshInterface:
row.update( row.update(
{ {
"SNR": formatFloat(node.get("snr"), 2, " dB"), "SNR": formatFloat(node.get("snr"), 2, " dB"),
"Hops Away": node.get("hopsAway", "unknown"),
"Channel": node.get("channel"), "Channel": node.get("channel"),
"LastHeard": getLH(node.get("lastHeard")), "LastHeard": getLH(node.get("lastHeard")),
"Since": getTimeAgo(node.get("lastHeard")), "Since": getTimeAgo(node.get("lastHeard")),
@@ -224,7 +227,7 @@ class MeshInterface:
print(table) print(table)
return table return table
def getNode(self, nodeId, requestChannels=True): def getNode(self, nodeId: str, requestChannels: bool=True) -> meshtastic.node.Node:
"""Return a node object which contains device settings and channel info""" """Return a node object which contains device settings and channel info"""
if nodeId in (LOCAL_ADDR, BROADCAST_ADDR): if nodeId in (LOCAL_ADDR, BROADCAST_ADDR):
return self.localNode return self.localNode
@@ -241,11 +244,11 @@ class MeshInterface:
def sendText( def sendText(
self, self,
text: str, text: str,
destinationId=BROADCAST_ADDR, destinationId: Union[int, str]=BROADCAST_ADDR,
wantAck=False, wantAck: bool=False,
wantResponse=False, wantResponse: bool=False,
onResponse=None, onResponse: Optional[Callable[[mesh_pb2.MeshPacket], Any]]=None,
channelIndex=0, channelIndex: int=0,
): ):
"""Send a utf8 string to some other node, if the node has a display it """Send a utf8 string to some other node, if the node has a display it
will also be shown on the device. will also be shown on the device.
@@ -280,12 +283,12 @@ class MeshInterface:
def sendData( def sendData(
self, self,
data, data,
destinationId=BROADCAST_ADDR, destinationId: Union[int, str]=BROADCAST_ADDR,
portNum=portnums_pb2.PortNum.PRIVATE_APP, portNum: portnums_pb2.PortNum.ValueType=portnums_pb2.PortNum.PRIVATE_APP,
wantAck=False, wantAck: bool=False,
wantResponse=False, wantResponse: bool=False,
onResponse=None, onResponse: Optional[Callable[[mesh_pb2.MeshPacket], Any]]=None,
channelIndex=0, channelIndex: int=0,
): ):
"""Send a data packet to some other node """Send a data packet to some other node
@@ -340,13 +343,13 @@ class MeshInterface:
def sendPosition( def sendPosition(
self, self,
latitude=0.0, latitude: float=0.0,
longitude=0.0, longitude: float=0.0,
altitude=0, altitude: int=0,
timeSec=0, timeSec: int=0,
destinationId=BROADCAST_ADDR, destinationId: Union[int, str]=BROADCAST_ADDR,
wantAck=False, wantAck: bool=False,
wantResponse=False, wantResponse: bool=False,
): ):
""" """
Send a position packet to some other node (normally a broadcast) Send a position packet to some other node (normally a broadcast)
@@ -373,8 +376,8 @@ class MeshInterface:
logging.debug(f"p.altitude:{p.altitude}") logging.debug(f"p.altitude:{p.altitude}")
if timeSec == 0: if timeSec == 0:
timeSec = time.time() # returns unix timestamp in seconds timeSec = int(time.time()) # returns unix timestamp in seconds
p.time = int(timeSec) p.time = timeSec
logging.debug(f"p.time:{p.time}") logging.debug(f"p.time:{p.time}")
return self.sendData( return self.sendData(
@@ -385,7 +388,7 @@ class MeshInterface:
wantResponse=wantResponse, wantResponse=wantResponse,
) )
def sendTraceRoute(self, dest, hopLimit): def sendTraceRoute(self, dest: Union[int, str], hopLimit: int):
"""Send the trace route""" """Send the trace route"""
r = mesh_pb2.RouteDiscovery() r = mesh_pb2.RouteDiscovery()
self.sendData( self.sendData(
@@ -396,7 +399,7 @@ class MeshInterface:
onResponse=self.onResponseTraceRoute, onResponse=self.onResponseTraceRoute,
) )
# extend timeout based on number of nodes, limit by configured hopLimit # extend timeout based on number of nodes, limit by configured hopLimit
waitFactor = min(len(self.nodes) - 1, hopLimit) waitFactor = min(len(self.nodes) - 1 if self.nodes else 0, hopLimit)
self.waitForTraceRoute(waitFactor) self.waitForTraceRoute(waitFactor)
def onResponseTraceRoute(self, p): def onResponseTraceRoute(self, p):
@@ -479,10 +482,10 @@ class MeshInterface:
if p["decoded"]["routing"]["errorReason"] == 'NO_RESPONSE': if p["decoded"]["routing"]["errorReason"] == 'NO_RESPONSE':
our_exit("No response from node. At least firmware 2.1.22 is required on the destination node.") our_exit("No response from node. At least firmware 2.1.22 is required on the destination node.")
def _addResponseHandler(self, requestId, callback): def _addResponseHandler(self, requestId: int, callback: Callable):
self.responseHandlers[requestId] = ResponseHandler(callback) self.responseHandlers[requestId] = ResponseHandler(callback)
def _sendPacket(self, meshPacket, destinationId=BROADCAST_ADDR, wantAck=False): def _sendPacket(self, meshPacket: mesh_pb2.MeshPacket, destinationId: Union[int,str]=BROADCAST_ADDR, wantAck: bool=False):
"""Send a MeshPacket to the specified node (or if unspecified, broadcast). """Send a MeshPacket to the specified node (or if unspecified, broadcast).
You probably don't want this - use sendData instead. You probably don't want this - use sendData instead.
@@ -496,7 +499,7 @@ class MeshInterface:
toRadio = mesh_pb2.ToRadio() toRadio = mesh_pb2.ToRadio()
nodeNum = 0 nodeNum: int = 0
if destinationId is None: if destinationId is None:
our_exit("Warning: destinationId must not be None") our_exit("Warning: destinationId must not be None")
elif isinstance(destinationId, int): elif isinstance(destinationId, int):
@@ -514,8 +517,9 @@ class MeshInterface:
else: else:
if self.nodes: if self.nodes:
node = self.nodes.get(destinationId) node = self.nodes.get(destinationId)
if not node: if node is None:
our_exit(f"Warning: NodeId {destinationId} not found in DB") our_exit(f"Warning: NodeId {destinationId} not found in DB")
else:
nodeNum = node["num"] nodeNum = node["num"]
else: else:
logging.warning("Warning: There were no self.nodes.") logging.warning("Warning: There were no self.nodes.")
@@ -568,9 +572,9 @@ class MeshInterface:
if not success: if not success:
raise MeshInterface.MeshInterfaceError("Timed out waiting for telemetry") raise MeshInterface.MeshInterfaceError("Timed out waiting for telemetry")
def getMyNodeInfo(self): def getMyNodeInfo(self) -> Optional[Dict]:
"""Get info about my node.""" """Get info about my node."""
if self.myInfo is None: if self.myInfo is None or self.nodesByNum is None:
return None return None
logging.debug(f"self.nodesByNum:{self.nodesByNum}") logging.debug(f"self.nodesByNum:{self.nodesByNum}")
return self.nodesByNum.get(self.myInfo.my_node_num) return self.nodesByNum.get(self.myInfo.my_node_num)
@@ -607,7 +611,7 @@ class MeshInterface:
if self.failure: if self.failure:
raise self.failure raise self.failure
def _generatePacketId(self): def _generatePacketId(self) -> int:
"""Get a new unique packet ID""" """Get a new unique packet ID"""
if self.currentPacketId is None: if self.currentPacketId is None:
raise MeshInterface.MeshInterfaceError("Not connected yet, can not generate packet") raise MeshInterface.MeshInterfaceError("Not connected yet, can not generate packet")
@@ -669,18 +673,18 @@ class MeshInterface:
m.disconnect = True m.disconnect = True
self._sendToRadio(m) self._sendToRadio(m)
def _queueHasFreeSpace(self): def _queueHasFreeSpace(self) -> bool:
# We never got queueStatus, maybe the firmware is old # We never got queueStatus, maybe the firmware is old
if self.queueStatus is None: if self.queueStatus is None:
return True return True
return self.queueStatus.free > 0 return self.queueStatus.free > 0
def _queueClaim(self): def _queueClaim(self) -> None:
if self.queueStatus is None: if self.queueStatus is None:
return return
self.queueStatus.free -= 1 self.queueStatus.free -= 1
def _sendToRadio(self, toRadio): def _sendToRadio(self, toRadio: mesh_pb2.ToRadio) -> None:
"""Send a ToRadio protobuf to the device""" """Send a ToRadio protobuf to the device"""
if self.noProto: if self.noProto:
logging.warning( logging.warning(
@@ -729,18 +733,18 @@ class MeshInterface:
self.queue[packetId] = packet self.queue[packetId] = packet
# logging.warn("queue + resentQueue: " + " ".join(f'{k:08x}' for k in self.queue)) # logging.warn("queue + resentQueue: " + " ".join(f'{k:08x}' for k in self.queue))
def _sendToRadioImpl(self, toRadio): def _sendToRadioImpl(self, toRadio: mesh_pb2.ToRadio) -> None:
"""Send a ToRadio protobuf to the device""" """Send a ToRadio protobuf to the device"""
logging.error(f"Subclass must provide toradio: {toRadio}") logging.error(f"Subclass must provide toradio: {toRadio}")
def _handleConfigComplete(self): def _handleConfigComplete(self) -> None:
""" """
Done with initial config messages, now send regular MeshPackets Done with initial config messages, now send regular MeshPackets
to ask for settings and channels to ask for settings and channels
""" """
self.localNode.requestChannels() self.localNode.requestChannels()
def _handleQueueStatusFromRadio(self, queueStatus): def _handleQueueStatusFromRadio(self, queueStatus) -> None:
self.queueStatus = queueStatus self.queueStatus = queueStatus
logging.debug( logging.debug(
f"TX QUEUE free {queueStatus.free} of {queueStatus.maxlen}, res = {queueStatus.res}, id = {queueStatus.mesh_packet_id:08x} " f"TX QUEUE free {queueStatus.free} of {queueStatus.maxlen}, res = {queueStatus.res}, id = {queueStatus.mesh_packet_id:08x} "
@@ -891,7 +895,7 @@ class MeshInterface:
else: else:
logging.debug("Unexpected FromRadio payload") logging.debug("Unexpected FromRadio payload")
def _fixupPosition(self, position): def _fixupPosition(self, position: Dict) -> Dict:
"""Convert integer lat/lon into floats """Convert integer lat/lon into floats
Arguments: Arguments:

View File

@@ -5,6 +5,8 @@ import base64
import logging import logging
import time import time
from typing import Union
from meshtastic import admin_pb2, apponly_pb2, channel_pb2, localonly_pb2, portnums_pb2 from meshtastic import admin_pb2, apponly_pb2, channel_pb2, localonly_pb2, portnums_pb2
from meshtastic.util import ( from meshtastic.util import (
Timeout, Timeout,
@@ -603,6 +605,23 @@ class Node:
onResponse = self.onAckNak onResponse = self.onAckNak
return self._sendAdmin(p, onResponse=onResponse) return self._sendAdmin(p, onResponse=onResponse)
def removeNode(self, nodeId: Union[int, str]):
"""Tell the node to remove a specific node by ID"""
if isinstance(nodeId, str):
if nodeId.startswith("!"):
nodeId = int(nodeId[1:], 16)
else:
nodeId = int(nodeId)
p = admin_pb2.AdminMessage()
p.remove_by_nodenum = nodeId
if self == self.iface.localNode:
onResponse = None
else:
onResponse = self.onAckNak
return self._sendAdmin(p, onResponse=onResponse)
def resetNodeDb(self): def resetNodeDb(self):
"""Tell the node to reset its list of nodes.""" """Tell the node to reset its list of nodes."""
p = admin_pb2.AdminMessage() p = admin_pb2.AdminMessage()
@@ -740,9 +759,9 @@ class Node:
def _sendAdmin( def _sendAdmin(
self, self,
p: admin_pb2.AdminMessage, p: admin_pb2.AdminMessage,
wantResponse=True, wantResponse: bool=True,
onResponse=None, onResponse=None,
adminIndex=0, adminIndex: int=0,
): ):
"""Send an admin message to the specified node (or the local node if destNodeNum is zero)""" """Send an admin message to the specified node (or the local node if destNodeNum is zero)"""

View File

@@ -13,7 +13,7 @@ _sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1ameshtastic/telemetry.proto\x12\nmeshtastic\"i\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\"\x9b\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\"\x8c\x01\n\x0cPowerMetrics\x12\x13\n\x0b\x63h1_voltage\x18\x01 \x01(\x02\x12\x13\n\x0b\x63h1_current\x18\x02 \x01(\x02\x12\x13\n\x0b\x63h2_voltage\x18\x03 \x01(\x02\x12\x13\n\x0b\x63h2_current\x18\x04 \x01(\x02\x12\x13\n\x0b\x63h3_voltage\x18\x05 \x01(\x02\x12\x13\n\x0b\x63h3_current\x18\x06 \x01(\x02\"\xbf\x02\n\x11\x41irQualityMetrics\x12\x15\n\rpm10_standard\x18\x01 \x01(\r\x12\x15\n\rpm25_standard\x18\x02 \x01(\r\x12\x16\n\x0epm100_standard\x18\x03 \x01(\r\x12\x1a\n\x12pm10_environmental\x18\x04 \x01(\r\x12\x1a\n\x12pm25_environmental\x18\x05 \x01(\r\x12\x1b\n\x13pm100_environmental\x18\x06 \x01(\r\x12\x16\n\x0eparticles_03um\x18\x07 \x01(\r\x12\x16\n\x0eparticles_05um\x18\x08 \x01(\r\x12\x16\n\x0eparticles_10um\x18\t \x01(\r\x12\x16\n\x0eparticles_25um\x18\n \x01(\r\x12\x16\n\x0eparticles_50um\x18\x0b \x01(\r\x12\x17\n\x0fparticles_100um\x18\x0c \x01(\r\"\x89\x02\n\tTelemetry\x12\x0c\n\x04time\x18\x01 \x01(\x07\x12\x33\n\x0e\x64\x65vice_metrics\x18\x02 \x01(\x0b\x32\x19.meshtastic.DeviceMetricsH\x00\x12=\n\x13\x65nvironment_metrics\x18\x03 \x01(\x0b\x32\x1e.meshtastic.EnvironmentMetricsH\x00\x12<\n\x13\x61ir_quality_metrics\x18\x04 \x01(\x0b\x32\x1d.meshtastic.AirQualityMetricsH\x00\x12\x31\n\rpower_metrics\x18\x05 \x01(\x0b\x32\x18.meshtastic.PowerMetricsH\x00\x42\t\n\x07variant*\xe0\x01\n\x13TelemetrySensorType\x12\x10\n\x0cSENSOR_UNSET\x10\x00\x12\n\n\x06\x42ME280\x10\x01\x12\n\n\x06\x42ME680\x10\x02\x12\x0b\n\x07MCP9808\x10\x03\x12\n\n\x06INA260\x10\x04\x12\n\n\x06INA219\x10\x05\x12\n\n\x06\x42MP280\x10\x06\x12\t\n\x05SHTC3\x10\x07\x12\t\n\x05LPS22\x10\x08\x12\x0b\n\x07QMC6310\x10\t\x12\x0b\n\x07QMI8658\x10\n\x12\x0c\n\x08QMC5883L\x10\x0b\x12\t\n\x05SHT31\x10\x0c\x12\x0c\n\x08PMSA003I\x10\r\x12\x0b\n\x07INA3221\x10\x0e\x12\n\n\x06\x42MP085\x10\x0f\x42\x64\n\x13\x63om.geeksville.meshB\x0fTelemetryProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3') DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1ameshtastic/telemetry.proto\x12\nmeshtastic\"\x81\x01\n\rDeviceMetrics\x12\x15\n\rbattery_level\x18\x01 \x01(\r\x12\x0f\n\x07voltage\x18\x02 \x01(\x02\x12\x1b\n\x13\x63hannel_utilization\x18\x03 \x01(\x02\x12\x13\n\x0b\x61ir_util_tx\x18\x04 \x01(\x02\x12\x16\n\x0euptime_seconds\x18\x05 \x01(\r\"\xa8\x01\n\x12\x45nvironmentMetrics\x12\x13\n\x0btemperature\x18\x01 \x01(\x02\x12\x19\n\x11relative_humidity\x18\x02 \x01(\x02\x12\x1b\n\x13\x62\x61rometric_pressure\x18\x03 \x01(\x02\x12\x16\n\x0egas_resistance\x18\x04 \x01(\x02\x12\x0f\n\x07voltage\x18\x05 \x01(\x02\x12\x0f\n\x07\x63urrent\x18\x06 \x01(\x02\x12\x0b\n\x03iaq\x18\x07 \x01(\r\"\x8c\x01\n\x0cPowerMetrics\x12\x13\n\x0b\x63h1_voltage\x18\x01 \x01(\x02\x12\x13\n\x0b\x63h1_current\x18\x02 \x01(\x02\x12\x13\n\x0b\x63h2_voltage\x18\x03 \x01(\x02\x12\x13\n\x0b\x63h2_current\x18\x04 \x01(\x02\x12\x13\n\x0b\x63h3_voltage\x18\x05 \x01(\x02\x12\x13\n\x0b\x63h3_current\x18\x06 \x01(\x02\"\xbf\x02\n\x11\x41irQualityMetrics\x12\x15\n\rpm10_standard\x18\x01 \x01(\r\x12\x15\n\rpm25_standard\x18\x02 \x01(\r\x12\x16\n\x0epm100_standard\x18\x03 \x01(\r\x12\x1a\n\x12pm10_environmental\x18\x04 \x01(\r\x12\x1a\n\x12pm25_environmental\x18\x05 \x01(\r\x12\x1b\n\x13pm100_environmental\x18\x06 \x01(\r\x12\x16\n\x0eparticles_03um\x18\x07 \x01(\r\x12\x16\n\x0eparticles_05um\x18\x08 \x01(\r\x12\x16\n\x0eparticles_10um\x18\t \x01(\r\x12\x16\n\x0eparticles_25um\x18\n \x01(\r\x12\x16\n\x0eparticles_50um\x18\x0b \x01(\r\x12\x17\n\x0fparticles_100um\x18\x0c \x01(\r\"\x89\x02\n\tTelemetry\x12\x0c\n\x04time\x18\x01 \x01(\x07\x12\x33\n\x0e\x64\x65vice_metrics\x18\x02 \x01(\x0b\x32\x19.meshtastic.DeviceMetricsH\x00\x12=\n\x13\x65nvironment_metrics\x18\x03 \x01(\x0b\x32\x1e.meshtastic.EnvironmentMetricsH\x00\x12<\n\x13\x61ir_quality_metrics\x18\x04 \x01(\x0b\x32\x1d.meshtastic.AirQualityMetricsH\x00\x12\x31\n\rpower_metrics\x18\x05 \x01(\x0b\x32\x18.meshtastic.PowerMetricsH\x00\x42\t\n\x07variant*\xe0\x01\n\x13TelemetrySensorType\x12\x10\n\x0cSENSOR_UNSET\x10\x00\x12\n\n\x06\x42ME280\x10\x01\x12\n\n\x06\x42ME680\x10\x02\x12\x0b\n\x07MCP9808\x10\x03\x12\n\n\x06INA260\x10\x04\x12\n\n\x06INA219\x10\x05\x12\n\n\x06\x42MP280\x10\x06\x12\t\n\x05SHTC3\x10\x07\x12\t\n\x05LPS22\x10\x08\x12\x0b\n\x07QMC6310\x10\t\x12\x0b\n\x07QMI8658\x10\n\x12\x0c\n\x08QMC5883L\x10\x0b\x12\t\n\x05SHT31\x10\x0c\x12\x0c\n\x08PMSA003I\x10\r\x12\x0b\n\x07INA3221\x10\x0e\x12\n\n\x06\x42MP085\x10\x0f\x42\x64\n\x13\x63om.geeksville.meshB\x0fTelemetryProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.telemetry_pb2', globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.telemetry_pb2', globals())
@@ -21,16 +21,16 @@ if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None DESCRIPTOR._options = None
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\017TelemetryProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000' DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\017TelemetryProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
_TELEMETRYSENSORTYPE._serialized_start=1041 _TELEMETRYSENSORTYPE._serialized_start=1079
_TELEMETRYSENSORTYPE._serialized_end=1265 _TELEMETRYSENSORTYPE._serialized_end=1303
_DEVICEMETRICS._serialized_start=42 _DEVICEMETRICS._serialized_start=43
_DEVICEMETRICS._serialized_end=147 _DEVICEMETRICS._serialized_end=172
_ENVIRONMENTMETRICS._serialized_start=150 _ENVIRONMENTMETRICS._serialized_start=175
_ENVIRONMENTMETRICS._serialized_end=305 _ENVIRONMENTMETRICS._serialized_end=343
_POWERMETRICS._serialized_start=308 _POWERMETRICS._serialized_start=346
_POWERMETRICS._serialized_end=448 _POWERMETRICS._serialized_end=486
_AIRQUALITYMETRICS._serialized_start=451 _AIRQUALITYMETRICS._serialized_start=489
_AIRQUALITYMETRICS._serialized_end=770 _AIRQUALITYMETRICS._serialized_end=808
_TELEMETRY._serialized_start=773 _TELEMETRY._serialized_start=811
_TELEMETRY._serialized_end=1038 _TELEMETRY._serialized_end=1076
# @@protoc_insertion_point(module_scope) # @@protoc_insertion_point(module_scope)

View File

@@ -170,6 +170,7 @@ class DeviceMetrics(google.protobuf.message.Message):
VOLTAGE_FIELD_NUMBER: builtins.int VOLTAGE_FIELD_NUMBER: builtins.int
CHANNEL_UTILIZATION_FIELD_NUMBER: builtins.int CHANNEL_UTILIZATION_FIELD_NUMBER: builtins.int
AIR_UTIL_TX_FIELD_NUMBER: builtins.int AIR_UTIL_TX_FIELD_NUMBER: builtins.int
UPTIME_SECONDS_FIELD_NUMBER: builtins.int
battery_level: builtins.int battery_level: builtins.int
""" """
0-100 (>100 means powered) 0-100 (>100 means powered)
@@ -186,6 +187,10 @@ class DeviceMetrics(google.protobuf.message.Message):
""" """
Percent of airtime for transmission used within the last hour. Percent of airtime for transmission used within the last hour.
""" """
uptime_seconds: builtins.int
"""
How long the device has been running since the last reboot (in seconds)
"""
def __init__( def __init__(
self, self,
*, *,
@@ -193,8 +198,9 @@ class DeviceMetrics(google.protobuf.message.Message):
voltage: builtins.float = ..., voltage: builtins.float = ...,
channel_utilization: builtins.float = ..., channel_utilization: builtins.float = ...,
air_util_tx: builtins.float = ..., air_util_tx: builtins.float = ...,
uptime_seconds: builtins.int = ...,
) -> None: ... ) -> None: ...
def ClearField(self, field_name: typing_extensions.Literal["air_util_tx", b"air_util_tx", "battery_level", b"battery_level", "channel_utilization", b"channel_utilization", "voltage", b"voltage"]) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["air_util_tx", b"air_util_tx", "battery_level", b"battery_level", "channel_utilization", b"channel_utilization", "uptime_seconds", b"uptime_seconds", "voltage", b"voltage"]) -> None: ...
global___DeviceMetrics = DeviceMetrics global___DeviceMetrics = DeviceMetrics
@@ -212,6 +218,7 @@ class EnvironmentMetrics(google.protobuf.message.Message):
GAS_RESISTANCE_FIELD_NUMBER: builtins.int GAS_RESISTANCE_FIELD_NUMBER: builtins.int
VOLTAGE_FIELD_NUMBER: builtins.int VOLTAGE_FIELD_NUMBER: builtins.int
CURRENT_FIELD_NUMBER: builtins.int CURRENT_FIELD_NUMBER: builtins.int
IAQ_FIELD_NUMBER: builtins.int
temperature: builtins.float temperature: builtins.float
""" """
Temperature measured Temperature measured
@@ -236,6 +243,11 @@ class EnvironmentMetrics(google.protobuf.message.Message):
""" """
Current measured (To be depreciated in favor of PowerMetrics in Meshtastic 3.x) Current measured (To be depreciated in favor of PowerMetrics in Meshtastic 3.x)
""" """
iaq: builtins.int
"""
relative scale IAQ value as measured by Bosch BME680 . value 0-500.
Belongs to Air Quality but is not particle but VOC measurement. Other VOC values can also be put in here.
"""
def __init__( def __init__(
self, self,
*, *,
@@ -245,8 +257,9 @@ class EnvironmentMetrics(google.protobuf.message.Message):
gas_resistance: builtins.float = ..., gas_resistance: builtins.float = ...,
voltage: builtins.float = ..., voltage: builtins.float = ...,
current: builtins.float = ..., current: builtins.float = ...,
iaq: builtins.int = ...,
) -> None: ... ) -> None: ...
def ClearField(self, field_name: typing_extensions.Literal["barometric_pressure", b"barometric_pressure", "current", b"current", "gas_resistance", b"gas_resistance", "relative_humidity", b"relative_humidity", "temperature", b"temperature", "voltage", b"voltage"]) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["barometric_pressure", b"barometric_pressure", "current", b"current", "gas_resistance", b"gas_resistance", "iaq", b"iaq", "relative_humidity", b"relative_humidity", "temperature", b"temperature", "voltage", b"voltage"]) -> None: ...
global___EnvironmentMetrics = EnvironmentMetrics global___EnvironmentMetrics = EnvironmentMetrics

View File

@@ -11,6 +11,8 @@ import threading
import time import time
import traceback import traceback
from queue import Queue from queue import Queue
from typing import Union
from google.protobuf.json_format import MessageToJson from google.protobuf.json_format import MessageToJson
import packaging.version as pkg_version import packaging.version as pkg_version
@@ -153,16 +155,16 @@ class dotdict(dict):
class Timeout: class Timeout:
"""Timeout class""" """Timeout class"""
def __init__(self, maxSecs=20): def __init__(self, maxSecs: int=20):
self.expireTime = 0 self.expireTime: Union[int, float] = 0
self.sleepInterval = 0.1 self.sleepInterval: float = 0.1
self.expireTimeout = maxSecs self.expireTimeout: int = maxSecs
def reset(self): def reset(self):
"""Restart the waitForSet timer""" """Restart the waitForSet timer"""
self.expireTime = time.time() + self.expireTimeout self.expireTime = time.time() + self.expireTimeout
def waitForSet(self, target, attrs=()): def waitForSet(self, target, attrs=()) -> bool:
"""Block until the specified attributes are set. Returns True if config has been received.""" """Block until the specified attributes are set. Returns True if config has been received."""
self.reset() self.reset()
while time.time() < self.expireTime: while time.time() < self.expireTime:
@@ -173,7 +175,7 @@ class Timeout:
def waitForAckNak( def waitForAckNak(
self, acknowledgment, attrs=("receivedAck", "receivedNak", "receivedImplAck") self, acknowledgment, attrs=("receivedAck", "receivedNak", "receivedImplAck")
): ) -> bool:
"""Block until an ACK or NAK has been received. Returns True if ACK or NAK has been received.""" """Block until an ACK or NAK has been received. Returns True if ACK or NAK has been received."""
self.reset() self.reset()
while time.time() < self.expireTime: while time.time() < self.expireTime:
@@ -183,7 +185,7 @@ class Timeout:
time.sleep(self.sleepInterval) time.sleep(self.sleepInterval)
return False return False
def waitForTraceRoute(self, waitFactor, acknowledgment, attr="receivedTraceRoute"): def waitForTraceRoute(self, waitFactor, acknowledgment, attr="receivedTraceRoute") -> bool:
"""Block until traceroute response is received. Returns True if traceroute response has been received.""" """Block until traceroute response is received. Returns True if traceroute response has been received."""
self.expireTimeout *= waitFactor self.expireTimeout *= waitFactor
self.reset() self.reset()
@@ -194,7 +196,7 @@ class Timeout:
time.sleep(self.sleepInterval) time.sleep(self.sleepInterval)
return False return False
def waitForTelemetry(self, acknowledgment): def waitForTelemetry(self, acknowledgment) -> bool:
"""Block until telemetry response is received. Returns True if telemetry response has been received.""" """Block until telemetry response is received. Returns True if telemetry response has been received."""
self.reset() self.reset()
while time.time() < self.expireTime: while time.time() < self.expireTime:

View File

@@ -1,6 +1,6 @@
markdown markdown
pyserial pyserial
protobuf protobuf>=5.26.0
dotmap dotmap
pexpect pexpect
pyqrcode pyqrcode

View File

@@ -13,7 +13,7 @@ with open("README.md", "r") as fh:
# This call to setup() does all the work # This call to setup() does all the work
setup( setup(
name="meshtastic", name="meshtastic",
version="2.3.3", version="2.3.4",
description="Python API & client shell for talking to Meshtastic devices", description="Python API & client shell for talking to Meshtastic devices",
long_description=long_description, long_description=long_description,
long_description_content_type="text/markdown", long_description_content_type="text/markdown",
@@ -34,7 +34,7 @@ setup(
include_package_data=True, include_package_data=True,
install_requires=[ install_requires=[
"pyserial>=3.4", "pyserial>=3.4",
"protobuf>=3.13.0", "protobuf>=5.26.0",
"requests>=2.25.0", "requests>=2.25.0",
"pypubsub>=4.0.3", "pypubsub>=4.0.3",
"dotmap>=1.3.14", "dotmap>=1.3.14",