Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Waypoint creation/move/deletion #714

Merged
merged 3 commits into from
Dec 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions examples/waypoint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""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
python3 examples/waypoint.py delete 45
"""

import argparse
import datetime
import sys

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(
waypoint_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()
115 changes: 115 additions & 0 deletions meshtastic/mesh_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
import collections
import json
import logging
import math
import random
import secrets
import sys
import threading
import time
Expand Down Expand Up @@ -700,6 +702,113 @@
"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()

Check warning on line 709 in meshtastic/mesh_interface.py

View check run for this annotation

Codecov / codecov/patch

meshtastic/mesh_interface.py#L705-L709

Added lines #L705 - L709 were not covered by tests
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(

Check warning on line 718 in meshtastic/mesh_interface.py

View check run for this annotation

Codecov / codecov/patch

meshtastic/mesh_interface.py#L712-L718

Added lines #L712 - L718 were not covered by tests
self,
name,
description,
expire: int,

Check warning on line 722 in meshtastic/mesh_interface.py

View check run for this annotation

Codecov / codecov/patch

meshtastic/mesh_interface.py#L721-L722

Added lines #L721 - L722 were not covered by tests
waypoint_id: Optional[int] = None,
latitude: float = 0.0,
longitude: float = 0.0,
destinationId: Union[int, str] = BROADCAST_ADDR,

Check warning on line 726 in meshtastic/mesh_interface.py

View check run for this annotation

Codecov / codecov/patch

meshtastic/mesh_interface.py#L725-L726

Added lines #L725 - L726 were not covered by tests
wantAck: bool = True,
wantResponse: bool = False,
channelIndex: int = 0,
): # pylint: disable=R0913
"""
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

Check warning on line 734 in meshtastic/mesh_interface.py

View check run for this annotation

Codecov / codecov/patch

meshtastic/mesh_interface.py#L729-L734

Added lines #L729 - L734 were not covered by tests
can be used to track future message acks/naks.
"""
w = mesh_pb2.Waypoint()
w.name = name

Check warning on line 738 in meshtastic/mesh_interface.py

View check run for this annotation

Codecov / codecov/patch

meshtastic/mesh_interface.py#L736-L738

Added lines #L736 - L738 were not covered by tests
w.description = description
w.expire = expire
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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the source of this ID generation formula? Looking at the other clients, android seems to just use whatever would be the next packet ID and apple seems to generate a random one.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have done similar has in javascript API.
Note that it is not a packet ID but a waypoint ID.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough. I'm not sure why restricting it to 9 digits is useful, but it's probably fine in any case for this applicaiton.

logging.debug(f"w.id:{w.id}")
else:
w.id = waypoint_id
if latitude != 0.0:
w.latitude_i = int(latitude * 1e7)
logging.debug(f"w.latitude_i:{w.latitude_i}")

Check warning on line 751 in meshtastic/mesh_interface.py

View check run for this annotation

Codecov / codecov/patch

meshtastic/mesh_interface.py#L744-L751

Added lines #L744 - L751 were not covered by tests
if longitude != 0.0:
w.longitude_i = int(longitude * 1e7)
logging.debug(f"w.longitude_i:{w.longitude_i}")

if wantResponse:
onResponse = self.onResponseWaypoint
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function doesn't appear to exist.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My bad, I forgot those indeed.
Latest commit add them but I am not sure how they should really behave from a protocol perspective. So please guide me if it is wrong.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These functions are what'd normally be used by the CLI if it supported this stuff, but of course this PR doesn't include that. For now, I think they can just serve as placeholders, so what you have should be fine, I think.

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()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem to exist either.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see commit 7cc18e9

return d

def deleteWaypoint(
self,
waypoint_id: int,
destinationId: Union[int, str] = BROADCAST_ADDR,
wantAck: bool = True,

Check warning on line 778 in meshtastic/mesh_interface.py

View check run for this annotation

Codecov / codecov/patch

meshtastic/mesh_interface.py#L774-L778

Added lines #L774 - L778 were not covered by tests
wantResponse: bool = False,
channelIndex: int = 0,
):
"""
Send a waypoint deletion packet to some other node (normally a broadcast)

Check warning on line 783 in meshtastic/mesh_interface.py

View check run for this annotation

Codecov / codecov/patch

meshtastic/mesh_interface.py#L781-L783

Added lines #L781 - L783 were not covered by tests

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 = waypoint_id

Check warning on line 791 in meshtastic/mesh_interface.py

View check run for this annotation

Codecov / codecov/patch

meshtastic/mesh_interface.py#L785-L791

Added lines #L785 - L791 were not covered by tests
p.expire = 0

if wantResponse:

Check warning on line 794 in meshtastic/mesh_interface.py

View check run for this annotation

Codecov / codecov/patch

meshtastic/mesh_interface.py#L793-L794

Added lines #L793 - L794 were not covered by tests
onResponse = self.onResponseWaypoint
else:

Check warning on line 796 in meshtastic/mesh_interface.py

View check run for this annotation

Codecov / codecov/patch

meshtastic/mesh_interface.py#L796

Added line #L796 was not covered by tests
onResponse = None

Check warning on line 798 in meshtastic/mesh_interface.py

View check run for this annotation

Codecov / codecov/patch

meshtastic/mesh_interface.py#L798

Added line #L798 was not covered by tests
d = self.sendData(
p,
destinationId,
portNum=portnums_pb2.PortNum.WAYPOINT_APP,
wantAck=wantAck,
wantResponse=wantResponse,
onResponse=onResponse,
channelIndex=channelIndex,
)
if wantResponse:
self.waitForWaypoint()

Check warning on line 809 in meshtastic/mesh_interface.py

View check run for this annotation

Codecov / codecov/patch

meshtastic/mesh_interface.py#L807-L809

Added lines #L807 - L809 were not covered by tests
return d

def _addResponseHandler(
self,
requestId: int,
Expand Down Expand Up @@ -824,6 +933,12 @@
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:
Expand Down
12 changes: 12 additions & 0 deletions meshtastic/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,16 @@
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

Check warning on line 265 in meshtastic/util.py

View check run for this annotation

Codecov / codecov/patch

meshtastic/util.py#L259-L265

Added lines #L259 - L265 were not covered by tests

class Acknowledgment:
"A class that records which type of acknowledgment was just received, if any."

Expand All @@ -265,6 +275,7 @@
self.receivedTraceRoute = False
self.receivedTelemetry = False
self.receivedPosition = False
self.receivedWaypoint = False

def reset(self) -> None:
"""reset"""
Expand All @@ -274,6 +285,7 @@
self.receivedTraceRoute = False
self.receivedTelemetry = False
self.receivedPosition = False
self.receivedWaypoint = False


class DeferredExecution:
Expand Down
Loading