Skip to content

Commit f4c085f

Browse files
authored
Merge pull request #714 from loic-fejoz/feature/waypoint
Waypoint creation/move/deletion
2 parents 55d3188 + 57f0598 commit f4c085f

File tree

3 files changed

+182
-0
lines changed

3 files changed

+182
-0
lines changed

examples/waypoint.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"""Program to create and delete waypoint
2+
To run:
3+
python3 examples/waypoint.py --port /dev/ttyUSB0 create 45 test the_desc_2 '2024-12-18T23:05:23' 48.74 7.35
4+
python3 examples/waypoint.py delete 45
5+
"""
6+
7+
import argparse
8+
import datetime
9+
import sys
10+
11+
import meshtastic
12+
import meshtastic.serial_interface
13+
14+
parser = argparse.ArgumentParser(
15+
prog='waypoint',
16+
description='Create and delete Meshtastic waypoint')
17+
parser.add_argument('--port', default=None)
18+
parser.add_argument('--debug', default=False, action='store_true')
19+
20+
subparsers = parser.add_subparsers(dest='cmd')
21+
parser_delete = subparsers.add_parser('delete', help='Delete a waypoint')
22+
parser_delete.add_argument('id', help="id of the waypoint")
23+
24+
parser_create = subparsers.add_parser('create', help='Create a new waypoint')
25+
parser_create.add_argument('id', help="id of the waypoint")
26+
parser_create.add_argument('name', help="name of the waypoint")
27+
parser_create.add_argument('description', help="description of the waypoint")
28+
parser_create.add_argument('expire', help="expiration date of the waypoint as interpreted by datetime.fromisoformat")
29+
parser_create.add_argument('latitude', help="latitude of the waypoint")
30+
parser_create.add_argument('longitude', help="longitude of the waypoint")
31+
32+
args = parser.parse_args()
33+
print(args)
34+
35+
# By default will try to find a meshtastic device,
36+
# otherwise provide a device path like /dev/ttyUSB0
37+
if args.debug:
38+
d = sys.stderr
39+
else:
40+
d = None
41+
with meshtastic.serial_interface.SerialInterface(args.port, debugOut=d) as iface:
42+
if args.cmd == 'create':
43+
p = iface.sendWaypoint(
44+
waypoint_id=int(args.id),
45+
name=args.name,
46+
description=args.description,
47+
expire=int(datetime.datetime.fromisoformat(args.expire).timestamp()),
48+
latitude=float(args.latitude),
49+
longitude=float(args.longitude),
50+
)
51+
else:
52+
p = iface.deleteWaypoint(int(args.id))
53+
print(p)
54+
55+
# iface.close()

meshtastic/mesh_interface.py

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
import collections
66
import json
77
import logging
8+
import math
89
import random
10+
import secrets
911
import sys
1012
import threading
1113
import time
@@ -737,6 +739,113 @@ def onResponseTelemetry(self, p: dict):
737739
"No response from node. At least firmware 2.1.22 is required on the destination node."
738740
)
739741

742+
def onResponseWaypoint(self, p: dict):
743+
"""on response for waypoint"""
744+
if p["decoded"]["portnum"] == "WAYPOINT_APP":
745+
self._acknowledgment.receivedWaypoint = True
746+
w = mesh_pb2.Waypoint()
747+
w.ParseFromString(p["decoded"]["payload"])
748+
print(f"Waypoint received: {w}")
749+
elif p["decoded"]["portnum"] == "ROUTING_APP":
750+
if p["decoded"]["routing"]["errorReason"] == "NO_RESPONSE":
751+
our_exit(
752+
"No response from node. At least firmware 2.1.22 is required on the destination node."
753+
)
754+
755+
def sendWaypoint(
756+
self,
757+
name,
758+
description,
759+
expire: int,
760+
waypoint_id: Optional[int] = None,
761+
latitude: float = 0.0,
762+
longitude: float = 0.0,
763+
destinationId: Union[int, str] = BROADCAST_ADDR,
764+
wantAck: bool = True,
765+
wantResponse: bool = False,
766+
channelIndex: int = 0,
767+
): # pylint: disable=R0913
768+
"""
769+
Send a waypoint packet to some other node (normally a broadcast)
770+
771+
Returns the sent packet. The id field will be populated in this packet and
772+
can be used to track future message acks/naks.
773+
"""
774+
w = mesh_pb2.Waypoint()
775+
w.name = name
776+
w.description = description
777+
w.expire = expire
778+
if waypoint_id is None:
779+
# Generate a waypoint's id, NOT a packet ID.
780+
# same algorithm as https://github.com/meshtastic/js/blob/715e35d2374276a43ffa93c628e3710875d43907/src/meshDevice.ts#L791
781+
seed = secrets.randbits(32)
782+
w.id = math.floor(seed * math.pow(2, -32) * 1e9)
783+
logging.debug(f"w.id:{w.id}")
784+
else:
785+
w.id = waypoint_id
786+
if latitude != 0.0:
787+
w.latitude_i = int(latitude * 1e7)
788+
logging.debug(f"w.latitude_i:{w.latitude_i}")
789+
if longitude != 0.0:
790+
w.longitude_i = int(longitude * 1e7)
791+
logging.debug(f"w.longitude_i:{w.longitude_i}")
792+
793+
if wantResponse:
794+
onResponse = self.onResponseWaypoint
795+
else:
796+
onResponse = None
797+
798+
d = self.sendData(
799+
w,
800+
destinationId,
801+
portNum=portnums_pb2.PortNum.WAYPOINT_APP,
802+
wantAck=wantAck,
803+
wantResponse=wantResponse,
804+
onResponse=onResponse,
805+
channelIndex=channelIndex,
806+
)
807+
if wantResponse:
808+
self.waitForWaypoint()
809+
return d
810+
811+
def deleteWaypoint(
812+
self,
813+
waypoint_id: int,
814+
destinationId: Union[int, str] = BROADCAST_ADDR,
815+
wantAck: bool = True,
816+
wantResponse: bool = False,
817+
channelIndex: int = 0,
818+
):
819+
"""
820+
Send a waypoint deletion packet to some other node (normally a broadcast)
821+
822+
NB: The id must be the waypoint's id and not the id of the packet creation.
823+
824+
Returns the sent packet. The id field will be populated in this packet and
825+
can be used to track future message acks/naks.
826+
"""
827+
p = mesh_pb2.Waypoint()
828+
p.id = waypoint_id
829+
p.expire = 0
830+
831+
if wantResponse:
832+
onResponse = self.onResponseWaypoint
833+
else:
834+
onResponse = None
835+
836+
d = self.sendData(
837+
p,
838+
destinationId,
839+
portNum=portnums_pb2.PortNum.WAYPOINT_APP,
840+
wantAck=wantAck,
841+
wantResponse=wantResponse,
842+
onResponse=onResponse,
843+
channelIndex=channelIndex,
844+
)
845+
if wantResponse:
846+
self.waitForWaypoint()
847+
return d
848+
740849
def _addResponseHandler(
741850
self,
742851
requestId: int,
@@ -861,6 +970,12 @@ def waitForPosition(self):
861970
if not success:
862971
raise MeshInterface.MeshInterfaceError("Timed out waiting for position")
863972

973+
def waitForWaypoint(self):
974+
"""Wait for waypoint"""
975+
success = self._timeout.waitForWaypoint(self._acknowledgment)
976+
if not success:
977+
raise MeshInterface.MeshInterfaceError("Timed out waiting for waypoint")
978+
864979
def getMyNodeInfo(self) -> Optional[Dict]:
865980
"""Get info about my node."""
866981
if self.myInfo is None or self.nodesByNum is None:

meshtastic/util.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,16 @@ def waitForPosition(self, acknowledgment) -> bool:
254254
time.sleep(self.sleepInterval)
255255
return False
256256

257+
def waitForWaypoint(self, acknowledgment) -> bool:
258+
"""Block until waypoint response is received. Returns True if waypoint response has been received."""
259+
self.reset()
260+
while time.time() < self.expireTime:
261+
if getattr(acknowledgment, "receivedWaypoint", None):
262+
acknowledgment.reset()
263+
return True
264+
time.sleep(self.sleepInterval)
265+
return False
266+
257267
class Acknowledgment:
258268
"A class that records which type of acknowledgment was just received, if any."
259269

@@ -265,6 +275,7 @@ def __init__(self) -> None:
265275
self.receivedTraceRoute = False
266276
self.receivedTelemetry = False
267277
self.receivedPosition = False
278+
self.receivedWaypoint = False
268279

269280
def reset(self) -> None:
270281
"""reset"""
@@ -274,6 +285,7 @@ def reset(self) -> None:
274285
self.receivedTraceRoute = False
275286
self.receivedTelemetry = False
276287
self.receivedPosition = False
288+
self.receivedWaypoint = False
277289

278290

279291
class DeferredExecution:

0 commit comments

Comments
 (0)