From 9858fa19765e03b542c3abeb7b64f7d0d59c861e Mon Sep 17 00:00:00 2001 From: GUVWAF Date: Mon, 21 Aug 2023 20:56:59 +0200 Subject: [PATCH 1/3] Add `request-telemetry` option --- meshtastic/__main__.py | 15 ++++++++ meshtastic/mesh_interface.py | 71 +++++++++++++++++++++++++++++++++++- meshtastic/util.py | 13 ++++++- 3 files changed, 97 insertions(+), 2 deletions(-) diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index d7a282f..f9d319b 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -412,6 +412,13 @@ def onConnected(interface): print(f"Sending traceroute request to {dest} (this could take a while)") interface.sendTraceRoute(dest, hopLimit) + if args.request_telemetry: + if args.dest == BROADCAST_ADDR: + meshtastic.util.our_exit("Warning: Must use a destination node ID.") + else: + print(f"Sending telemetry request to {args.dest} (this could take a while)") + interface.sendTelemetry(destinationId=args.dest, wantResponse=True) + if args.gpio_wrb or args.gpio_rd or args.gpio_watch: if args.dest == BROADCAST_ADDR: meshtastic.util.our_exit("Warning: Must use a destination node ID.") @@ -1188,6 +1195,14 @@ def initParser(): "Only nodes that have the encryption key can be traced.", ) + parser.add_argument( + "--request-telemetry", + help="Request telemetry from a node." + "You need pass the destination ID as argument with '--dest'." + "For repeaters, the nodeNum is required.", + action="store_true", + ) + parser.add_argument( "--ack", help="Use in combination with --sendtext to wait for an acknowledgment.", diff --git a/meshtastic/mesh_interface.py b/meshtastic/mesh_interface.py index 3900241..3b16e46 100644 --- a/meshtastic/mesh_interface.py +++ b/meshtastic/mesh_interface.py @@ -18,7 +18,7 @@ from pubsub import pub from tabulate import tabulate import meshtastic.node -from meshtastic import mesh_pb2, portnums_pb2 +from meshtastic import mesh_pb2, portnums_pb2, telemetry_pb2 from meshtastic.__init__ import ( BROADCAST_ADDR, BROADCAST_NUM, @@ -409,6 +409,69 @@ class MeshInterface: self._acknowledgment.receivedTraceRoute = True + def sendTelemetry(self, destinationId=BROADCAST_ADDR, wantResponse=False): + """Send telemetry and optionally ask for a response""" + r = telemetry_pb2.Telemetry() + + node = next(n for n in self.nodes.values() if n["num"] == self.localNode.nodeNum) + if node is not None: + metrics = node.get("deviceMetrics") + if metrics: + batteryLevel = metrics.get("batteryLevel") + if batteryLevel is not None: + r.device_metrics.battery_level = batteryLevel + voltage = metrics.get("voltage") + if voltage is not None: + r.device_metrics.voltage = voltage + channel_utilization = metrics.get("channelUtilization") + if channel_utilization is not None: + r.device_metrics.channel_utilization = channel_utilization + air_util_tx = metrics.get("airUtilTx") + if air_util_tx is not None: + r.device_metrics.air_util_tx = air_util_tx + + if wantResponse: + onResponse = self.onResponseTelemetry + else: + onResponse = None + + if destinationId.startswith("!"): + destinationId = int(destinationId[1:], 16) + else: + destinationId = int(destinationId) + + self.sendData( + r, + destinationId=destinationId, + portNum=portnums_pb2.PortNum.TELEMETRY_APP, + wantResponse=wantResponse, + onResponse=onResponse, + ) + self.waitForTelemetry() + + def onResponseTelemetry(self, p): + """on response for telemetry""" + if p["decoded"]["portnum"] == 'TELEMETRY_APP': + self._acknowledgment.receivedTelemetry = True + telemetry = telemetry_pb2.Telemetry() + telemetry.ParseFromString(p["decoded"]["payload"]) + + print("Telemetry received:") + if telemetry.device_metrics.battery_level is not None: + print(f"Battery level: {telemetry.device_metrics.battery_level:.2f}%") + if telemetry.device_metrics.voltage is not None: + print(f"Voltage: {telemetry.device_metrics.voltage:.2f} V") + if telemetry.device_metrics.channel_utilization is not None: + print( + f"Total channel utilization: {telemetry.device_metrics.channel_utilization:.2f}%" + ) + if telemetry.device_metrics.air_util_tx is not None: + print(f"Transmit air utilization: {telemetry.device_metrics.air_util_tx:.2f}%") + + elif p["decoded"]["portnum"] == 'ROUTING_APP': + if p["decoded"]["routing"]["errorReason"] == 'NO_RESPONSE': + our_exit("No response from node. At least firmware 2.1.22 is required on the destination node.") + def _addResponseHandler(self, requestId, callback): self.responseHandlers[requestId] = ResponseHandler(callback) @@ -491,6 +554,12 @@ class MeshInterface: success = self._timeout.waitForTraceRoute(waitFactor, self._acknowledgment) if not success: raise Exception("Timed out waiting for traceroute") + + def waitForTelemetry(self): + """Wait for telemetry""" + success = self._timeout.waitForTelemetry(self._acknowledgment) + if not success: + raise Exception("Timed out waiting for telemetry") def getMyNodeInfo(self): """Get info about my node.""" diff --git a/meshtastic/util.py b/meshtastic/util.py index 083e9dd..7a5dd10 100644 --- a/meshtastic/util.py +++ b/meshtastic/util.py @@ -192,7 +192,16 @@ class Timeout: return True time.sleep(self.sleepInterval) return False - + + def waitForTelemetry(self, acknowledgment): + """Block until telemetry response is received. Returns True if telemetry response has been received.""" + self.reset() + while time.time() < self.expireTime: + if getattr(acknowledgment, "receivedTelemetry", None): + acknowledgment.reset() + return True + time.sleep(self.sleepInterval) + return False class Acknowledgment: "A class that records which type of acknowledgment was just received, if any." @@ -203,6 +212,7 @@ class Acknowledgment: self.receivedNak = False self.receivedImplAck = False self.receivedTraceRoute = False + self.receivedTelemetry = False def reset(self): """reset""" @@ -210,6 +220,7 @@ class Acknowledgment: self.receivedNak = False self.receivedImplAck = False self.receivedTraceRoute = False + self.receivedTelemetry = False class DeferredExecution: From 0192eed76e4c9edc97ec659c5c3acbd6e11b234b Mon Sep 17 00:00:00 2001 From: GUVWAF Date: Mon, 21 Aug 2023 21:04:30 +0200 Subject: [PATCH 2/3] Spacing --- meshtastic/__main__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index f9d319b..1d0c248 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -1197,8 +1197,8 @@ def initParser(): parser.add_argument( "--request-telemetry", - help="Request telemetry from a node." - "You need pass the destination ID as argument with '--dest'." + help="Request telemetry from a node. " + "You need pass the destination ID as argument with '--dest'. " "For repeaters, the nodeNum is required.", action="store_true", ) From d6ee815183d834176ff133b9e0e522bd9ddb30a7 Mon Sep 17 00:00:00 2001 From: GUVWAF Date: Mon, 21 Aug 2023 21:09:12 +0200 Subject: [PATCH 3/3] Only wait if response is wanted --- meshtastic/mesh_interface.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/meshtastic/mesh_interface.py b/meshtastic/mesh_interface.py index 3b16e46..bda2bdc 100644 --- a/meshtastic/mesh_interface.py +++ b/meshtastic/mesh_interface.py @@ -447,7 +447,8 @@ class MeshInterface: wantResponse=wantResponse, onResponse=onResponse, ) - self.waitForTelemetry() + if wantResponse: + self.waitForTelemetry() def onResponseTelemetry(self, p): """on response for telemetry"""