diff --git a/.gitignore b/.gitignore index d6f5bdc..a9683e9 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,5 @@ examples/__pycache__ meshtastic.spec .hypothesis/ coverage.xml -.ipynb_checkpoints \ No newline at end of file +.ipynb_checkpoints +.cursor/ \ No newline at end of file diff --git a/examples/waypoint.py b/examples/waypoint.py index af8dadc..6e3a973 100644 --- a/examples/waypoint.py +++ b/examples/waypoint.py @@ -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), diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index 08ecf57..68571f4 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -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 diff --git a/meshtastic/mesh_interface.py b/meshtastic/mesh_interface.py index c269209..7052bc5 100644 --- a/meshtastic/mesh_interface.py +++ b/meshtastic/mesh_interface.py @@ -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. diff --git a/meshtastic/node.py b/meshtastic/node.py index 6f76852..1b89881 100644 --- a/meshtastic/node.py +++ b/meshtastic/node.py @@ -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) diff --git a/meshtastic/serial_interface.py b/meshtastic/serial_interface.py index 88f17de..8d1397c 100644 --- a/meshtastic/serial_interface.py +++ b/meshtastic/serial_interface.py @@ -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) diff --git a/meshtastic/supported_device.py b/meshtastic/supported_device.py index e5c8271..8035694 100755 --- a/meshtastic/supported_device.py +++ b/meshtastic/supported_device.py @@ -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 ] diff --git a/meshtastic/util.py b/meshtastic/util.py index a644195..23fe246 100644 --- a/meshtastic/util.py +++ b/meshtastic/util.py @@ -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)