From 9284a848f2ed78bf5e732882cdbf0ec1482a6627 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Fejoz?= Date: Wed, 18 Dec 2024 17:36:58 +0100 Subject: [PATCH 1/3] feat(waypoint): Waypoint creation/deletion Add methods to send (create or move), delete waypoint. Add an example script to create, move, delete waypoint. --- examples/waypoint.py | 56 +++++++++++++++++++++ meshtastic/mesh_interface.py | 94 ++++++++++++++++++++++++++++++++++++ 2 files changed, 150 insertions(+) create mode 100644 examples/waypoint.py diff --git a/examples/waypoint.py b/examples/waypoint.py new file mode 100644 index 0000000..57d2e90 --- /dev/null +++ b/examples/waypoint.py @@ -0,0 +1,56 @@ +"""Program to create and delete waypoint + To run: + python3 examples/waypoint.py --port /dev/ttyUSB0 create 45 test the_desc_2 '2024-12-18T23:05:23' 48.74 7.35 + python examples/waypoint.py delete 45 +""" + +import argparse +import datetime +import sys +import time + +import meshtastic +import meshtastic.serial_interface + +parser = argparse.ArgumentParser( + prog='waypoint', + description='Create and delete Meshtastic waypoint') +parser.add_argument('--port', default=None) +parser.add_argument('--debug', default=False, action='store_true') + +subparsers = parser.add_subparsers(dest='cmd') +parser_delete = subparsers.add_parser('delete', help='Delete a waypoint') +parser_delete.add_argument('id', help="id of the waypoint") + +parser_create = subparsers.add_parser('create', help='Create a new waypoint') +parser_create.add_argument('id', help="id of the waypoint") +parser_create.add_argument('name', help="name of the waypoint") +parser_create.add_argument('description', help="description of the waypoint") +parser_create.add_argument('expire', help="expiration date of the waypoint as interpreted by datetime.fromisoformat") +parser_create.add_argument('latitude', help="latitude of the waypoint") +parser_create.add_argument('longitude', help="longitude of the waypoint") + +args = parser.parse_args() +print(args) + +# By default will try to find a meshtastic device, +# otherwise provide a device path like /dev/ttyUSB0 +if args.debug: + d = sys.stderr +else: + d = None +with meshtastic.serial_interface.SerialInterface(args.port, debugOut=d) as iface: + if args.cmd == 'create': + p = iface.sendWaypoint( + id=int(args.id), + name=args.name, + description=args.description, + expire=int(datetime.datetime.fromisoformat(args.expire).timestamp()), + latitude=float(args.latitude), + longitude=float(args.longitude), + ) + else: + p = iface.deleteWaypoint(int(args.id)) + print(p) + +# iface.close() diff --git a/meshtastic/mesh_interface.py b/meshtastic/mesh_interface.py index 2da04ce..3f5a1b1 100644 --- a/meshtastic/mesh_interface.py +++ b/meshtastic/mesh_interface.py @@ -5,7 +5,9 @@ import collections import json import logging +import math import random +import secrets import sys import threading import time @@ -700,6 +702,98 @@ class MeshInterface: # pylint: disable=R0902 "No response from node. At least firmware 2.1.22 is required on the destination node." ) + def sendWaypoint( + self, + name, + description, + expire: int, + id: Optional[int] = None, + latitude: float = 0.0, + longitude: float = 0.0, + destinationId: Union[int, str] = BROADCAST_ADDR, + wantAck: bool = True, + wantResponse: bool = False, + channelIndex: int = 0, + ): + """ + Send a waypoint packet to some other node (normally a broadcast) + + Returns the sent packet. The id field will be populated in this packet and + can be used to track future message acks/naks. + """ + w = mesh_pb2.Waypoint() + w.name = name + w.description = description + w.expire = expire + if id is None: + seed = secrets.randbits(32) + w.id = math.floor(seed * math.pow(2, -32) * 1e9) + logging.debug(f"w.id:{w.id}") + else: + w.id = id + if latitude != 0.0: + w.latitude_i = int(latitude * 1e7) + logging.debug(f"w.latitude_i:{w.latitude_i}") + if longitude != 0.0: + w.longitude_i = int(longitude * 1e7) + logging.debug(f"w.longitude_i:{w.longitude_i}") + + if wantResponse: + onResponse = self.onResponseWaypoint + else: + onResponse = None + + d = self.sendData( + w, + destinationId, + portNum=portnums_pb2.PortNum.WAYPOINT_APP, + wantAck=wantAck, + wantResponse=wantResponse, + onResponse=onResponse, + channelIndex=channelIndex, + ) + if wantResponse: + self.waitForWaypoint() + return d + + def deleteWaypoint( + self, + id: int, + destinationId: Union[int, str] = BROADCAST_ADDR, + wantAck: bool = True, + wantResponse: bool = False, + channelIndex: int = 0, + ): + """ + Send a waypoint deletion packet to some other node (normally a broadcast) + + NB: The id must be the waypoint's id and not the id of the packet creation. + + Returns the sent packet. The id field will be populated in this packet and + can be used to track future message acks/naks. + """ + p = mesh_pb2.Waypoint() + p.id = id + p.expire = 0 + + if wantResponse: + onResponse = self.onResponseWaypoint + else: + onResponse = None + + d = self.sendData( + p, + destinationId, + portNum=portnums_pb2.PortNum.WAYPOINT_APP, + wantAck=wantAck, + wantResponse=wantResponse, + onResponse=onResponse, + channelIndex=channelIndex, + ) + if wantResponse: + self.waitForWaypoint() + return d + def _addResponseHandler( self, requestId: int, From 7cc18e9df627a8730fdd1777789230c239bf5e0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Fejoz?= Date: Sat, 21 Dec 2024 10:36:36 +0100 Subject: [PATCH 2/3] fix(waypoint): Missing methods Add missing methods callbacks regarding waypoints. --- examples/waypoint.py | 2 +- meshtastic/mesh_interface.py | 21 +++++++++++++++++++++ meshtastic/util.py | 12 ++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/examples/waypoint.py b/examples/waypoint.py index 57d2e90..1ce862d 100644 --- a/examples/waypoint.py +++ b/examples/waypoint.py @@ -1,7 +1,7 @@ """Program to create and delete waypoint To run: python3 examples/waypoint.py --port /dev/ttyUSB0 create 45 test the_desc_2 '2024-12-18T23:05:23' 48.74 7.35 - python examples/waypoint.py delete 45 + python3 examples/waypoint.py delete 45 """ import argparse diff --git a/meshtastic/mesh_interface.py b/meshtastic/mesh_interface.py index 3f5a1b1..5f7ba25 100644 --- a/meshtastic/mesh_interface.py +++ b/meshtastic/mesh_interface.py @@ -702,6 +702,19 @@ class MeshInterface: # pylint: disable=R0902 "No response from node. At least firmware 2.1.22 is required on the destination node." ) + def onResponseWaypoint(self, p: dict): + """on response for waypoint""" + if p["decoded"]["portnum"] == "WAYPOINT_APP": + self._acknowledgment.receivedWaypoint = True + w = mesh_pb2.Waypoint() + w.ParseFromString(p["decoded"]["payload"]) + print(f"Waypoint received: {w}") + 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 sendWaypoint( self, name, @@ -726,6 +739,8 @@ class MeshInterface: # pylint: disable=R0902 w.description = description w.expire = expire if id is None: + # Generate a waypoint's id, NOT a packet ID. + # same algorithm as https://github.com/meshtastic/js/blob/715e35d2374276a43ffa93c628e3710875d43907/src/meshDevice.ts#L791 seed = secrets.randbits(32) w.id = math.floor(seed * math.pow(2, -32) * 1e9) logging.debug(f"w.id:{w.id}") @@ -918,6 +933,12 @@ class MeshInterface: # pylint: disable=R0902 if not success: raise MeshInterface.MeshInterfaceError("Timed out waiting for position") + def waitForWaypoint(self): + """Wait for waypoint""" + success = self._timeout.waitForWaypoint(self._acknowledgment) + if not success: + raise MeshInterface.MeshInterfaceError("Timed out waiting for waypoint") + def getMyNodeInfo(self) -> Optional[Dict]: """Get info about my node.""" if self.myInfo is None or self.nodesByNum is None: diff --git a/meshtastic/util.py b/meshtastic/util.py index f36655e..e7b07a0 100644 --- a/meshtastic/util.py +++ b/meshtastic/util.py @@ -254,6 +254,16 @@ class Timeout: time.sleep(self.sleepInterval) return False + def waitForWaypoint(self, acknowledgment) -> bool: + """Block until waypoint response is received. Returns True if waypoint response has been received.""" + self.reset() + while time.time() < self.expireTime: + if getattr(acknowledgment, "receivedWaypoint", 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." @@ -265,6 +275,7 @@ class Acknowledgment: self.receivedTraceRoute = False self.receivedTelemetry = False self.receivedPosition = False + self.receivedWaypoint = False def reset(self) -> None: """reset""" @@ -274,6 +285,7 @@ class Acknowledgment: self.receivedTraceRoute = False self.receivedTelemetry = False self.receivedPosition = False + self.receivedWaypoint = False class DeferredExecution: From 57f0598082bf2b9c9cdf551f24e0a9e2be3040fc Mon Sep 17 00:00:00 2001 From: Ian McEwen Date: Fri, 27 Dec 2024 09:40:17 -0700 Subject: [PATCH 3/3] Fix some pylint complaints --- examples/waypoint.py | 3 +-- meshtastic/mesh_interface.py | 12 ++++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/examples/waypoint.py b/examples/waypoint.py index 1ce862d..af8dadc 100644 --- a/examples/waypoint.py +++ b/examples/waypoint.py @@ -7,7 +7,6 @@ import argparse import datetime import sys -import time import meshtastic import meshtastic.serial_interface @@ -42,7 +41,7 @@ else: with meshtastic.serial_interface.SerialInterface(args.port, debugOut=d) as iface: if args.cmd == 'create': p = iface.sendWaypoint( - id=int(args.id), + waypoint_id=int(args.id), name=args.name, description=args.description, expire=int(datetime.datetime.fromisoformat(args.expire).timestamp()), diff --git a/meshtastic/mesh_interface.py b/meshtastic/mesh_interface.py index 5f7ba25..aeccc47 100644 --- a/meshtastic/mesh_interface.py +++ b/meshtastic/mesh_interface.py @@ -720,14 +720,14 @@ class MeshInterface: # pylint: disable=R0902 name, description, expire: int, - id: Optional[int] = None, + waypoint_id: Optional[int] = None, latitude: float = 0.0, longitude: float = 0.0, destinationId: Union[int, str] = BROADCAST_ADDR, wantAck: bool = True, wantResponse: bool = False, channelIndex: int = 0, - ): + ): # pylint: disable=R0913 """ Send a waypoint packet to some other node (normally a broadcast) @@ -738,14 +738,14 @@ class MeshInterface: # pylint: disable=R0902 w.name = name w.description = description w.expire = expire - if id is None: + if waypoint_id is None: # Generate a waypoint's id, NOT a packet ID. # same algorithm as https://github.com/meshtastic/js/blob/715e35d2374276a43ffa93c628e3710875d43907/src/meshDevice.ts#L791 seed = secrets.randbits(32) w.id = math.floor(seed * math.pow(2, -32) * 1e9) logging.debug(f"w.id:{w.id}") else: - w.id = id + w.id = waypoint_id if latitude != 0.0: w.latitude_i = int(latitude * 1e7) logging.debug(f"w.latitude_i:{w.latitude_i}") @@ -773,7 +773,7 @@ class MeshInterface: # pylint: disable=R0902 def deleteWaypoint( self, - id: int, + waypoint_id: int, destinationId: Union[int, str] = BROADCAST_ADDR, wantAck: bool = True, wantResponse: bool = False, @@ -788,7 +788,7 @@ class MeshInterface: # pylint: disable=R0902 can be used to track future message acks/naks. """ p = mesh_pb2.Waypoint() - p.id = id + p.id = waypoint_id p.expire = 0 if wantResponse: