Merge branch 'master' into channel-hash-info

This commit is contained in:
Ian McEwen
2025-11-06 14:51:32 -07:00
committed by GitHub
8 changed files with 57 additions and 40 deletions

3
.gitignore vendored
View File

@@ -17,4 +17,5 @@ examples/__pycache__
meshtastic.spec
.hypothesis/
coverage.xml
.ipynb_checkpoints
.ipynb_checkpoints
.cursor/

View File

@@ -25,6 +25,7 @@ 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('icon', help="icon 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")
@@ -44,6 +45,7 @@ with meshtastic.serial_interface.SerialInterface(args.port, debugOut=d) as iface
waypoint_id=int(args.id),
name=args.name,
description=args.description,
icon=args.icon,
expire=int(datetime.datetime.fromisoformat(args.expire).timestamp()),
latitude=float(args.latitude),
longitude=float(args.longitude),

View File

@@ -277,7 +277,8 @@ def setPref(config, comp_name, raw_val) -> bool:
else:
print(f"Adding '{raw_val}' to the {pref.name} list")
cur_vals = [x for x in getattr(config_values, pref.name) if x not in [0, "", b""]]
cur_vals.append(val)
if val not in cur_vals:
cur_vals.append(val)
getattr(config_values, pref.name)[:] = cur_vals
return True

View File

@@ -830,6 +830,7 @@ class MeshInterface: # pylint: disable=R0902
self,
name,
description,
icon,
expire: int,
waypoint_id: Optional[int] = None,
latitude: float = 0.0,
@@ -848,6 +849,7 @@ class MeshInterface: # pylint: disable=R0902
w = mesh_pb2.Waypoint()
w.name = name
w.description = description
w.icon = icon
w.expire = expire
if waypoint_id is None:
# Generate a waypoint's id, NOT a packet ID.

View File

@@ -17,6 +17,7 @@ from meshtastic.util import (
stripnl,
message_to_json,
generate_channel_hash,
to_node_num,
)
logger = logging.getLogger(__name__)
@@ -715,11 +716,7 @@ class Node:
def removeNode(self, nodeId: Union[int, str]):
"""Tell the node to remove a specific node by ID"""
self.ensureSessionKey()
if isinstance(nodeId, str):
if nodeId.startswith("!"):
nodeId = int(nodeId[1:], 16)
else:
nodeId = int(nodeId)
nodeId = to_node_num(nodeId)
p = admin_pb2.AdminMessage()
p.remove_by_nodenum = nodeId
@@ -733,11 +730,7 @@ class Node:
def setFavorite(self, nodeId: Union[int, str]):
"""Tell the node to set the specified node ID to be favorited on the NodeDB on the device"""
self.ensureSessionKey()
if isinstance(nodeId, str):
if nodeId.startswith("!"):
nodeId = int(nodeId[1:], 16)
else:
nodeId = int(nodeId)
nodeId = to_node_num(nodeId)
p = admin_pb2.AdminMessage()
p.set_favorite_node = nodeId
@@ -751,11 +744,7 @@ class Node:
def removeFavorite(self, nodeId: Union[int, str]):
"""Tell the node to set the specified node ID to be un-favorited on the NodeDB on the device"""
self.ensureSessionKey()
if isinstance(nodeId, str):
if nodeId.startswith("!"):
nodeId = int(nodeId[1:], 16)
else:
nodeId = int(nodeId)
nodeId = to_node_num(nodeId)
p = admin_pb2.AdminMessage()
p.remove_favorite_node = nodeId
@@ -769,11 +758,7 @@ class Node:
def setIgnored(self, nodeId: Union[int, str]):
"""Tell the node to set the specified node ID to be ignored on the NodeDB on the device"""
self.ensureSessionKey()
if isinstance(nodeId, str):
if nodeId.startswith("!"):
nodeId = int(nodeId[1:], 16)
else:
nodeId = int(nodeId)
nodeId = to_node_num(nodeId)
p = admin_pb2.AdminMessage()
p.set_ignored_node = nodeId
@@ -787,11 +772,7 @@ class Node:
def removeIgnored(self, nodeId: Union[int, str]):
"""Tell the node to set the specified node ID to be un-ignored on the NodeDB on the device"""
self.ensureSessionKey()
if isinstance(nodeId, str):
if nodeId.startswith("!"):
nodeId = int(nodeId[1:], 16)
else:
nodeId = int(nodeId)
nodeId = to_node_num(nodeId)
p = admin_pb2.AdminMessage()
p.remove_ignored_node = nodeId
@@ -1014,10 +995,7 @@ class Node:
): # unless a special channel index was used, we want to use the admin index
adminIndex = self.iface.localNode._getAdminChannelIndex()
logger.debug(f"adminIndex:{adminIndex}")
if isinstance(self.nodeNum, int):
nodeid = self.nodeNum
else: # assume string starting with !
nodeid = int(self.nodeNum[1:],16)
nodeid = to_node_num(self.nodeNum)
if "adminSessionPassKey" in self.iface._getOrCreateByNum(nodeid):
p.session_passkey = self.iface._getOrCreateByNum(nodeid).get("adminSessionPassKey")
return self.iface.sendData(
@@ -1038,10 +1016,7 @@ class Node:
f"Not ensuring session key, because protocol use is disabled by noProto"
)
else:
if isinstance(self.nodeNum, int):
nodeid = self.nodeNum
else: # assume string starting with !
nodeid = int(self.nodeNum[1:],16)
nodeid = to_node_num(self.nodeNum)
if self.iface._getOrCreateByNum(nodeid).get("adminSessionPassKey") is None:
self.requestConfig(admin_pb2.AdminMessage.SESSIONKEY_CONFIG)

View File

@@ -94,10 +94,16 @@ class SerialInterface(StreamInterface):
def close(self) -> None:
"""Close a connection to the device"""
if self.stream: # Stream can be null if we were already closed
self.stream.flush() # FIXME: why are there these two flushes with 100ms sleeps? This shouldn't be necessary
time.sleep(0.1)
self.stream.flush()
time.sleep(0.1)
if hasattr(self, "stream") and self.stream and getattr(self.stream, "is_open", False):
try:
self.stream.flush()
time.sleep(0.1)
except Exception as e:
logger.debug(f"Exception during flush: {e}")
try:
self.stream.close()
except Exception as e:
logger.debug(f"Exception during close: {e}")
self.stream = None
logger.debug("Closing Serial stream")
StreamInterface.close(self)

View File

@@ -217,6 +217,18 @@ seeed_xiao_s3 = SupportedDevice(
usb_product_id_in_hex="0059",
)
tdeck = SupportedDevice(
name="T-Deck",
version="",
for_firmware="t-deck", # Confirmed firmware identifier
device_class="esp32",
baseport_on_linux="ttyACM",
baseport_on_mac="cu.usbmodem",
baseport_on_windows="COM",
usb_vendor_id_in_hex="303a", # Espressif Systems (VERIFIED)
usb_product_id_in_hex="1001", # VERIFIED from actual device
)
supported_devices = [
@@ -239,4 +251,5 @@ supported_devices = [
rak11200,
nano_g1,
seeed_xiao_s3,
tdeck, # T-Deck support added
]

View File

@@ -719,3 +719,20 @@ def message_to_json(message: Message, multiline: bool=False) -> str:
except TypeError:
json = MessageToJson(message, including_default_value_fields=True) # type: ignore[call-arg] # pylint: disable=E1123
return stripnl(json) if not multiline else json
def to_node_num(node_id: Union[int, str]) -> int:
"""
Normalize a node id from int | '!hex' | '0xhex' | 'decimal' to int.
"""
if isinstance(node_id, int):
return node_id
s = str(node_id).strip()
if s.startswith("!"):
s = s[1:]
if s.lower().startswith("0x"):
return int(s, 16)
try:
return int(s, 10)
except ValueError:
return int(s, 16)