From f1670748c3d76f078755485a29977964fa27b64f Mon Sep 17 00:00:00 2001 From: tylerpieper Date: Thu, 8 Oct 2026 21:24:54 -0700 Subject: [PATCH 1/3] feat: Add --request-nodeinfo CLI flag --- meshtastic/__main__.py | 23 ++++++++++++++++++++ meshtastic/mesh_interface.py | 42 ++++++++++++++++++++++++++++++++++++ meshtastic/util.py | 12 +++++++++++ 3 files changed, 77 insertions(+) diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index ca1680a58..081ebac9e 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -642,6 +642,21 @@ def onConnected(interface): telemetryType=telemType, ) + if args.request_nodeinfo: + if args.dest == BROADCAST_ADDR: + meshtastic.util.our_exit("Warning: Must use a destination node ID.") + else: + channelIndex = mt_config.channel_index or 0 + if checkChannel(interface, channelIndex): + print( + f"Sending NodeInfo request to {args.dest} on channelIndex:{channelIndex} (this could take a while)" + ) + interface.requestNodeInfo( + destinationId=args.dest, + wantResponse=True, + channelIndex=channelIndex, + ) + if args.request_position: if args.dest == BROADCAST_ADDR: meshtastic.util.our_exit("Warning: Must use a destination node ID.") @@ -2267,6 +2282,14 @@ def addRemoteActionArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentPar metavar="TYPE", ) + group.add_argument( + "--request-nodeinfo", + help="Request NodeInfo (name, hardware model, etc) from a node. " + "You need to pass the destination ID as an argument with '--dest'. " + "For repeaters, the nodeNum is required.", + action="store_true", + ) + group.add_argument( "--request-position", help="Request the position from a node. " diff --git a/meshtastic/mesh_interface.py b/meshtastic/mesh_interface.py index 1a7dd6eae..da5152f2a 100644 --- a/meshtastic/mesh_interface.py +++ b/meshtastic/mesh_interface.py @@ -582,6 +582,35 @@ def sendData( p = self._sendPacket(meshPacket, destinationId, wantAck=wantAck, hopLimit=hopLimit, pkiEncrypted=pkiEncrypted, publicKey=publicKey) return p + def requestNodeInfo( + self, + destinationId: Union[int, str] = BROADCAST_ADDR, + wantResponse: bool = False, + channelIndex: int = 0, + hopLimit: Optional[int] = None, + ): + """Send a NodeInfo request and optionally ask for a response""" + u = mesh_pb2.User() + + if wantResponse: + onResponse = self.onResponseNodeInfo + else: + onResponse = None + + d = self.sendData( + u, + destinationId=destinationId, + portNum=portnums_pb2.PortNum.NODEINFO_APP, + wantResponse=wantResponse, + onResponse=onResponse, + channelIndex=channelIndex, + hopLimit=hopLimit, + ) + + if wantResponse: + self.waitForNodeInfo() + return d + def sendPosition( self, latitude: float = 0.0, @@ -634,6 +663,13 @@ def sendPosition( self.waitForPosition() return d + def onResponseNodeInfo(self, p: dict): + """on response for NodeInfo""" + if p["decoded"]["portnum"] == "NODEINFO_APP": + self._acknowledgment.receivedNodeInfo = True + if "user" in p["decoded"]: + print(f"NodeInfo received:\n{p['decoded']['user']}") + def onResponsePosition(self, p): """on response for position""" if p["decoded"]["portnum"] == "POSITION_APP": @@ -1067,6 +1103,12 @@ def waitForTelemetry(self): if not success: raise MeshInterface.MeshInterfaceError("Timed out waiting for telemetry") + def waitForNodeInfo(self): + """Wait for NodeInfo""" + success = self._timeout.waitForNodeInfo(self._acknowledgment) + if not success: + raise MeshInterface.MeshInterfaceError("Timed out waiting for NodeInfo") + def waitForPosition(self): """Wait for position""" success = self._timeout.waitForPosition(self._acknowledgment) diff --git a/meshtastic/util.py b/meshtastic/util.py index 7faa2d6c0..e127f930f 100644 --- a/meshtastic/util.py +++ b/meshtastic/util.py @@ -266,6 +266,16 @@ def waitForWaypoint(self, acknowledgment) -> bool: time.sleep(self.sleepInterval) return False + def waitForNodeInfo(self, acknowledgment) -> bool: + """Block until NodeInfo response is received. Returns True if NodeInfo response has been received.""" + self.reset() + while time.time() < self.expireTime: + if getattr(acknowledgment, "receivedNodeInfo", 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." @@ -278,6 +288,7 @@ def __init__(self) -> None: self.receivedTelemetry = False self.receivedPosition = False self.receivedWaypoint = False + self.receivedNodeInfo = False def reset(self) -> None: """reset""" @@ -288,6 +299,7 @@ def reset(self) -> None: self.receivedTelemetry = False self.receivedPosition = False self.receivedWaypoint = False + self.receivedNodeInfo = False class DeferredExecution: From 17e5543d3e432b46670e5b46d1b476deb4322ca0 Mon Sep 17 00:00:00 2001 From: tylerpieper Date: Fri, 9 Oct 2026 09:01:29 -0700 Subject: [PATCH 2/3] style: Clean up NodeInfo CLI output format --- meshtastic/mesh_interface.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/meshtastic/mesh_interface.py b/meshtastic/mesh_interface.py index da5152f2a..f9dc85489 100644 --- a/meshtastic/mesh_interface.py +++ b/meshtastic/mesh_interface.py @@ -668,7 +668,10 @@ def onResponseNodeInfo(self, p: dict): if p["decoded"]["portnum"] == "NODEINFO_APP": self._acknowledgment.receivedNodeInfo = True if "user" in p["decoded"]: - print(f"NodeInfo received:\n{p['decoded']['user']}") + print("NodeInfo received:") + for key, value in p["decoded"]["user"].items(): + if key != "raw": + print(f" {key}: {value}") def onResponsePosition(self, p): """on response for position""" From 09435706e3b117ed2c21973fd0490e465ba28953 Mon Sep 17 00:00:00 2001 From: tylerpieper Date: Fri, 9 Oct 2026 09:15:39 -0700 Subject: [PATCH 3/3] style: Remove indentation from NodeInfo output --- meshtastic/mesh_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshtastic/mesh_interface.py b/meshtastic/mesh_interface.py index f9dc85489..4845cd8a1 100644 --- a/meshtastic/mesh_interface.py +++ b/meshtastic/mesh_interface.py @@ -671,7 +671,7 @@ def onResponseNodeInfo(self, p: dict): print("NodeInfo received:") for key, value in p["decoded"]["user"].items(): if key != "raw": - print(f" {key}: {value}") + print(f"{key}: {value}") def onResponsePosition(self, p): """on response for position"""