From 0413d00825fea5e3aa2230278852bb2292bbb774 Mon Sep 17 00:00:00 2001 From: Nox Date: Wed, 3 Jun 2026 22:50:56 +0800 Subject: [PATCH 01/17] Handle ROUTING_APP error response in onResponseTraceRoute Previously, receiving a ROUTING_APP packet in response to a traceroute would cause the function to attempt parsing it as a RouteDiscovery payload, resulting in a crash or silent failure followed by a timeout. This mirrors the error handling already present in onResponseTelemetry and onResponseWaypoint, but displays the specific errorReason instead of a hardcoded firmware version message, since traceroute is a diagnostic tool where the exact failure reason is more valuable. Co-Authored-By: Claude Sonnet 4.6 --- meshtastic/mesh_interface.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/meshtastic/mesh_interface.py b/meshtastic/mesh_interface.py index 5dfb393b7..d8083c654 100644 --- a/meshtastic/mesh_interface.py +++ b/meshtastic/mesh_interface.py @@ -687,6 +687,12 @@ def sendTraceRoute( def onResponseTraceRoute(self, p: dict): """on response for trace route""" + if p["decoded"]["portnum"] == "ROUTING_APP": + error = p["decoded"]["routing"]["errorReason"] + print(f"Traceroute failed: {error}") + self._acknowledgment.receivedTraceRoute = True + return + UNK_SNR = -128 # Value representing unknown SNR routeDiscovery = mesh_pb2.RouteDiscovery() From c71d3df3322cb2168395ff29f753c08cd43d8045 Mon Sep 17 00:00:00 2001 From: Nox Date: Thu, 4 Jun 2026 12:45:59 +0800 Subject: [PATCH 02/17] Add unit test for onResponseTraceRoute ROUTING_APP error handling Co-Authored-By: Claude Sonnet 4.6 --- meshtastic/tests/test_mesh_interface.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/meshtastic/tests/test_mesh_interface.py b/meshtastic/tests/test_mesh_interface.py index 8d53628fb..d002c187c 100644 --- a/meshtastic/tests/test_mesh_interface.py +++ b/meshtastic/tests/test_mesh_interface.py @@ -757,3 +757,23 @@ def test_timeago_fuzz(seconds): """Fuzz _timeago to ensure it works with any integer""" val = _timeago(seconds) assert re.match(r"(now|\d+ (secs?|mins?|hours?|days?|months?|years?))", val) + + +@pytest.mark.unit +@pytest.mark.usefixtures("reset_mt_config") +def test_onResponseTraceRoute_routing_error(capsys): + """Test that onResponseTraceRoute handles ROUTING_APP error packets correctly.""" + iface = MeshInterface(noProto=True) + + packet = { + "decoded": { + "portnum": "ROUTING_APP", + "routing": {"errorReason": "MAX_RETRANSMIT"}, + } + } + + iface.onResponseTraceRoute(packet) + + assert iface._acknowledgment.receivedTraceRoute is True + out, _ = capsys.readouterr() + assert "Traceroute failed: MAX_RETRANSMIT" in out From 953d01206bbc5f3ee3cee32422a359cd3f459e63 Mon Sep 17 00:00:00 2001 From: Nox Date: Thu, 4 Jun 2026 21:03:49 +0800 Subject: [PATCH 03/17] Handle NONE errorReason in onResponseTraceRoute and add regression test ROUTING_APP with errorReason NONE is an ACK (success), not an error. Only print failure message for non-NONE error reasons. Co-Authored-By: Claude Sonnet 4.6 --- meshtastic/mesh_interface.py | 3 ++- meshtastic/tests/test_mesh_interface.py | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/meshtastic/mesh_interface.py b/meshtastic/mesh_interface.py index d8083c654..1a7dd6eae 100644 --- a/meshtastic/mesh_interface.py +++ b/meshtastic/mesh_interface.py @@ -689,7 +689,8 @@ def onResponseTraceRoute(self, p: dict): """on response for trace route""" if p["decoded"]["portnum"] == "ROUTING_APP": error = p["decoded"]["routing"]["errorReason"] - print(f"Traceroute failed: {error}") + if error != "NONE": + print(f"Traceroute failed: {error}") self._acknowledgment.receivedTraceRoute = True return diff --git a/meshtastic/tests/test_mesh_interface.py b/meshtastic/tests/test_mesh_interface.py index d002c187c..7f0a9f5b1 100644 --- a/meshtastic/tests/test_mesh_interface.py +++ b/meshtastic/tests/test_mesh_interface.py @@ -777,3 +777,23 @@ def test_onResponseTraceRoute_routing_error(capsys): assert iface._acknowledgment.receivedTraceRoute is True out, _ = capsys.readouterr() assert "Traceroute failed: MAX_RETRANSMIT" in out + + +@pytest.mark.unit +@pytest.mark.usefixtures("reset_mt_config") +def test_onResponseTraceRoute_routing_none(capsys): + """Test that onResponseTraceRoute does not print error for ROUTING_APP with NONE errorReason.""" + iface = MeshInterface(noProto=True) + + packet = { + "decoded": { + "portnum": "ROUTING_APP", + "routing": {"errorReason": "NONE"}, + } + } + + iface.onResponseTraceRoute(packet) + + assert iface._acknowledgment.receivedTraceRoute is True + out, _ = capsys.readouterr() + assert "Traceroute failed" not in out From 1cffae6adddd3212678b635019a895ae020fbf91 Mon Sep 17 00:00:00 2001 From: Ian McEwen Date: Sun, 14 Jun 2026 15:28:07 -0700 Subject: [PATCH 04/17] Add support for adding contacts using the CLI, including remotely --- meshtastic/__main__.py | 46 ++++++++++++++++++++++++ meshtastic/node.py | 66 +++++++++++++++++++++++++++++++++++ meshtastic/tests/test_main.py | 50 ++++++++++++++++++++++++++ meshtastic/tests/test_node.py | 54 +++++++++++++++++++++++++++- 4 files changed, 215 insertions(+), 1 deletion(-) diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index 8b866def4..051a7a8e8 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -552,6 +552,13 @@ def onConnected(interface): waitForAckNak = True interface.getNode(args.dest, False, **getNode_kwargs).resetNodeDb() + if args.add_contact: + closeNow = True + waitForAckNak = True + interface.getNode(args.dest, False, **getNode_kwargs).addContactURL( + args.add_contact + ) + if args.sendtext: closeNow = True channelIndex = mt_config.channel_index or 0 @@ -1074,6 +1081,20 @@ def setSimpleConfig(modem_preset): else: print("Install pyqrcode to view a QR code printed to terminal.") + if args.contact_qr: + closeNow = True + url = interface.getNode(args.dest, True, **getNode_kwargs).getContactURL( + args.contact_qr, + should_ignore=args.contact_ignore, + manually_verified=args.contact_verified, + ) + print(f"Contact URL: {url}") + if pyqrcode is not None: + qr = pyqrcode.create(url) + print(qr.terminal()) + else: + print("Install pyqrcode to view a QR code printed to terminal.") + log_set: Optional = None # type: ignore[annotation-unchecked] # we need to keep a reference to the logset so it doesn't get GCed early @@ -1858,6 +1879,24 @@ def addChannelConfigArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentPa action="store_true", ) + group.add_argument( + "--contact-qr", + help="Display a QR code for a node's contact data. " + "Use the node ID with a '!' or '0x' prefix or the node number. " + "Also shows the shareable contact URL.", + metavar="!xxxxxxxx", + ) + group.add_argument( + "--contact-verified", + help="Set the IS_KEY_MANUALLY_VERIFIED bit in the generated contact URL", + action="store_true", + ) + group.add_argument( + "--contact-ignore", + help="Mark this contact as blocked/ignored in the generated contact URL", + action="store_true", + ) + group.add_argument( "--ch-enable", help="Enable the specified channel. Use --ch-add instead whenever possible.", @@ -2095,6 +2134,13 @@ def addRemoteAdminArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentPars action="store_true", ) + group.add_argument( + "--add-contact", + help="Add a contact (User) to the NodeDB from a shareable URL. " + "Example: https://meshtastic.org/v/#", + metavar="URL", + ) + group.add_argument( "--set-time", help="Set the time to the provided unix epoch timestamp, or the system's current time if omitted or 0.", diff --git a/meshtastic/node.py b/meshtastic/node.py index b18eff072..51b0eed74 100644 --- a/meshtastic/node.py +++ b/meshtastic/node.py @@ -380,6 +380,46 @@ def getURL(self, includeAll: bool = True): s = s.replace("=", "").replace("+", "-").replace("/", "_") return f"https://meshtastic.org/e/#{s}" + def getContactURL(self, node_id: Union[int, str], should_ignore: bool = False, manually_verified: bool = False): + """Generate a shareable contact URL for the specified node""" + nodeNum = to_node_num(node_id) + + node = self.iface.nodesByNum.get(nodeNum) + if not node or not node.get("user"): + our_exit(f"Warning: Node {node_id} not found in NodeDB") + + contact = admin_pb2.SharedContact() + contact.node_num = nodeNum + + u = node["user"] + if u.get("id"): + contact.user.id = u["id"] + if u.get("macaddr"): + contact.user.macaddr = base64.b64decode(u["macaddr"]) + if u.get("longName"): + contact.user.long_name = u["longName"] + if u.get("shortName"): + contact.user.short_name = u["shortName"] + if u.get("hwModel") and u["hwModel"] != "UNSET": + contact.user.hw_model = mesh_pb2.HardwareModel.Value(u["hwModel"]) + if u.get("role"): + contact.user.role = u["role"] + if u.get("publicKey"): + contact.user.public_key = base64.b64decode(u["publicKey"]) + if u.get("isLicensed"): + contact.user.is_licensed = u["isLicensed"] + if u.get("isUnmessagable") is not None: + contact.user.is_unmessagable = u["isUnmessagable"] + if should_ignore: + contact.should_ignore = True + if manually_verified: + contact.manually_verified = True + + data = contact.SerializeToString() + s = base64.urlsafe_b64encode(data).decode("ascii") + s = s.replace("=", "").replace("+", "-").replace("/", "_") + return f"https://meshtastic.org/v/#{s}" + def setURL(self, url: str, addOnly: bool = False): """Set mesh network URL""" if self.localConfig is None or self.channels is None: @@ -445,6 +485,32 @@ def setURL(self, url: str, addOnly: bool = False): self.ensureSessionKey() self._sendAdmin(p) + def addContactURL(self, url: str): + """Add a contact (User) to the NodeDB from a shareable URL""" + self.ensureSessionKey() + + splitURL = url.split("/#") + if len(splitURL) == 1: + our_exit(f"Warning: Invalid URL '{url}'") + b64 = splitURL[-1] + + missing_padding = len(b64) % 4 + if missing_padding: + b64 += "=" * (4 - missing_padding) + + decodedURL = base64.urlsafe_b64decode(b64) + contact = admin_pb2.SharedContact() + contact.ParseFromString(decodedURL) + + p = admin_pb2.AdminMessage() + p.add_contact.CopyFrom(contact) + + if self == self.iface.localNode: + onResponse = None + else: + onResponse = self.onAckNak + return self._sendAdmin(p, onResponse=onResponse) + def onResponseRequestRingtone(self, p): """Handle the response packet for requesting ringtone part 1""" logger.debug(f"onResponseRequestRingtone() p:{p}") diff --git a/meshtastic/tests/test_main.py b/meshtastic/tests/test_main.py index 60f352084..772717cb6 100644 --- a/meshtastic/tests/test_main.py +++ b/meshtastic/tests/test_main.py @@ -2998,6 +2998,56 @@ def test_remove_ignored_node(): main() mocked_node.removeIgnored.assert_called_once_with("!12345678") + +@pytest.mark.unit +@pytest.mark.usefixtures("reset_mt_config") +def test_add_contact_url(): + """Test --add-contact with a shareable URL""" + url = "https://meshtastic.org/v/#CKqkvZgIElEKCSE4MzBmNTIyYRIQUm9hZHJ1bm5lciBSaWRnZRoEUktTTiIGAAAAAAAAKAk4AkIgRxo_Fw_ergQIhRqBbrHasLYy3gU-Ay8hrhu4OVnIPQc=" # pylint: disable=line-too-long + sys.argv = ["", "--add-contact", url] + mt_config.args = sys.argv + mocked_node = MagicMock(autospec=Node) + iface = MagicMock(autospec=SerialInterface) + iface.getNode.return_value = mocked_node + with patch("meshtastic.serial_interface.SerialInterface", return_value=iface): + main() + + mocked_node.addContactURL.assert_called_once_with(url) + + +@pytest.mark.unit +@pytest.mark.usefixtures("reset_mt_config") +def test_contact_qr(): + """Test --contact-qr with a node ID""" + sys.argv = ["", "--contact-qr", "!830f522a"] + mt_config.args = sys.argv + mocked_node = MagicMock(autospec=Node) + iface = MagicMock(autospec=SerialInterface) + iface.getNode.return_value = mocked_node + mocked_node.iface = iface + with patch("meshtastic.serial_interface.SerialInterface", return_value=iface): + main() + + mocked_node.getContactURL.assert_called_once_with("!830f522a", should_ignore=False, manually_verified=False) + mocked_node.getContactURL.reset_mock() + + +@pytest.mark.unit +@pytest.mark.usefixtures("reset_mt_config") +def test_contact_qr_with_flags(): + """Test --contact-qr with --contact-verified and --contact-ignore""" + sys.argv = ["", "--contact-qr", "!830f522a", "--contact-verified", "--contact-ignore"] + mt_config.args = sys.argv + mocked_node = MagicMock(autospec=Node) + iface = MagicMock(autospec=SerialInterface) + iface.getNode.return_value = mocked_node + mocked_node.iface = iface + with patch("meshtastic.serial_interface.SerialInterface", return_value=iface): + main() + + mocked_node.getContactURL.assert_called_once_with("!830f522a", should_ignore=True, manually_verified=True) + + @pytest.mark.unit @pytest.mark.usefixtures("reset_mt_config") def test_main_set_owner_whitespace_only(capsys): diff --git a/meshtastic/tests/test_node.py b/meshtastic/tests/test_node.py index bd0644dec..47020f666 100644 --- a/meshtastic/tests/test_node.py +++ b/meshtastic/tests/test_node.py @@ -7,7 +7,9 @@ import pytest -from ..protobuf import admin_pb2, localonly_pb2, config_pb2 +import base64 + +from ..protobuf import admin_pb2, localonly_pb2, config_pb2, mesh_pb2 from ..protobuf.channel_pb2 import Channel # pylint: disable=E0611 from ..node import Node from ..serial_interface import SerialInterface @@ -339,6 +341,56 @@ def test_setURL_valid_URL_but_no_settings(capsys): assert err == "" +@pytest.mark.unit +def test_contact_url_roundtrip(): + """Verify that contact URL generation and parsing is fully reversible""" + def encode_url(contact): + data = contact.SerializeToString() + s = base64.urlsafe_b64encode(data).decode("ascii") + s = s.replace("=", "").replace("+", "-").replace("/", "_") + return f"https://meshtastic.org/v/#{s}" + + def decode_url(url): + b64 = url.split("/#")[-1] + missing_padding = len(b64) % 4 + if missing_padding: + b64 += "=" * (4 - missing_padding) + decoded = base64.urlsafe_b64decode(b64) + contact = admin_pb2.SharedContact() + contact.ParseFromString(decoded) + return contact + + original = admin_pb2.SharedContact() + original.node_num = 2198819370 + original.user.id = "!830f522a" + original.user.long_name = "Roadrunner Ridge" + original.user.short_name = "RKSN" + original.user.macaddr = b'\x00\x00\x00\x00\x00\x00' + original.user.hw_model = mesh_pb2.HardwareModel.Value("RAK4631") + original.user.role = mesh_pb2.User.DESCRIPTOR.fields_by_name['role'].enum_type.values_by_name["ROUTER"].number + original.user.public_key = bytes.fromhex("471a3f170fdeae0408851a816eb1dab0b632de053e032f21ae1bb83959c83d07") + original.user.is_licensed = True + original.user.is_unmessagable = False + original.should_ignore = True + original.manually_verified = True + + url = encode_url(original) + parsed = decode_url(url) + + assert parsed.node_num == original.node_num + assert parsed.user.id == original.user.id + assert parsed.user.long_name == original.user.long_name + assert parsed.user.short_name == original.user.short_name + assert parsed.user.macaddr == original.user.macaddr + assert parsed.user.hw_model == original.user.hw_model + assert parsed.user.role == original.user.role + assert parsed.user.public_key == original.user.public_key + assert parsed.user.is_licensed == original.user.is_licensed + assert parsed.user.is_unmessagable == original.user.is_unmessagable + assert parsed.should_ignore == original.should_ignore + assert parsed.manually_verified == original.manually_verified + + # TODO # @pytest.mark.unit # def test_showChannels(capsys): From 8c84074c1d1df99b11321433fbcaa8f0a43adf0b Mon Sep 17 00:00:00 2001 From: Ian McEwen Date: Sun, 14 Jun 2026 19:06:45 -0700 Subject: [PATCH 05/17] pylint appeasement --- meshtastic/tests/test_node.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/meshtastic/tests/test_node.py b/meshtastic/tests/test_node.py index 47020f666..6006f99f7 100644 --- a/meshtastic/tests/test_node.py +++ b/meshtastic/tests/test_node.py @@ -3,12 +3,11 @@ import logging import re +import base64 from unittest.mock import MagicMock, patch import pytest -import base64 - from ..protobuf import admin_pb2, localonly_pb2, config_pb2, mesh_pb2 from ..protobuf.channel_pb2 import Channel # pylint: disable=E0611 from ..node import Node From 9d445098f4fd40e92e7a9c0878dcd2418fcc849d Mon Sep 17 00:00:00 2001 From: Ian McEwen Date: Sun, 14 Jun 2026 20:39:26 -0700 Subject: [PATCH 06/17] small fixes --- meshtastic/node.py | 2 +- meshtastic/tests/test_node.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/meshtastic/node.py b/meshtastic/node.py index 51b0eed74..3554cc15b 100644 --- a/meshtastic/node.py +++ b/meshtastic/node.py @@ -403,7 +403,7 @@ def getContactURL(self, node_id: Union[int, str], should_ignore: bool = False, m if u.get("hwModel") and u["hwModel"] != "UNSET": contact.user.hw_model = mesh_pb2.HardwareModel.Value(u["hwModel"]) if u.get("role"): - contact.user.role = u["role"] + contact.user.role = config_pb2.Config.DeviceConfig.Role.Value(u["role"]) if u.get("publicKey"): contact.user.public_key = base64.b64decode(u["publicKey"]) if u.get("isLicensed"): diff --git a/meshtastic/tests/test_node.py b/meshtastic/tests/test_node.py index 6006f99f7..7065fc03e 100644 --- a/meshtastic/tests/test_node.py +++ b/meshtastic/tests/test_node.py @@ -1,9 +1,9 @@ """Meshtastic unit tests for node.py""" # pylint: disable=C0302 +import base64 import logging import re -import base64 from unittest.mock import MagicMock, patch import pytest From 13b8cdcb0482f93465449d438cff660741226ce7 Mon Sep 17 00:00:00 2001 From: Ian McEwen Date: Tue, 16 Jun 2026 20:27:14 -0700 Subject: [PATCH 07/17] Improve test coverage, use property-based tests --- meshtastic/tests/test_node.py | 295 ++++++++++++++++++++++++++++------ 1 file changed, 247 insertions(+), 48 deletions(-) diff --git a/meshtastic/tests/test_node.py b/meshtastic/tests/test_node.py index 7065fc03e..16f7bde11 100644 --- a/meshtastic/tests/test_node.py +++ b/meshtastic/tests/test_node.py @@ -7,12 +7,14 @@ from unittest.mock import MagicMock, patch import pytest +from hypothesis import given, strategies as st -from ..protobuf import admin_pb2, localonly_pb2, config_pb2, mesh_pb2 +from ..protobuf import admin_pb2, localonly_pb2, config_pb2, mesh_pb2, nanopb_pb2 from ..protobuf.channel_pb2 import Channel # pylint: disable=E0611 from ..node import Node from ..serial_interface import SerialInterface from ..mesh_interface import MeshInterface +from ..util import to_node_num # from ..config_pb2 import Config # from ..cannedmessages_pb2 import (CannedMessagePluginMessagePart1, CannedMessagePluginMessagePart2, @@ -20,6 +22,11 @@ # CannedMessagePluginMessagePart5) # from ..util import Timeout +# Extract nanopb max_size constraints from the User protobuf descriptor +_USER_NANOPB = { + field.name: field.GetOptions().Extensions[nanopb_pb2.nanopb] + for field in mesh_pb2.User.DESCRIPTOR.fields +} @pytest.mark.unit def test_node(capsys): @@ -341,53 +348,245 @@ def test_setURL_valid_URL_but_no_settings(capsys): @pytest.mark.unit -def test_contact_url_roundtrip(): - """Verify that contact URL generation and parsing is fully reversible""" - def encode_url(contact): - data = contact.SerializeToString() - s = base64.urlsafe_b64encode(data).decode("ascii") - s = s.replace("=", "").replace("+", "-").replace("/", "_") - return f"https://meshtastic.org/v/#{s}" - - def decode_url(url): - b64 = url.split("/#")[-1] - missing_padding = len(b64) % 4 - if missing_padding: - b64 += "=" * (4 - missing_padding) - decoded = base64.urlsafe_b64decode(b64) - contact = admin_pb2.SharedContact() - contact.ParseFromString(decoded) - return contact - - original = admin_pb2.SharedContact() - original.node_num = 2198819370 - original.user.id = "!830f522a" - original.user.long_name = "Roadrunner Ridge" - original.user.short_name = "RKSN" - original.user.macaddr = b'\x00\x00\x00\x00\x00\x00' - original.user.hw_model = mesh_pb2.HardwareModel.Value("RAK4631") - original.user.role = mesh_pb2.User.DESCRIPTOR.fields_by_name['role'].enum_type.values_by_name["ROUTER"].number - original.user.public_key = bytes.fromhex("471a3f170fdeae0408851a816eb1dab0b632de053e032f21ae1bb83959c83d07") - original.user.is_licensed = True - original.user.is_unmessagable = False - original.should_ignore = True - original.manually_verified = True - - url = encode_url(original) - parsed = decode_url(url) - - assert parsed.node_num == original.node_num - assert parsed.user.id == original.user.id - assert parsed.user.long_name == original.user.long_name - assert parsed.user.short_name == original.user.short_name - assert parsed.user.macaddr == original.user.macaddr - assert parsed.user.hw_model == original.user.hw_model - assert parsed.user.role == original.user.role - assert parsed.user.public_key == original.user.public_key - assert parsed.user.is_licensed == original.user.is_licensed - assert parsed.user.is_unmessagable == original.user.is_unmessagable - assert parsed.should_ignore == original.should_ignore - assert parsed.manually_verified == original.manually_verified +@pytest.mark.parametrize("node_id,node_data,should_ignore,manually_verified", [ + pytest.param( + "!830f522a", + { + "num": 2198819370, + "user": { + "id": "!830f522a", + "longName": "Roadrunner Ridge", + "shortName": "RKSN", + "macaddr": "AAAAAAAAAAA=", + "hwModel": "RAK4631", + "role": "ROUTER", + "publicKey": "Rx8XD96uBAiFGoFusdqwti3eBT4DLyGuG7g5Wcg9Bw==", + "isLicensed": True, + "isUnmessagable": False, + }, + }, + True, + True, + id="all_fields_all_flags", + ), + pytest.param( + "!12345678", + { + "num": 305419896, + "user": { + "id": "!12345678", + "longName": "Test Node", + "shortName": "TN", + "macaddr": "QkVTVEVWRVI=", + "hwModel": "TBEAM", + }, + }, + False, + False, + id="minimal_fields_no_flags", + ), + pytest.param( + 305419896, + { + "num": 305419896, + "user": { + "id": "!12345678", + "longName": "Another Node", + "shortName": "AN", + "macaddr": "QkVTVEVWRVI=", + "hwModel": "HELTEC_V3", + "role": "CLIENT", + "publicKey": "AAAAAAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=", + "isLicensed": False, + }, + }, + True, + False, + id="int_node_id_should_ignore_only", + ), + pytest.param( + "!deadbeef", + { + "num": 3735928559, + "user": { + "id": "!deadbeef", + "longName": "Minimal Contact", + "shortName": "MC", + "macaddr": "BQYHCAkKCw==", + "hwModel": "UNSET", + "role": "CLIENT_MUTE", + }, + }, + False, + True, + id="unset_hw_model_verified_only", + ), + pytest.param( + "!1a2b3c4d", + { + "num": 439041101, + "user": { + "id": "!1a2b3c4d", + "longName": "Licensed Node", + "shortName": "LN", + "macaddr": "DA0ODxAREg==", + "hwModel": "NANO_G1", + "isLicensed": True, + "isUnmessagable": True, + }, + }, + False, + False, + id="licensed_unmessagable_no_flags", + ), +]) +def test_contact_url_roundtrip(node_id, node_data, should_ignore, manually_verified): + """Verify that contact URL generation via getContactURL() and parsing via addContactURL() is fully reversible""" + iface = MagicMock(autospec=MeshInterface) + node_num = to_node_num(node_id) + iface.nodesByNum = {node_num: node_data} + iface.localNode = None + + anode = Node(iface, node_num, noProto=True) + + sent_admin = [] + def capture_send(p, *args, **kwargs): + sent_admin.append(p) + + with patch.object(anode, "_sendAdmin", side_effect=capture_send): + url = anode.getContactURL(node_id, should_ignore=should_ignore, manually_verified=manually_verified) + assert url.startswith("https://meshtastic.org/v/#") + + anode.addContactURL(url) + + assert len(sent_admin) == 1 + contact = sent_admin[0].add_contact + u = node_data["user"] + + assert contact.node_num == node_num + assert contact.user.id == u["id"] + assert contact.user.long_name == u["longName"] + assert contact.user.short_name == u["shortName"] + assert contact.user.macaddr == base64.b64decode(u["macaddr"]) + + if u.get("hwModel") and u["hwModel"] != "UNSET": + assert contact.user.hw_model == mesh_pb2.HardwareModel.Value(u["hwModel"]) + if u.get("role"): + assert contact.user.role == config_pb2.Config.DeviceConfig.Role.Value(u["role"]) + if u.get("publicKey"): + assert contact.user.public_key == base64.b64decode(u["publicKey"]) + if u.get("isLicensed"): + assert contact.user.is_licensed is True + if u.get("isUnmessagable") is not None: + assert contact.user.is_unmessagable == u["isUnmessagable"] + + assert contact.should_ignore == should_ignore + assert contact.manually_verified == manually_verified + + +@st.composite +def contact_url_roundtrip_params(draw): + """Hypothesis strategy: generate a full node config and roundtrip flags""" + should_ignore = draw(st.booleans()) + manually_verified = draw(st.booleans()) + + node_num = draw(st.integers(min_value=6, max_value=2**32 - 2)) + node_id = f"!{node_num:08x}" + + hw_model = draw(st.sampled_from(list(mesh_pb2.HardwareModel.keys()))) + role = draw(st.one_of( + st.none(), + st.sampled_from(list(config_pb2.Config.DeviceConfig.Role.keys())), + )) + + long_name = draw(st.text( + min_size=1, max_size=_USER_NANOPB['long_name'].max_size + )) + short_name = draw(st.text( + min_size=1, max_size=_USER_NANOPB['short_name'].max_size + )) + + macaddr_bytes = draw(st.binary( + min_size=_USER_NANOPB['macaddr'].max_size, + max_size=_USER_NANOPB['macaddr'].max_size, + )) + macaddr_b64 = base64.b64encode(macaddr_bytes).decode("ascii") + + has_public_key = draw(st.booleans()) + public_key_b64 = None + if has_public_key: + pk_bytes = draw(st.binary( + min_size=_USER_NANOPB['public_key'].max_size, + max_size=_USER_NANOPB['public_key'].max_size, + )) + public_key_b64 = base64.b64encode(pk_bytes).decode("ascii") + + is_licensed = draw(st.booleans()) + is_unmessagable = draw(st.booleans()) + + node_data = { + "num": node_num, + "user": { + "id": node_id, + "longName": long_name, + "shortName": short_name, + "macaddr": macaddr_b64, + "hwModel": hw_model, + "isLicensed": is_licensed, + "isUnmessagable": is_unmessagable, + }, + } + if role is not None: + node_data["user"]["role"] = role + if public_key_b64 is not None: + node_data["user"]["publicKey"] = public_key_b64 + + return node_num, node_data, should_ignore, manually_verified + + +@pytest.mark.unit +@given(contact_url_roundtrip_params()) +def test_contact_url_roundtrip_hypothesis(params): + """Property: roundtrip preserves data across random field configurations""" + node_num, node_data, should_ignore, manually_verified = params + + iface = MagicMock(autospec=MeshInterface) + iface.nodesByNum = {node_num: node_data} + iface.localNode = None + + anode = Node(iface, node_num, noProto=True) + + sent_admin = [] + def capture_send(p, *args, **kwargs): + sent_admin.append(p) + + with patch.object(anode, "_sendAdmin", side_effect=capture_send): + url = anode.getContactURL( + node_num, + should_ignore=should_ignore, + manually_verified=manually_verified, + ) + anode.addContactURL(url) + + assert len(sent_admin) == 1 + contact = sent_admin[0].add_contact + u = node_data["user"] + + assert contact.node_num == node_num + assert contact.user.id == u["id"] + assert contact.user.long_name == u["longName"] + assert contact.user.short_name == u["shortName"] + assert contact.user.macaddr == base64.b64decode(u["macaddr"]) + assert contact.user.hw_model == mesh_pb2.HardwareModel.Value(u["hwModel"]) + + if "role" in u: + assert contact.user.role == config_pb2.Config.DeviceConfig.Role.Value(u["role"]) + if "publicKey" in u: + assert contact.user.public_key == base64.b64decode(u["publicKey"]) + assert contact.user.is_licensed == u["isLicensed"] + assert contact.user.is_unmessagable == u["isUnmessagable"] + assert contact.should_ignore == should_ignore + assert contact.manually_verified == manually_verified # TODO From db746a09812cbb248f051b8ef64728a68ef4ee76 Mon Sep 17 00:00:00 2001 From: Ian McEwen Date: Tue, 16 Jun 2026 20:32:38 -0700 Subject: [PATCH 08/17] pylint appeasement --- meshtastic/tests/test_node.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meshtastic/tests/test_node.py b/meshtastic/tests/test_node.py index 16f7bde11..ce643c1e6 100644 --- a/meshtastic/tests/test_node.py +++ b/meshtastic/tests/test_node.py @@ -450,7 +450,7 @@ def test_contact_url_roundtrip(node_id, node_data, should_ignore, manually_verif anode = Node(iface, node_num, noProto=True) sent_admin = [] - def capture_send(p, *args, **kwargs): + def capture_send(p, *_args, **_kwargs): sent_admin.append(p) with patch.object(anode, "_sendAdmin", side_effect=capture_send): @@ -557,7 +557,7 @@ def test_contact_url_roundtrip_hypothesis(params): anode = Node(iface, node_num, noProto=True) sent_admin = [] - def capture_send(p, *args, **kwargs): + def capture_send(p, *_args, **_kwargs): sent_admin.append(p) with patch.object(anode, "_sendAdmin", side_effect=capture_send): From 405a6bbc5d0702113e193fbcc2089656cc065e4d Mon Sep 17 00:00:00 2001 From: Ian McEwen Date: Tue, 16 Jun 2026 20:36:06 -0700 Subject: [PATCH 09/17] Fix an issue in to_node_num and add a bunch of tests to it as well --- meshtastic/tests/test_util.py | 67 +++++++++++++++++++++++++++++++++++ meshtastic/util.py | 2 +- 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/meshtastic/tests/test_util.py b/meshtastic/tests/test_util.py index 916417ae8..ae5370d47 100644 --- a/meshtastic/tests/test_util.py +++ b/meshtastic/tests/test_util.py @@ -37,6 +37,7 @@ stripnl, support_info, message_to_json, + to_node_num, Acknowledgment ) @@ -715,3 +716,69 @@ def test_generate_channel_hash_fuzz_aes256(channel_name, key_bytes): "Test generate_channel_hash with fuzzed channel names and 256-bit keys, ensuring it produces single-byte values" hashed = generate_channel_hash(channel_name, key_bytes) assert 0 <= hashed <= 0xFF + + +@pytest.mark.unit +@pytest.mark.parametrize("input_val,expected", [ + # int passthrough + (0, 0), + (1, 1), + (6, 6), + (502009325, 502009325), + (2198819370, 2198819370), + (0xFFFFFFFF, 0xFFFFFFFF), + # !hex format (always treated as hex) + ("!00000000", 0x00000000), + ("!00000001", 0x00000001), + ("!00000010", 0x00000010), + ("!000000ff", 0x000000FF), + ("!830f522a", 0x830F522A), + ("!1dec0ded", 0x1DEC0DED), + ("!ffffffff", 0xFFFFFFFF), + ("!FFFFFFFF", 0xFFFFFFFF), + # 0xhex format + ("0x00000000", 0x00000000), + ("0x00000010", 0x00000010), + ("0x830f522a", 0x830F522A), + ("0x1dec0ded", 0x1DEC0DED), + ("0xFFFFFFFF", 0xFFFFFFFF), + # Unprefixed hex string (falls back to hex when decimal fails) + ("830f522a", 0x830F522A), + ("1dec0ded", 0x1DEC0DED), + # Decimal string + ("42", 42), + ("12345678", 12345678), + ("0", 0), + ("1", 1), + # With whitespace + (" !830f522a ", 2198819370), + (" !00000010 ", 16), + (" 0x830f522a ", 2198819370), +]) +def test_to_node_num(input_val, expected): + """Test to_node_num with various valid inputs""" + assert to_node_num(input_val) == expected + + +@pytest.mark.unit +@pytest.mark.parametrize("input_val", [ + "", + "!", + "!!", + "!0x10", + "!xyz", +]) +def test_to_node_num_invalid(input_val): + """Test to_node_num raises ValueError for invalid inputs""" + with pytest.raises(ValueError): + to_node_num(input_val) + + +@pytest.mark.unit +@given(st.integers(min_value=0, max_value=2**32 - 1)) +def test_to_node_num_hypothesis_roundtrip(n): + """Property: all supported input formats roundtrip for any valid node number""" + assert to_node_num(n) == n + assert to_node_num(f"!{n:08x}") == n + assert to_node_num(f"0x{n:x}") == n + assert to_node_num(str(n)) == n diff --git a/meshtastic/util.py b/meshtastic/util.py index f11a3af63..f4447095f 100644 --- a/meshtastic/util.py +++ b/meshtastic/util.py @@ -728,7 +728,7 @@ def to_node_num(node_id: Union[int, str]) -> int: return node_id s = str(node_id).strip() if s.startswith("!"): - s = s[1:] + s = "0x" + s[1:] if s.lower().startswith("0x"): return int(s, 16) try: From c1183349335f2cbd96520f9d25100376a739cf08 Mon Sep 17 00:00:00 2001 From: Ian McEwen Date: Thu, 18 Jun 2026 20:55:13 -0700 Subject: [PATCH 10/17] Add tests for the flags_to_list util function --- meshtastic/tests/test_util.py | 62 ++++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/meshtastic/tests/test_util.py b/meshtastic/tests/test_util.py index ae5370d47..e65881ba6 100644 --- a/meshtastic/tests/test_util.py +++ b/meshtastic/tests/test_util.py @@ -9,7 +9,7 @@ from hypothesis import given, strategies as st from meshtastic.supported_device import SupportedDevice -from meshtastic.protobuf import mesh_pb2 +from meshtastic.protobuf import mesh_pb2, config_pb2 from meshtastic.util import ( DEFAULT_KEY, Timeout, @@ -20,6 +20,7 @@ convert_mac_addr, eliminate_duplicate_port, findPorts, + flags_to_list, fixme, fromPSK, fromStr, @@ -782,3 +783,62 @@ def test_to_node_num_hypothesis_roundtrip(n): assert to_node_num(f"!{n:08x}") == n assert to_node_num(f"0x{n:x}") == n assert to_node_num(str(n)) == n + + +_EXCLUDED_MODULES = mesh_pb2.ExcludedModules +_POSITION_FLAGS = config_pb2.Config.PositionConfig.PositionFlags + + +@pytest.mark.unit +@pytest.mark.parametrize("flag_type,flags,expected", [ + (_EXCLUDED_MODULES, 0, []), + (_EXCLUDED_MODULES, 1, ["MQTT_CONFIG"]), + (_EXCLUDED_MODULES, 3, ["MQTT_CONFIG", "SERIAL_CONFIG"]), + (_EXCLUDED_MODULES, 0x7FFF, [ + "MQTT_CONFIG", "SERIAL_CONFIG", "EXTNOTIF_CONFIG", "STOREFORWARD_CONFIG", + "RANGETEST_CONFIG", "TELEMETRY_CONFIG", "CANNEDMSG_CONFIG", "AUDIO_CONFIG", + "REMOTEHARDWARE_CONFIG", "NEIGHBORINFO_CONFIG", "AMBIENTLIGHTING_CONFIG", + "DETECTIONSENSOR_CONFIG", "PAXCOUNTER_CONFIG", "BLUETOOTH_CONFIG", + "NETWORK_CONFIG", + ]), + (_EXCLUDED_MODULES, 0x8000, ["UNKNOWN_ADDITIONAL_FLAGS(32768)"]), + (_EXCLUDED_MODULES, 0x8001, ["MQTT_CONFIG", "UNKNOWN_ADDITIONAL_FLAGS(32768)"]), + (_POSITION_FLAGS, 0, []), + (_POSITION_FLAGS, 0x09, ["ALTITUDE", "DOP"]), + (_POSITION_FLAGS, 0x1FF, [ + "ALTITUDE", "ALTITUDE_MSL", "GEOIDAL_SEPARATION", "DOP", "HVDOP", + "SATINVIEW", "SEQ_NO", "TIMESTAMP", "HEADING", + ]), +]) +def test_flags_to_list(flag_type, flags, expected): + """Test flags_to_list decodes set bits in enum order and reports unknown remainders.""" + assert flags_to_list(flag_type, flags) == expected + + +@pytest.mark.unit +@given(st.integers(min_value=0, max_value=0xFFFFF)) +def test_flags_to_list_conservation(flags): + """Property: flags_to_list partitions `flags` into known names plus an exact unknown remainder. + + Every known bit that is set must appear as a name, and the leftover reported in + UNKNOWN_ADDITIONAL_FLAGS(...) must together with the named bits reconstruct the input. + """ + for flag_type in (_EXCLUDED_MODULES, _POSITION_FLAGS): + known_union = 0 + for key in flag_type.keys(): + value = flag_type.Value(key) + if key != "EXCLUDED_NONE" and value: + known_union |= value + + result = flags_to_list(flag_type, flags) + + accounted = 0 + leftover = 0 + for name in result: + if name.startswith("UNKNOWN_ADDITIONAL_FLAGS("): + leftover = int(name[len("UNKNOWN_ADDITIONAL_FLAGS("):-1]) + else: + accounted |= flag_type.Value(name) + + assert accounted == (flags & known_union) + assert (accounted | leftover) == flags From f0de977be50835f363a37d469c3e9d6bfcd2f679 Mon Sep 17 00:00:00 2001 From: Ian McEwen Date: Thu, 25 Jun 2026 14:07:36 -0700 Subject: [PATCH 11/17] Some more test improvements --- meshtastic/tests/test_showNodes_favorite.py | 19 ---- meshtastic/tests/test_util.py | 117 ++++++++++++++++++-- 2 files changed, 105 insertions(+), 31 deletions(-) diff --git a/meshtastic/tests/test_showNodes_favorite.py b/meshtastic/tests/test_showNodes_favorite.py index 1ddd47b25..3c8f7ee44 100644 --- a/meshtastic/tests/test_showNodes_favorite.py +++ b/meshtastic/tests/test_showNodes_favorite.py @@ -153,25 +153,6 @@ def test_showNodes_favorite_asterisk_display(capsys, _iface_with_favorite_nodes) assert err == "" -@pytest.mark.unit -def test_showNodes_favorite_field_formatting(): - """Test the formatting logic for isFavorite field""" - # Test favorite node - raw_value = True - formatted_value = "*" if raw_value else "" - assert formatted_value == "*" - - # Test non-favorite node - raw_value = False - formatted_value = "*" if raw_value else "" - assert formatted_value == "" - - # Test None/missing value - raw_value = None - formatted_value = "*" if raw_value else "" - assert formatted_value == "" - - @pytest.mark.unit def test_showNodes_with_custom_fields_including_favorite(capsys, _iface_with_favorite_nodes): """Test that isFavorite can be specified in custom showFields""" diff --git a/meshtastic/tests/test_util.py b/meshtastic/tests/test_util.py index e65881ba6..e682538c4 100644 --- a/meshtastic/tests/test_util.py +++ b/meshtastic/tests/test_util.py @@ -3,6 +3,8 @@ import json import logging import re +import time +from types import SimpleNamespace from unittest.mock import patch import pytest @@ -236,20 +238,111 @@ def test_remove_keys_from_dict_nested(): assert remove_keys_from_dict(keys, adict) == exp -@pytest.mark.unitslow -def test_Timeout_not_found(): - """Test Timeout()""" - to = Timeout(0.2) - attrs = "foo" - to.waitForSet("bar", attrs) +@pytest.mark.unit +def test_Timeout_waitForSet_found(): + """waitForSet returns True when all required attrs are truthy on target.""" + to = Timeout(0.01) + target = SimpleNamespace(foo=1, bar="ok") + assert to.waitForSet(target, ("foo", "bar")) is True -@pytest.mark.unitslow -def test_Timeout_found(): - """Test Timeout()""" - to = Timeout(0.2) - attrs = () - to.waitForSet("bar", attrs) +@pytest.mark.unit +@patch("meshtastic.util.time.sleep") +def test_Timeout_waitForSet_not_found(mock_sleep): # pylint: disable=unused-argument + """waitForSet returns False when attrs remain falsy until timeout.""" + to = Timeout(0.01) + target = SimpleNamespace(foo=None, bar=0) + assert to.waitForSet(target, ("foo", "bar")) is False + + +@pytest.mark.unit +def test_Timeout_waitForSet_empty_attrs(): + """waitForSet returns True immediately when attrs is empty (vacuous truth).""" + to = Timeout(0.01) + assert to.waitForSet(object(), ()) is True + + +@pytest.mark.unit +def test_Timeout_reset(): + """reset() sets expireTime to now + expireTimeout.""" + to = Timeout(maxSecs=5) + before = time.time() + to.reset() + assert to.expireTime == pytest.approx(before + 5, abs=0.1) + + +@pytest.mark.unit +def test_Timeout_reset_custom(): + """reset(expireTimeout) overrides the stored expireTimeout.""" + to = Timeout(maxSecs=5) + before = time.time() + to.reset(expireTimeout=10) + assert to.expireTime == pytest.approx(before + 10, abs=0.1) + + +@pytest.mark.unit +def test_Timeout_waitForAckNak_found(): + """waitForAckNak returns True when any acknowledgment attr is set, and clears it.""" + to = Timeout(0.01) + ack = Acknowledgment() + ack.receivedAck = True + assert to.waitForAckNak(ack) is True + assert ack.receivedAck is False # cleared after detection + + +@pytest.mark.unit +@patch("meshtastic.util.time.sleep") +def test_Timeout_waitForAckNak_not_found(mock_sleep): # pylint: disable=unused-argument + """waitForAckNak returns False when no acknowledgment attr is set.""" + to = Timeout(0.01) + ack = Acknowledgment() + assert to.waitForAckNak(ack) is False + + +@pytest.mark.unit +def test_Timeout_waitForTraceRoute_found(): + """waitForTraceRoute returns True on receivedTraceRoute, and clears it.""" + to = Timeout(0.01) + ack = Acknowledgment() + ack.receivedTraceRoute = True + assert to.waitForTraceRoute(3.0, ack) is True + assert ack.receivedTraceRoute is False + + +@pytest.mark.unit +@patch("meshtastic.util.time.sleep") +def test_Timeout_waitForTraceRoute_not_found(mock_sleep): # pylint: disable=unused-argument + """waitForTraceRoute returns False on timeout.""" + to = Timeout(0.01) + ack = Acknowledgment() + assert to.waitForTraceRoute(3.0, ack) is False + + +@pytest.mark.unit +def test_Timeout_waitForTelemetry_found(): + """waitForTelemetry returns True when receivedTelemetry is set.""" + to = Timeout(0.01) + ack = Acknowledgment() + ack.receivedTelemetry = True + assert to.waitForTelemetry(ack) is True + + +@pytest.mark.unit +def test_Timeout_waitForPosition_found(): + """waitForPosition returns True when receivedPosition is set.""" + to = Timeout(0.01) + ack = Acknowledgment() + ack.receivedPosition = True + assert to.waitForPosition(ack) is True + + +@pytest.mark.unit +def test_Timeout_waitForWaypoint_found(): + """waitForWaypoint returns True when receivedWaypoint is set.""" + to = Timeout(0.01) + ack = Acknowledgment() + ack.receivedWaypoint = True + assert to.waitForWaypoint(ack) is True @pytest.mark.unitslow From 7fc69e957c637b9582361540ba6932b75bd10301 Mon Sep 17 00:00:00 2001 From: Ian McEwen Date: Thu, 25 Jun 2026 14:28:32 -0700 Subject: [PATCH 12/17] Fail earlier if the ota update file isn't found --- meshtastic/__main__.py | 5 +++++ meshtastic/tests/test_main.py | 3 +++ 2 files changed, 8 insertions(+) diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index 051a7a8e8..9fa2a4aae 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -1383,6 +1383,11 @@ def common(): if not stripped_ham_name: meshtastic.util.our_exit("ERROR: Ham radio callsign cannot be empty or contain only whitespace characters") + # Early validation for OTA firmware file before attempting device connection + if hasattr(args, 'ota_update') and args.ota_update is not None: + if not os.path.isfile(args.ota_update): + meshtastic.util.our_exit(f"Error: OTA firmware file not found: {args.ota_update}", 1) + if have_powermon: create_power_meter() diff --git a/meshtastic/tests/test_main.py b/meshtastic/tests/test_main.py index 772717cb6..90dc8579d 100644 --- a/meshtastic/tests/test_main.py +++ b/meshtastic/tests/test_main.py @@ -3157,6 +3157,9 @@ def test_main_ota_update_file_not_found(capsys): assert pytest_wrapped_e.type == SystemExit assert pytest_wrapped_e.value.code == 1 + out, err = capsys.readouterr() + assert "OTA firmware file not found" in out + assert "/nonexistent/firmware.bin" in out @pytest.mark.unit From 990ecc23506cea115cc41a2856da9d3453df98bf Mon Sep 17 00:00:00 2001 From: Ian McEwen Date: Thu, 25 Jun 2026 14:46:45 -0700 Subject: [PATCH 13/17] Better document how to specify a port when using TCP connection Fixes #868 Also, fix a pylint complaint. --- meshtastic/__main__.py | 4 +++- meshtastic/tests/test_main.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index 9fa2a4aae..4e7556a96 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -1621,10 +1621,12 @@ def addConnectionArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentParse "--host", "--tcp", "-t", - help="Connect to a device using TCP, optionally passing hostname or IP address to use. (defaults to '%(const)s')", + help=("Connect to a device using TCP, optionally passing hostname or IP address to use. (defaults to '%(const)s'). " + "A port number may be specified as well, e.g. meshtastic.local:4404. The default port is 4403."), nargs="?", default=None, const="localhost", + metavar="HOST[:PORT]", ) group.add_argument( diff --git a/meshtastic/tests/test_main.py b/meshtastic/tests/test_main.py index 90dc8579d..b2dda97cd 100644 --- a/meshtastic/tests/test_main.py +++ b/meshtastic/tests/test_main.py @@ -3157,7 +3157,7 @@ def test_main_ota_update_file_not_found(capsys): assert pytest_wrapped_e.type == SystemExit assert pytest_wrapped_e.value.code == 1 - out, err = capsys.readouterr() + out, _ = capsys.readouterr() assert "OTA firmware file not found" in out assert "/nonexistent/firmware.bin" in out From dcdbda0524ff7e974080054d3285748b1b2073cc Mon Sep 17 00:00:00 2001 From: Ian McEwen Date: Fri, 26 Jun 2026 11:01:19 -0700 Subject: [PATCH 14/17] feat: add named-flag/power-of-2 bitfield support to --set This enables e.g. `--set position.position_flags ALTITUDE,SPEED` rather than the dedicated `--pos-flags`, and similarly for `network.enabled_protocols`. Displaying these named values when printing out configurations is not yet implemented. --- meshtastic/__main__.py | 26 ++++++++++++++++++- meshtastic/tests/test_main.py | 47 +++++++++++++++++++++++++++++++++++ meshtastic/tests/test_util.py | 35 ++++++++++++++++++++++++++ meshtastic/util.py | 33 +++++++++++++++++++++--- 4 files changed, 137 insertions(+), 4 deletions(-) diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index 4e7556a96..21453d8a8 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -66,6 +66,14 @@ logger = logging.getLogger(__name__) +# Map dotted preference paths to the protobuf enum that defines their flags. +# These fields are stored as uint32 bitmasks in the protobuf but have an +# associated enum that names the individual flags. +BITFIELD_ENUMS = { + "network.enabled_protocols": config_pb2.Config.NetworkConfig.ProtocolFlags, + "position.position_flags": config_pb2.Config.PositionConfig.PositionFlags, +} + def onReceive(packet, interface) -> None: """Callback invoked when a packet arrives""" args = mt_config.args @@ -238,6 +246,21 @@ def setPref(config, comp_name, raw_val) -> bool: print("Warning: network.wifi_psk must be 8 or more characters.") return False + # Handle uint32 bitfields that have an associated enum of flag names. + bitfield_enum = None + if config_type.message_type is not None: + bitfield_path = f"{config_type.name}.{pref.name}" + bitfield_enum = BITFIELD_ENUMS.get(bitfield_path) + if bitfield_enum and isinstance(val, str): + # At this point fromStr() could not parse val as int/float/bool/bytes, + # so treat it as a comma-separated list of bitfield flag names. + flag_names = [name.strip() for name in val.split(",") if name.strip()] + try: + val = meshtastic.util.flags_from_list(bitfield_enum, flag_names) + except ValueError as e: + print(f"ERROR: {e}") + return False + enumType = pref.enum_type # pylint: disable=C0123 if enumType and type(val) == str: @@ -1956,7 +1979,8 @@ def addPositionConfigArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentP group.add_argument( "--pos-fields", - help="Specify fields to send when sending a position. Use no argument for a list of valid values. " + help="Deprecated: use '--set position.position_flags FLAG1,FLAG2' instead. " + "Specify fields to send when sending a position. Use no argument for a list of valid values. " "Can pass multiple values as a space separated list like " "this: '--pos-fields ALTITUDE HEADING SPEED'", nargs="*", diff --git a/meshtastic/tests/test_main.py b/meshtastic/tests/test_main.py index b2dda97cd..3ca0564a6 100644 --- a/meshtastic/tests/test_main.py +++ b/meshtastic/tests/test_main.py @@ -20,12 +20,14 @@ onConnection, onNode, onReceive, + setPref, tunnelMain, set_missing_flags_false, ) from meshtastic import mt_config from ..protobuf.channel_pb2 import Channel # pylint: disable=E0611 +from ..protobuf.config_pb2 import Config # pylint: disable=E0611 # from ..ble_interface import BLEInterface from ..mesh_interface import MeshInterface @@ -3204,3 +3206,48 @@ def test_main_ota_update_retries(mock_our_exit, mock_ota_class, capsys): finally: os.unlink(firmware_file) + + +@pytest.mark.unit +@pytest.mark.usefixtures("reset_mt_config") +def test_main_setPref_network_enabled_protocols_by_name(capsys): + """Test setPref() accepts bitfield flag names for network.enabled_protocols.""" + config = Config() + assert setPref(config, "network.enabled_protocols", "UDP_BROADCAST") is True + assert config.network.enabled_protocols == 1 + out, _ = capsys.readouterr() + assert "Set network.enabled_protocols to UDP_BROADCAST" in out + + +@pytest.mark.unit +@pytest.mark.usefixtures("reset_mt_config") +def test_main_setPref_position_flags_multiple(capsys): + """Test setPref() accepts comma-separated bitfield flag names.""" + config = Config() + assert setPref(config, "position.position_flags", "ALTITUDE,SPEED") is True + assert config.position.position_flags == 513 + out, _ = capsys.readouterr() + assert "Set position.position_flags to ALTITUDE,SPEED" in out + + +@pytest.mark.unit +@pytest.mark.usefixtures("reset_mt_config") +def test_main_setPref_bitfield_raw_integer(capsys): + """Test setPref() still accepts raw integers for bitfields.""" + config = Config() + assert setPref(config, "network.enabled_protocols", "0") is True + assert config.network.enabled_protocols == 0 + out, _ = capsys.readouterr() + assert "Set network.enabled_protocols to 0" in out + + +@pytest.mark.unit +@pytest.mark.usefixtures("reset_mt_config") +def test_main_setPref_bitfield_invalid_name(capsys): + """Test setPref() rejects unknown bitfield flag names.""" + config = Config() + assert setPref(config, "network.enabled_protocols", "TCP") is False + out, _ = capsys.readouterr() + assert "Unknown flag 'TCP'" in out + assert "NO_BROADCAST" in out + assert "UDP_BROADCAST" in out diff --git a/meshtastic/tests/test_util.py b/meshtastic/tests/test_util.py index e682538c4..db4701b79 100644 --- a/meshtastic/tests/test_util.py +++ b/meshtastic/tests/test_util.py @@ -22,6 +22,7 @@ convert_mac_addr, eliminate_duplicate_port, findPorts, + flags_from_list, flags_to_list, fixme, fromPSK, @@ -880,6 +881,7 @@ def test_to_node_num_hypothesis_roundtrip(n): _EXCLUDED_MODULES = mesh_pb2.ExcludedModules _POSITION_FLAGS = config_pb2.Config.PositionConfig.PositionFlags +_NETWORK_PROTOCOLS = config_pb2.Config.NetworkConfig.ProtocolFlags @pytest.mark.unit @@ -935,3 +937,36 @@ def test_flags_to_list_conservation(flags): assert accounted == (flags & known_union) assert (accounted | leftover) == flags + + +@pytest.mark.unit +@pytest.mark.parametrize("flag_type, flags, expected", [ + (_NETWORK_PROTOCOLS, ["UDP_BROADCAST"], 1), + (_NETWORK_PROTOCOLS, ["NO_BROADCAST"], 0), + (_NETWORK_PROTOCOLS, [], 0), + (_POSITION_FLAGS, ["ALTITUDE"], 1), + (_POSITION_FLAGS, ["ALTITUDE", "SPEED"], 513), + (_POSITION_FLAGS, ["ALTITUDE", " SPEED "], 513), +]) +def test_flags_from_list(flag_type, flags, expected): + """Test flags_from_list combines named flags into the expected bitmask.""" + assert flags_from_list(flag_type, flags) == expected + + +@pytest.mark.unit +def test_flags_from_list_unknown_flag(): + """Test flags_from_list raises ValueError for an unknown flag name.""" + with pytest.raises(ValueError, match="Unknown flag 'TCP'"): + flags_from_list(_NETWORK_PROTOCOLS, ["UDP_BROADCAST", "TCP"]) + + +@pytest.mark.unit +@given(st.lists(st.sampled_from(list(_POSITION_FLAGS.keys())), unique=True)) +def test_flags_from_list_roundtrip(flags): + """Property: flags_from_list and flags_to_list are inverses for known position flags.""" + combined = flags_from_list(_POSITION_FLAGS, flags) + decoded = flags_to_list(_POSITION_FLAGS, combined) + # flags_to_list drops zero-value flags and may report unknown remainders, + # but for combinations of known non-zero flags it should return the same set of names. + nonzero_flags = {f for f in flags if _POSITION_FLAGS.Value(f)} + assert set(decoded) == nonzero_flags diff --git a/meshtastic/util.py b/meshtastic/util.py index f4447095f..7faa2d6c0 100644 --- a/meshtastic/util.py +++ b/meshtastic/util.py @@ -737,14 +737,41 @@ def to_node_num(node_id: Union[int, str]) -> int: return int(s, 16) def flags_to_list(flag_type, flags: int) -> List[str]: - """Given a flag_type that's a protobuf EnumTypeWrapper, and a flag int, give a list of flags enabled.""" + """Given a flag_type that's a protobuf EnumTypeWrapper, and a flag int, give a list of flags enabled. + + Zero-valued members (e.g. EXCLUDED_NONE, UNSET, NO_BROADCAST) never appear in the + result: they hold no bit, so `flags & value` is always False for them, and a flags + value of 0 therefore decodes to an empty list rather than a named "no flags" entry. + Any leftover bits not corresponding to a known member are reported via + `UNKNOWN_ADDITIONAL_FLAGS()`. + """ ret = [] for key in flag_type.keys(): - if key == "EXCLUDED_NONE": - continue if flags & flag_type.Value(key): ret.append(key) flags = flags - flag_type.Value(key) if flags > 0: ret.append(f"UNKNOWN_ADDITIONAL_FLAGS({flags})") return ret + + +def flags_from_list(flag_type, flags: List[str]) -> int: + """Given a flag_type that's a protobuf EnumTypeWrapper, and a list of flag names, return the combined bitmask. + + Zero-valued members (e.g. EXCLUDED_NONE, UNSET, NO_BROADCAST) are accepted but are + no-ops: they OR in 0 and thus set nothing. A list consisting solely of such a member + (or an empty list) yields 0, which round-trips back through flags_to_list as an + empty list rather than the original member name -- see flags_to_list's docstring. + """ + result = 0 + valid_names = list(flag_type.keys()) + for flag_name in flags: + flag_name = flag_name.strip() + if not flag_name: + continue + if flag_name not in valid_names: + raise ValueError( + f"Unknown flag '{flag_name}'. Valid choices: {', '.join(sorted(valid_names))}" + ) + result |= flag_type.Value(flag_name) + return result From fa01cb7a5d8df08aed699242e65f406ba2c9e866 Mon Sep 17 00:00:00 2001 From: Ian McEwen Date: Fri, 26 Jun 2026 11:09:15 -0700 Subject: [PATCH 15/17] Fix mypy complaint --- meshtastic/__main__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index 21453d8a8..9ec00bede 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -265,9 +265,9 @@ def setPref(config, comp_name, raw_val) -> bool: # pylint: disable=C0123 if enumType and type(val) == str: # We've failed so far to convert this string into an enum, try to find it by reflection - e = enumType.values_by_name.get(val) - if e: - val = e.number + ev = enumType.values_by_name.get(val) + if ev: + val = ev.number else: print( f"{name[0]}.{uni_name} does not have an enum called {val}, so you can not set it." From 74cce6bab9408b12891c5caa275c9941d63387c5 Mon Sep 17 00:00:00 2001 From: Ian McEwen Date: Fri, 26 Jun 2026 12:51:57 -0700 Subject: [PATCH 16/17] protobufs: v2.7.26 --- meshtastic/protobuf/mesh_pb2.py | 20 ++++++++++---------- meshtastic/protobuf/mesh_pb2.pyi | 32 ++++++++++++++++++++++++++++++++ protobufs | 2 +- 3 files changed, 43 insertions(+), 11 deletions(-) diff --git a/meshtastic/protobuf/mesh_pb2.py b/meshtastic/protobuf/mesh_pb2.py index ef13bd98d..5cc8b5867 100644 --- a/meshtastic/protobuf/mesh_pb2.py +++ b/meshtastic/protobuf/mesh_pb2.py @@ -21,7 +21,7 @@ from meshtastic.protobuf import nanopb_pb2 as meshtastic_dot_protobuf_dot_nanopb__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1emeshtastic/protobuf/mesh.proto\x12\x13meshtastic.protobuf\x1a!meshtastic/protobuf/channel.proto\x1a meshtastic/protobuf/config.proto\x1a#meshtastic/protobuf/device_ui.proto\x1a\'meshtastic/protobuf/module_config.proto\x1a\"meshtastic/protobuf/portnums.proto\x1a#meshtastic/protobuf/telemetry.proto\x1a meshtastic/protobuf/xmodem.proto\x1a meshtastic/protobuf/nanopb.proto\"\x99\x07\n\x08Position\x12\x17\n\nlatitude_i\x18\x01 \x01(\x0fH\x00\x88\x01\x01\x12\x18\n\x0blongitude_i\x18\x02 \x01(\x0fH\x01\x88\x01\x01\x12\x15\n\x08\x61ltitude\x18\x03 \x01(\x05H\x02\x88\x01\x01\x12\x0c\n\x04time\x18\x04 \x01(\x07\x12@\n\x0flocation_source\x18\x05 \x01(\x0e\x32\'.meshtastic.protobuf.Position.LocSource\x12@\n\x0f\x61ltitude_source\x18\x06 \x01(\x0e\x32\'.meshtastic.protobuf.Position.AltSource\x12\x11\n\ttimestamp\x18\x07 \x01(\x07\x12\x1f\n\x17timestamp_millis_adjust\x18\x08 \x01(\x05\x12\x19\n\x0c\x61ltitude_hae\x18\t \x01(\x11H\x03\x88\x01\x01\x12(\n\x1b\x61ltitude_geoidal_separation\x18\n \x01(\x11H\x04\x88\x01\x01\x12\x0c\n\x04PDOP\x18\x0b \x01(\r\x12\x0c\n\x04HDOP\x18\x0c \x01(\r\x12\x0c\n\x04VDOP\x18\r \x01(\r\x12\x14\n\x0cgps_accuracy\x18\x0e \x01(\r\x12\x19\n\x0cground_speed\x18\x0f \x01(\rH\x05\x88\x01\x01\x12\x19\n\x0cground_track\x18\x10 \x01(\rH\x06\x88\x01\x01\x12\x13\n\x0b\x66ix_quality\x18\x11 \x01(\r\x12\x10\n\x08\x66ix_type\x18\x12 \x01(\r\x12\x14\n\x0csats_in_view\x18\x13 \x01(\r\x12\x11\n\tsensor_id\x18\x14 \x01(\r\x12\x13\n\x0bnext_update\x18\x15 \x01(\r\x12\x12\n\nseq_number\x18\x16 \x01(\r\x12\x16\n\x0eprecision_bits\x18\x17 \x01(\r\"N\n\tLocSource\x12\r\n\tLOC_UNSET\x10\x00\x12\x0e\n\nLOC_MANUAL\x10\x01\x12\x10\n\x0cLOC_INTERNAL\x10\x02\x12\x10\n\x0cLOC_EXTERNAL\x10\x03\"b\n\tAltSource\x12\r\n\tALT_UNSET\x10\x00\x12\x0e\n\nALT_MANUAL\x10\x01\x12\x10\n\x0c\x41LT_INTERNAL\x10\x02\x12\x10\n\x0c\x41LT_EXTERNAL\x10\x03\x12\x12\n\x0e\x41LT_BAROMETRIC\x10\x04\x42\r\n\x0b_latitude_iB\x0e\n\x0c_longitude_iB\x0b\n\t_altitudeB\x0f\n\r_altitude_haeB\x1e\n\x1c_altitude_geoidal_separationB\x0f\n\r_ground_speedB\x0f\n\r_ground_track\"\xbf\x02\n\x04User\x12\x11\n\x02id\x18\x01 \x01(\tB\x05\x92?\x02\x08\x10\x12\x18\n\tlong_name\x18\x02 \x01(\tB\x05\x92?\x02\x08(\x12\x19\n\nshort_name\x18\x03 \x01(\tB\x05\x92?\x02\x08\x05\x12\x1a\n\x07macaddr\x18\x04 \x01(\x0c\x42\t\x18\x01\x92?\x04\x08\x06x\x01\x12\x34\n\x08hw_model\x18\x05 \x01(\x0e\x32\".meshtastic.protobuf.HardwareModel\x12\x13\n\x0bis_licensed\x18\x06 \x01(\x08\x12;\n\x04role\x18\x07 \x01(\x0e\x32-.meshtastic.protobuf.Config.DeviceConfig.Role\x12\x19\n\npublic_key\x18\x08 \x01(\x0c\x42\x05\x92?\x02\x08 \x12\x1c\n\x0fis_unmessagable\x18\t \x01(\x08H\x00\x88\x01\x01\x42\x12\n\x10_is_unmessagable\"z\n\x0eRouteDiscovery\x12\x14\n\x05route\x18\x01 \x03(\x07\x42\x05\x92?\x02\x10\x08\x12\x1c\n\x0bsnr_towards\x18\x02 \x03(\x05\x42\x07\x92?\x04\x10\x08\x38\x08\x12\x19\n\nroute_back\x18\x03 \x03(\x07\x42\x05\x92?\x02\x10\x08\x12\x19\n\x08snr_back\x18\x04 \x03(\x05\x42\x07\x92?\x04\x10\x08\x38\x08\"\xb4\x04\n\x07Routing\x12<\n\rroute_request\x18\x01 \x01(\x0b\x32#.meshtastic.protobuf.RouteDiscoveryH\x00\x12:\n\x0broute_reply\x18\x02 \x01(\x0b\x32#.meshtastic.protobuf.RouteDiscoveryH\x00\x12:\n\x0c\x65rror_reason\x18\x03 \x01(\x0e\x32\".meshtastic.protobuf.Routing.ErrorH\x00\"\xe7\x02\n\x05\x45rror\x12\x08\n\x04NONE\x10\x00\x12\x0c\n\x08NO_ROUTE\x10\x01\x12\x0b\n\x07GOT_NAK\x10\x02\x12\x0b\n\x07TIMEOUT\x10\x03\x12\x10\n\x0cNO_INTERFACE\x10\x04\x12\x12\n\x0eMAX_RETRANSMIT\x10\x05\x12\x0e\n\nNO_CHANNEL\x10\x06\x12\r\n\tTOO_LARGE\x10\x07\x12\x0f\n\x0bNO_RESPONSE\x10\x08\x12\x14\n\x10\x44UTY_CYCLE_LIMIT\x10\t\x12\x0f\n\x0b\x42\x41\x44_REQUEST\x10 \x12\x12\n\x0eNOT_AUTHORIZED\x10!\x12\x0e\n\nPKI_FAILED\x10\"\x12\x16\n\x12PKI_UNKNOWN_PUBKEY\x10#\x12\x19\n\x15\x41\x44MIN_BAD_SESSION_KEY\x10$\x12!\n\x1d\x41\x44MIN_PUBLIC_KEY_UNAUTHORIZED\x10%\x12\x17\n\x13RATE_LIMIT_EXCEEDED\x10&\x12\x1c\n\x18PKI_SEND_FAIL_PUBLIC_KEY\x10\'B\t\n\x07variant\"\xe3\x01\n\x04\x44\x61ta\x12-\n\x07portnum\x18\x01 \x01(\x0e\x32\x1c.meshtastic.protobuf.PortNum\x12\x17\n\x07payload\x18\x02 \x01(\x0c\x42\x06\x92?\x03\x08\xe9\x01\x12\x15\n\rwant_response\x18\x03 \x01(\x08\x12\x0c\n\x04\x64\x65st\x18\x04 \x01(\x07\x12\x0e\n\x06source\x18\x05 \x01(\x07\x12\x12\n\nrequest_id\x18\x06 \x01(\x07\x12\x10\n\x08reply_id\x18\x07 \x01(\x07\x12\r\n\x05\x65moji\x18\x08 \x01(\x07\x12\x1c\n\x08\x62itfield\x18\t \x01(\rB\x05\x92?\x02\x38\x08H\x00\x88\x01\x01\x42\x0b\n\t_bitfield\"L\n\x0fKeyVerification\x12\r\n\x05nonce\x18\x01 \x01(\x04\x12\x14\n\x05hash1\x18\x02 \x01(\x0c\x42\x05\x92?\x02\x08 \x12\x14\n\x05hash2\x18\x03 \x01(\x0c\x42\x05\x92?\x02\x08 \"\xf1\x03\n\x14StoreForwardPlusPlus\x12V\n\x11sfpp_message_type\x18\x01 \x01(\x0e\x32;.meshtastic.protobuf.StoreForwardPlusPlus.SFPP_message_type\x12\x1b\n\x0cmessage_hash\x18\x02 \x01(\x0c\x42\x05\x92?\x02\x08 \x12\x1a\n\x0b\x63ommit_hash\x18\x03 \x01(\x0c\x42\x05\x92?\x02\x08 \x12\x18\n\troot_hash\x18\x04 \x01(\x0c\x42\x05\x92?\x02\x08 \x12\x17\n\x07message\x18\x05 \x01(\x0c\x42\x06\x92?\x03\x08\xf0\x01\x12\x17\n\x0f\x65ncapsulated_id\x18\x06 \x01(\r\x12\x17\n\x0f\x65ncapsulated_to\x18\x07 \x01(\r\x12\x19\n\x11\x65ncapsulated_from\x18\x08 \x01(\r\x12\x1b\n\x13\x65ncapsulated_rxtime\x18\t \x01(\r\x12\x13\n\x0b\x63hain_count\x18\n \x01(\r\"\x95\x01\n\x11SFPP_message_type\x12\x12\n\x0e\x43\x41NON_ANNOUNCE\x10\x00\x12\x0f\n\x0b\x43HAIN_QUERY\x10\x01\x12\x10\n\x0cLINK_REQUEST\x10\x03\x12\x10\n\x0cLINK_PROVIDE\x10\x04\x12\x1a\n\x16LINK_PROVIDE_FIRSTHALF\x10\x05\x12\x1b\n\x17LINK_PROVIDE_SECONDHALF\x10\x06\"\xf4\x02\n\x0bRemoteShell\x12\x33\n\x02op\x18\x01 \x01(\x0e\x32\'.meshtastic.protobuf.RemoteShell.OpCode\x12\x12\n\nsession_id\x18\x02 \x01(\r\x12\x0b\n\x03seq\x18\x03 \x01(\r\x12\x0f\n\x07\x61\x63k_seq\x18\x04 \x01(\r\x12\x17\n\x07payload\x18\x05 \x01(\x0c\x42\x06\x92?\x03\x08\xc8\x01\x12\x0c\n\x04\x63ols\x18\x06 \x01(\r\x12\x0c\n\x04rows\x18\x07 \x01(\r\x12\r\n\x05\x66lags\x18\x08 \x01(\r\x12\x13\n\x0blast_tx_seq\x18\t \x01(\r\x12\x13\n\x0blast_rx_seq\x18\n \x01(\r\"\x8f\x01\n\x06OpCode\x12\x0c\n\x08OP_UNSET\x10\x00\x12\x08\n\x04OPEN\x10\x01\x12\t\n\x05INPUT\x10\x02\x12\n\n\x06RESIZE\x10\x03\x12\t\n\x05\x43LOSE\x10\x04\x12\x08\n\x04PING\x10\x05\x12\x07\n\x03\x41\x43K\x10\x06\x12\x0b\n\x07OPEN_OK\x10@\x12\n\n\x06OUTPUT\x10\x41\x12\n\n\x06\x43LOSED\x10\x42\x12\t\n\x05\x45RROR\x10\x43\x12\x08\n\x04PONG\x10\x44\"\xd1\x01\n\x08Waypoint\x12\x11\n\x02id\x18\x01 \x01(\rB\x05\x92?\x02\x08\x10\x12\x17\n\nlatitude_i\x18\x02 \x01(\x0fH\x00\x88\x01\x01\x12\x18\n\x0blongitude_i\x18\x03 \x01(\x0fH\x01\x88\x01\x01\x12\x0e\n\x06\x65xpire\x18\x04 \x01(\r\x12\x11\n\tlocked_to\x18\x05 \x01(\r\x12\x13\n\x04name\x18\x06 \x01(\tB\x05\x92?\x02\x08\x1e\x12\x1a\n\x0b\x64\x65scription\x18\x07 \x01(\tB\x05\x92?\x02\x08\x64\x12\x0c\n\x04icon\x18\x08 \x01(\x07\x42\r\n\x0b_latitude_iB\x0e\n\x0c_longitude_i\"&\n\rStatusMessage\x12\x15\n\x06status\x18\x01 \x01(\tB\x05\x92?\x02\x08P\"\x83\x01\n\x16MqttClientProxyMessage\x12\x14\n\x05topic\x18\x01 \x01(\tB\x05\x92?\x02\x08<\x12\x16\n\x04\x64\x61ta\x18\x02 \x01(\x0c\x42\x06\x92?\x03\x08\xb3\x03H\x00\x12\x16\n\x04text\x18\x03 \x01(\tB\x06\x92?\x03\x08\xb3\x03H\x00\x12\x10\n\x08retained\x18\x04 \x01(\x08\x42\x11\n\x0fpayload_variant\"\x92\x08\n\nMeshPacket\x12\x0c\n\x04\x66rom\x18\x01 \x01(\x07\x12\n\n\x02to\x18\x02 \x01(\x07\x12\x16\n\x07\x63hannel\x18\x03 \x01(\rB\x05\x92?\x02\x38\x08\x12,\n\x07\x64\x65\x63oded\x18\x04 \x01(\x0b\x32\x19.meshtastic.protobuf.DataH\x00\x12\x1b\n\tencrypted\x18\x05 \x01(\x0c\x42\x06\x92?\x03\x08\x80\x02H\x00\x12\x11\n\x02id\x18\x06 \x01(\x07\x42\x05\x92?\x02\x08\x10\x12\x0f\n\x07rx_time\x18\x07 \x01(\x07\x12\x0e\n\x06rx_snr\x18\x08 \x01(\x02\x12\x18\n\thop_limit\x18\t \x01(\rB\x05\x92?\x02\x38\x08\x12\x10\n\x08want_ack\x18\n \x01(\x08\x12:\n\x08priority\x18\x0b \x01(\x0e\x32(.meshtastic.protobuf.MeshPacket.Priority\x12\x0f\n\x07rx_rssi\x18\x0c \x01(\x05\x12<\n\x07\x64\x65layed\x18\r \x01(\x0e\x32\'.meshtastic.protobuf.MeshPacket.DelayedB\x02\x18\x01\x12\x10\n\x08via_mqtt\x18\x0e \x01(\x08\x12\x18\n\thop_start\x18\x0f \x01(\rB\x05\x92?\x02\x38\x08\x12\x19\n\npublic_key\x18\x10 \x01(\x0c\x42\x05\x92?\x02\x08 \x12\x15\n\rpki_encrypted\x18\x11 \x01(\x08\x12\x17\n\x08next_hop\x18\x12 \x01(\rB\x05\x92?\x02\x38\x08\x12\x19\n\nrelay_node\x18\x13 \x01(\rB\x05\x92?\x02\x38\x08\x12\x10\n\x08tx_after\x18\x14 \x01(\r\x12O\n\x13transport_mechanism\x18\x15 \x01(\x0e\x32\x32.meshtastic.protobuf.MeshPacket.TransportMechanism\"~\n\x08Priority\x12\t\n\x05UNSET\x10\x00\x12\x07\n\x03MIN\x10\x01\x12\x0e\n\nBACKGROUND\x10\n\x12\x0b\n\x07\x44\x45\x46\x41ULT\x10@\x12\x0c\n\x08RELIABLE\x10\x46\x12\x0c\n\x08RESPONSE\x10P\x12\x08\n\x04HIGH\x10\x64\x12\t\n\x05\x41LERT\x10n\x12\x07\n\x03\x41\x43K\x10x\x12\x07\n\x03MAX\x10\x7f\"B\n\x07\x44\x65layed\x12\x0c\n\x08NO_DELAY\x10\x00\x12\x15\n\x11\x44\x45LAYED_BROADCAST\x10\x01\x12\x12\n\x0e\x44\x45LAYED_DIRECT\x10\x02\"\xcf\x01\n\x12TransportMechanism\x12\x16\n\x12TRANSPORT_INTERNAL\x10\x00\x12\x12\n\x0eTRANSPORT_LORA\x10\x01\x12\x17\n\x13TRANSPORT_LORA_ALT1\x10\x02\x12\x17\n\x13TRANSPORT_LORA_ALT2\x10\x03\x12\x17\n\x13TRANSPORT_LORA_ALT3\x10\x04\x12\x12\n\x0eTRANSPORT_MQTT\x10\x05\x12\x1b\n\x17TRANSPORT_MULTICAST_UDP\x10\x06\x12\x11\n\rTRANSPORT_API\x10\x07\x42\x11\n\x0fpayload_variant\"\x82\x03\n\x08NodeInfo\x12\x0b\n\x03num\x18\x01 \x01(\r\x12\'\n\x04user\x18\x02 \x01(\x0b\x32\x19.meshtastic.protobuf.User\x12/\n\x08position\x18\x03 \x01(\x0b\x32\x1d.meshtastic.protobuf.Position\x12\x0b\n\x03snr\x18\x04 \x01(\x02\x12\x12\n\nlast_heard\x18\x05 \x01(\x07\x12:\n\x0e\x64\x65vice_metrics\x18\x06 \x01(\x0b\x32\".meshtastic.protobuf.DeviceMetrics\x12\x16\n\x07\x63hannel\x18\x07 \x01(\rB\x05\x92?\x02\x38\x08\x12\x10\n\x08via_mqtt\x18\x08 \x01(\x08\x12\x1d\n\thops_away\x18\t \x01(\rB\x05\x92?\x02\x38\x08H\x00\x88\x01\x01\x12\x13\n\x0bis_favorite\x18\n \x01(\x08\x12\x12\n\nis_ignored\x18\x0b \x01(\x08\x12 \n\x18is_key_manually_verified\x18\x0c \x01(\x08\x12\x10\n\x08is_muted\x18\r \x01(\x08\x42\x0c\n\n_hops_away\"\xe6\x01\n\nMyNodeInfo\x12\x13\n\x0bmy_node_num\x18\x01 \x01(\r\x12\x14\n\x0creboot_count\x18\x08 \x01(\r\x12\x17\n\x0fmin_app_version\x18\x0b \x01(\r\x12\x18\n\tdevice_id\x18\x0c \x01(\x0c\x42\x05\x92?\x02\x08\x10\x12\x16\n\x07pio_env\x18\r \x01(\tB\x05\x92?\x02\x08(\x12\x45\n\x10\x66irmware_edition\x18\x0e \x01(\x0e\x32$.meshtastic.protobuf.FirmwareEditionB\x05\x92?\x02\x38\x08\x12\x1b\n\x0cnodedb_count\x18\x0f \x01(\rB\x05\x92?\x02\x38\x10\"\xd8\x01\n\tLogRecord\x12\x17\n\x07message\x18\x01 \x01(\tB\x06\x92?\x03\x08\x80\x03\x12\x0c\n\x04time\x18\x02 \x01(\x07\x12\x15\n\x06source\x18\x03 \x01(\tB\x05\x92?\x02\x08 \x12\x33\n\x05level\x18\x04 \x01(\x0e\x32$.meshtastic.protobuf.LogRecord.Level\"X\n\x05Level\x12\t\n\x05UNSET\x10\x00\x12\x0c\n\x08\x43RITICAL\x10\x32\x12\t\n\x05\x45RROR\x10(\x12\x0b\n\x07WARNING\x10\x1e\x12\x08\n\x04INFO\x10\x14\x12\t\n\x05\x44\x45\x42UG\x10\n\x12\t\n\x05TRACE\x10\x05\"e\n\x0bQueueStatus\x12\x12\n\x03res\x18\x01 \x01(\x05\x42\x05\x92?\x02\x38\x08\x12\x13\n\x04\x66ree\x18\x02 \x01(\rB\x05\x92?\x02\x38\x08\x12\x15\n\x06maxlen\x18\x03 \x01(\rB\x05\x92?\x02\x38\x08\x12\x16\n\x0emesh_packet_id\x18\x04 \x01(\r\"\xbe\x07\n\tFromRadio\x12\x11\n\x02id\x18\x01 \x01(\rB\x05\x92?\x02\x08\x10\x12\x31\n\x06packet\x18\x02 \x01(\x0b\x32\x1f.meshtastic.protobuf.MeshPacketH\x00\x12\x32\n\x07my_info\x18\x03 \x01(\x0b\x32\x1f.meshtastic.protobuf.MyNodeInfoH\x00\x12\x32\n\tnode_info\x18\x04 \x01(\x0b\x32\x1d.meshtastic.protobuf.NodeInfoH\x00\x12-\n\x06\x63onfig\x18\x05 \x01(\x0b\x32\x1b.meshtastic.protobuf.ConfigH\x00\x12\x34\n\nlog_record\x18\x06 \x01(\x0b\x32\x1e.meshtastic.protobuf.LogRecordH\x00\x12\x1c\n\x12\x63onfig_complete_id\x18\x07 \x01(\rH\x00\x12\x12\n\x08rebooted\x18\x08 \x01(\x08H\x00\x12\x39\n\x0cmoduleConfig\x18\t \x01(\x0b\x32!.meshtastic.protobuf.ModuleConfigH\x00\x12/\n\x07\x63hannel\x18\n \x01(\x0b\x32\x1c.meshtastic.protobuf.ChannelH\x00\x12\x37\n\x0bqueueStatus\x18\x0b \x01(\x0b\x32 .meshtastic.protobuf.QueueStatusH\x00\x12\x33\n\x0cxmodemPacket\x18\x0c \x01(\x0b\x32\x1b.meshtastic.protobuf.XModemH\x00\x12\x37\n\x08metadata\x18\r \x01(\x0b\x32#.meshtastic.protobuf.DeviceMetadataH\x00\x12M\n\x16mqttClientProxyMessage\x18\x0e \x01(\x0b\x32+.meshtastic.protobuf.MqttClientProxyMessageH\x00\x12\x31\n\x08\x66ileInfo\x18\x0f \x01(\x0b\x32\x1d.meshtastic.protobuf.FileInfoH\x00\x12\x45\n\x12\x63lientNotification\x18\x10 \x01(\x0b\x32\'.meshtastic.protobuf.ClientNotificationH\x00\x12=\n\x0e\x64\x65viceuiConfig\x18\x11 \x01(\x0b\x32#.meshtastic.protobuf.DeviceUIConfigH\x00\x12>\n\x0flockdown_status\x18\x12 \x01(\x0b\x32#.meshtastic.protobuf.LockdownStatusH\x00\x42\x11\n\x0fpayload_variant\"\x95\x02\n\x0eLockdownStatus\x12\x38\n\x05state\x18\x01 \x01(\x0e\x32).meshtastic.protobuf.LockdownStatus.State\x12\x1a\n\x0block_reason\x18\x02 \x01(\tB\x05\x92?\x02\x08 \x12\x17\n\x0f\x62oots_remaining\x18\x03 \x01(\r\x12\x19\n\x11valid_until_epoch\x18\x04 \x01(\r\x12\x17\n\x0f\x62\x61\x63koff_seconds\x18\x05 \x01(\r\"`\n\x05State\x12\x15\n\x11STATE_UNSPECIFIED\x10\x00\x12\x13\n\x0fNEEDS_PROVISION\x10\x01\x12\n\n\x06LOCKED\x10\x02\x12\x0c\n\x08UNLOCKED\x10\x03\x12\x11\n\rUNLOCK_FAILED\x10\x04\"\xb8\x04\n\x12\x43lientNotification\x12\x15\n\x08reply_id\x18\x01 \x01(\rH\x01\x88\x01\x01\x12\x0c\n\x04time\x18\x02 \x01(\x07\x12\x33\n\x05level\x18\x03 \x01(\x0e\x32$.meshtastic.protobuf.LogRecord.Level\x12\x17\n\x07message\x18\x04 \x01(\tB\x06\x92?\x03\x08\x90\x03\x12Z\n\x1ekey_verification_number_inform\x18\x0b \x01(\x0b\x32\x30.meshtastic.protobuf.KeyVerificationNumberInformH\x00\x12\\\n\x1fkey_verification_number_request\x18\x0c \x01(\x0b\x32\x31.meshtastic.protobuf.KeyVerificationNumberRequestH\x00\x12K\n\x16key_verification_final\x18\r \x01(\x0b\x32).meshtastic.protobuf.KeyVerificationFinalH\x00\x12I\n\x15\x64uplicated_public_key\x18\x0e \x01(\x0b\x32(.meshtastic.protobuf.DuplicatedPublicKeyH\x00\x12=\n\x0flow_entropy_key\x18\x0f \x01(\x0b\x32\".meshtastic.protobuf.LowEntropyKeyH\x00\x42\x11\n\x0fpayload_variantB\x0b\n\t_reply_id\"e\n\x1bKeyVerificationNumberInform\x12\r\n\x05nonce\x18\x01 \x01(\x04\x12\x1e\n\x0fremote_longname\x18\x02 \x01(\tB\x05\x92?\x02\x08(\x12\x17\n\x0fsecurity_number\x18\x03 \x01(\r\"M\n\x1cKeyVerificationNumberRequest\x12\r\n\x05nonce\x18\x01 \x01(\x04\x12\x1e\n\x0fremote_longname\x18\x02 \x01(\tB\x05\x92?\x02\x08(\"\x7f\n\x14KeyVerificationFinal\x12\r\n\x05nonce\x18\x01 \x01(\x04\x12\x1e\n\x0fremote_longname\x18\x02 \x01(\tB\x05\x92?\x02\x08(\x12\x10\n\x08isSender\x18\x03 \x01(\x08\x12&\n\x17verification_characters\x18\x04 \x01(\tB\x05\x92?\x02\x08\n\"\x15\n\x13\x44uplicatedPublicKey\"\x0f\n\rLowEntropyKey\"9\n\x08\x46ileInfo\x12\x19\n\tfile_name\x18\x01 \x01(\tB\x06\x92?\x03\x08\xe4\x01\x12\x12\n\nsize_bytes\x18\x02 \x01(\r\"\xb8\x02\n\x07ToRadio\x12\x31\n\x06packet\x18\x01 \x01(\x0b\x32\x1f.meshtastic.protobuf.MeshPacketH\x00\x12\x18\n\x0ewant_config_id\x18\x03 \x01(\rH\x00\x12\x14\n\ndisconnect\x18\x04 \x01(\x08H\x00\x12\x33\n\x0cxmodemPacket\x18\x05 \x01(\x0b\x32\x1b.meshtastic.protobuf.XModemH\x00\x12M\n\x16mqttClientProxyMessage\x18\x06 \x01(\x0b\x32+.meshtastic.protobuf.MqttClientProxyMessageH\x00\x12\x33\n\theartbeat\x18\x07 \x01(\x0b\x32\x1e.meshtastic.protobuf.HeartbeatH\x00\x42\x11\n\x0fpayload_variant\"Q\n\nCompressed\x12-\n\x07portnum\x18\x01 \x01(\x0e\x32\x1c.meshtastic.protobuf.PortNum\x12\x14\n\x04\x64\x61ta\x18\x02 \x01(\x0c\x42\x06\x92?\x03\x08\xe9\x01\"\x97\x01\n\x0cNeighborInfo\x12\x0f\n\x07node_id\x18\x01 \x01(\r\x12\x17\n\x0flast_sent_by_id\x18\x02 \x01(\r\x12$\n\x1cnode_broadcast_interval_secs\x18\x03 \x01(\r\x12\x37\n\tneighbors\x18\x04 \x03(\x0b\x32\x1d.meshtastic.protobuf.NeighborB\x05\x92?\x02\x10\n\"d\n\x08Neighbor\x12\x0f\n\x07node_id\x18\x01 \x01(\r\x12\x0b\n\x03snr\x18\x02 \x01(\x02\x12\x14\n\x0clast_rx_time\x18\x03 \x01(\x07\x12$\n\x1cnode_broadcast_interval_secs\x18\x04 \x01(\r\"\xf0\x02\n\x0e\x44\x65viceMetadata\x12\x1f\n\x10\x66irmware_version\x18\x01 \x01(\tB\x05\x92?\x02\x08\x12\x12\x1c\n\x14\x64\x65vice_state_version\x18\x02 \x01(\r\x12\x13\n\x0b\x63\x61nShutdown\x18\x03 \x01(\x08\x12\x0f\n\x07hasWifi\x18\x04 \x01(\x08\x12\x14\n\x0chasBluetooth\x18\x05 \x01(\x08\x12\x13\n\x0bhasEthernet\x18\x06 \x01(\x08\x12;\n\x04role\x18\x07 \x01(\x0e\x32-.meshtastic.protobuf.Config.DeviceConfig.Role\x12\x16\n\x0eposition_flags\x18\x08 \x01(\r\x12\x34\n\x08hw_model\x18\t \x01(\x0e\x32\".meshtastic.protobuf.HardwareModel\x12\x19\n\x11hasRemoteHardware\x18\n \x01(\x08\x12\x0e\n\x06hasPKC\x18\x0b \x01(\x08\x12\x18\n\x10\x65xcluded_modules\x18\x0c \x01(\r\"\x1a\n\tHeartbeat\x12\r\n\x05nonce\x18\x01 \x01(\r\"^\n\x15NodeRemoteHardwarePin\x12\x10\n\x08node_num\x18\x01 \x01(\r\x12\x33\n\x03pin\x18\x02 \x01(\x0b\x32&.meshtastic.protobuf.RemoteHardwarePin\"{\n\x0e\x43hunkedPayload\x12\x12\n\npayload_id\x18\x01 \x01(\r\x12\x1a\n\x0b\x63hunk_count\x18\x02 \x01(\rB\x05\x92?\x02\x38\x10\x12\x1a\n\x0b\x63hunk_index\x18\x03 \x01(\rB\x05\x92?\x02\x38\x10\x12\x1d\n\rpayload_chunk\x18\x04 \x01(\x0c\x42\x06\x92?\x03\x08\xe4\x01\"\x1f\n\rresend_chunks\x12\x0e\n\x06\x63hunks\x18\x01 \x03(\r\"\xb3\x01\n\x16\x43hunkedPayloadResponse\x12\x12\n\npayload_id\x18\x01 \x01(\r\x12\x1a\n\x10request_transfer\x18\x02 \x01(\x08H\x00\x12\x19\n\x0f\x61\x63\x63\x65pt_transfer\x18\x03 \x01(\x08H\x00\x12;\n\rresend_chunks\x18\x04 \x01(\x0b\x32\".meshtastic.protobuf.resend_chunksH\x00\x42\x11\n\x0fpayload_variant*\xce\x14\n\rHardwareModel\x12\t\n\x05UNSET\x10\x00\x12\x0c\n\x08TLORA_V2\x10\x01\x12\x0c\n\x08TLORA_V1\x10\x02\x12\x12\n\x0eTLORA_V2_1_1P6\x10\x03\x12\t\n\x05TBEAM\x10\x04\x12\x0f\n\x0bHELTEC_V2_0\x10\x05\x12\x0e\n\nTBEAM_V0P7\x10\x06\x12\n\n\x06T_ECHO\x10\x07\x12\x10\n\x0cTLORA_V1_1P3\x10\x08\x12\x0b\n\x07RAK4631\x10\t\x12\x0f\n\x0bHELTEC_V2_1\x10\n\x12\r\n\tHELTEC_V1\x10\x0b\x12\x18\n\x14LILYGO_TBEAM_S3_CORE\x10\x0c\x12\x0c\n\x08RAK11200\x10\r\x12\x0b\n\x07NANO_G1\x10\x0e\x12\x12\n\x0eTLORA_V2_1_1P8\x10\x0f\x12\x0f\n\x0bTLORA_T3_S3\x10\x10\x12\x14\n\x10NANO_G1_EXPLORER\x10\x11\x12\x11\n\rNANO_G2_ULTRA\x10\x12\x12\r\n\tLORA_TYPE\x10\x13\x12\x0b\n\x07WIPHONE\x10\x14\x12\x0e\n\nWIO_WM1110\x10\x15\x12\x0b\n\x07RAK2560\x10\x16\x12\x13\n\x0fHELTEC_HRU_3601\x10\x17\x12\x1a\n\x16HELTEC_WIRELESS_BRIDGE\x10\x18\x12\x0e\n\nSTATION_G1\x10\x19\x12\x0c\n\x08RAK11310\x10\x1a\x12\x14\n\x10SENSELORA_RP2040\x10\x1b\x12\x10\n\x0cSENSELORA_S3\x10\x1c\x12\r\n\tCANARYONE\x10\x1d\x12\x0f\n\x0bRP2040_LORA\x10\x1e\x12\x0e\n\nSTATION_G2\x10\x1f\x12\x11\n\rLORA_RELAY_V1\x10 \x12\x0f\n\x0bT_ECHO_PLUS\x10!\x12\x07\n\x03PPR\x10\"\x12\x0f\n\x0bGENIEBLOCKS\x10#\x12\x11\n\rNRF52_UNKNOWN\x10$\x12\r\n\tPORTDUINO\x10%\x12\x0f\n\x0b\x41NDROID_SIM\x10&\x12\n\n\x06\x44IY_V1\x10\'\x12\x15\n\x11NRF52840_PCA10059\x10(\x12\n\n\x06\x44R_DEV\x10)\x12\x0b\n\x07M5STACK\x10*\x12\r\n\tHELTEC_V3\x10+\x12\x11\n\rHELTEC_WSL_V3\x10,\x12\x13\n\x0f\x42\x45TAFPV_2400_TX\x10-\x12\x17\n\x13\x42\x45TAFPV_900_NANO_TX\x10.\x12\x0c\n\x08RPI_PICO\x10/\x12\x1b\n\x17HELTEC_WIRELESS_TRACKER\x10\x30\x12\x19\n\x15HELTEC_WIRELESS_PAPER\x10\x31\x12\n\n\x06T_DECK\x10\x32\x12\x0e\n\nT_WATCH_S3\x10\x33\x12\x11\n\rPICOMPUTER_S3\x10\x34\x12\x0f\n\x0bHELTEC_HT62\x10\x35\x12\x12\n\x0e\x45\x42YTE_ESP32_S3\x10\x36\x12\x11\n\rESP32_S3_PICO\x10\x37\x12\r\n\tCHATTER_2\x10\x38\x12\x1e\n\x1aHELTEC_WIRELESS_PAPER_V1_0\x10\x39\x12 \n\x1cHELTEC_WIRELESS_TRACKER_V1_0\x10:\x12\x0b\n\x07UNPHONE\x10;\x12\x0c\n\x08TD_LORAC\x10<\x12\x13\n\x0f\x43\x44\x45\x42YTE_EORA_S3\x10=\x12\x0f\n\x0bTWC_MESH_V4\x10>\x12\x16\n\x12NRF52_PROMICRO_DIY\x10?\x12\x1f\n\x1bRADIOMASTER_900_BANDIT_NANO\x10@\x12\x1c\n\x18HELTEC_CAPSULE_SENSOR_V3\x10\x41\x12\x1d\n\x19HELTEC_VISION_MASTER_T190\x10\x42\x12\x1d\n\x19HELTEC_VISION_MASTER_E213\x10\x43\x12\x1d\n\x19HELTEC_VISION_MASTER_E290\x10\x44\x12\x19\n\x15HELTEC_MESH_NODE_T114\x10\x45\x12\x16\n\x12SENSECAP_INDICATOR\x10\x46\x12\x13\n\x0fTRACKER_T1000_E\x10G\x12\x0b\n\x07RAK3172\x10H\x12\n\n\x06WIO_E5\x10I\x12\x1a\n\x16RADIOMASTER_900_BANDIT\x10J\x12\x13\n\x0fME25LS01_4Y10TD\x10K\x12\x18\n\x14RP2040_FEATHER_RFM95\x10L\x12\x15\n\x11M5STACK_COREBASIC\x10M\x12\x11\n\rM5STACK_CORE2\x10N\x12\r\n\tRPI_PICO2\x10O\x12\x12\n\x0eM5STACK_CORES3\x10P\x12\x11\n\rSEEED_XIAO_S3\x10Q\x12\x0b\n\x07MS24SF1\x10R\x12\x0c\n\x08TLORA_C6\x10S\x12\x0f\n\x0bWISMESH_TAP\x10T\x12\r\n\tROUTASTIC\x10U\x12\x0c\n\x08MESH_TAB\x10V\x12\x0c\n\x08MESHLINK\x10W\x12\x12\n\x0eXIAO_NRF52_KIT\x10X\x12\x10\n\x0cTHINKNODE_M1\x10Y\x12\x10\n\x0cTHINKNODE_M2\x10Z\x12\x0f\n\x0bT_ETH_ELITE\x10[\x12\x15\n\x11HELTEC_SENSOR_HUB\x10\\\x12\r\n\tMUZI_BASE\x10]\x12\x16\n\x12HELTEC_MESH_POCKET\x10^\x12\x14\n\x10SEEED_SOLAR_NODE\x10_\x12\x18\n\x14NOMADSTAR_METEOR_PRO\x10`\x12\r\n\tCROWPANEL\x10\x61\x12\x0b\n\x07LINK_32\x10\x62\x12\x18\n\x14SEEED_WIO_TRACKER_L1\x10\x63\x12\x1d\n\x19SEEED_WIO_TRACKER_L1_EINK\x10\x64\x12\x0f\n\x0bMUZI_R1_NEO\x10\x65\x12\x0e\n\nT_DECK_PRO\x10\x66\x12\x10\n\x0cT_LORA_PAGER\x10g\x12\x14\n\x10M5STACK_RESERVED\x10h\x12\x0f\n\x0bWISMESH_TAG\x10i\x12\x0b\n\x07RAK3312\x10j\x12\x10\n\x0cTHINKNODE_M5\x10k\x12\x15\n\x11HELTEC_MESH_SOLAR\x10l\x12\x0f\n\x0bT_ECHO_LITE\x10m\x12\r\n\tHELTEC_V4\x10n\x12\x0f\n\x0bM5STACK_C6L\x10o\x12\x19\n\x15M5STACK_CARDPUTER_ADV\x10p\x12\x1e\n\x1aHELTEC_WIRELESS_TRACKER_V2\x10q\x12\x11\n\rT_WATCH_ULTRA\x10r\x12\x10\n\x0cTHINKNODE_M3\x10s\x12\x12\n\x0eWISMESH_TAP_V2\x10t\x12\x0b\n\x07RAK3401\x10u\x12\x0b\n\x07RAK6421\x10v\x12\x10\n\x0cTHINKNODE_M4\x10w\x12\x10\n\x0cTHINKNODE_M6\x10x\x12\x12\n\x0eMESHSTICK_1262\x10y\x12\x10\n\x0cTBEAM_1_WATT\x10z\x12\x14\n\x10T5_S3_EPAPER_PRO\x10{\x12\r\n\tTBEAM_BPF\x10|\x12\x12\n\x0eMINI_EPAPER_S3\x10}\x12\x13\n\x0fTDISPLAY_S3_PRO\x10~\x12\x19\n\x15HELTEC_MESH_NODE_T096\x10\x7f\x12\x18\n\x13TRACKER_T1000_E_PRO\x10\x80\x01\x12\x11\n\x0cTHINKNODE_M7\x10\x81\x01\x12\x11\n\x0cTHINKNODE_M8\x10\x82\x01\x12\x11\n\x0cTHINKNODE_M9\x10\x83\x01\x12\x11\n\x0cHELTEC_V4_R8\x10\x84\x01\x12\x18\n\x13HELTEC_MESH_NODE_T1\x10\x85\x01\x12\x0f\n\nSTATION_G3\x10\x86\x01\x12\x13\n\x0eT_IMPULSE_PLUS\x10\x87\x01\x12\x10\n\x0bT_ECHO_CARD\x10\x88\x01\x12\x0f\n\nPRIVATE_HW\x10\xff\x01*,\n\tConstants\x12\x08\n\x04ZERO\x10\x00\x12\x15\n\x10\x44\x41TA_PAYLOAD_LEN\x10\xe9\x01*\xb4\x02\n\x11\x43riticalErrorCode\x12\x08\n\x04NONE\x10\x00\x12\x0f\n\x0bTX_WATCHDOG\x10\x01\x12\x14\n\x10SLEEP_ENTER_WAIT\x10\x02\x12\x0c\n\x08NO_RADIO\x10\x03\x12\x0f\n\x0bUNSPECIFIED\x10\x04\x12\x15\n\x11UBLOX_UNIT_FAILED\x10\x05\x12\r\n\tNO_AXP192\x10\x06\x12\x19\n\x15INVALID_RADIO_SETTING\x10\x07\x12\x13\n\x0fTRANSMIT_FAILED\x10\x08\x12\x0c\n\x08\x42ROWNOUT\x10\t\x12\x12\n\x0eSX1262_FAILURE\x10\n\x12\x11\n\rRADIO_SPI_BUG\x10\x0b\x12 \n\x1c\x46LASH_CORRUPTION_RECOVERABLE\x10\x0c\x12\"\n\x1e\x46LASH_CORRUPTION_UNRECOVERABLE\x10\r*\x7f\n\x0f\x46irmwareEdition\x12\x0b\n\x07VANILLA\x10\x00\x12\x11\n\rSMART_CITIZEN\x10\x01\x12\x0e\n\nOPEN_SAUCE\x10\x10\x12\n\n\x06\x44\x45\x46\x43ON\x10\x11\x12\x0f\n\x0b\x42URNING_MAN\x10\x12\x12\x0e\n\nHAMVENTION\x10\x13\x12\x0f\n\x0b\x44IY_EDITION\x10\x7f*\x80\x03\n\x0f\x45xcludedModules\x12\x11\n\rEXCLUDED_NONE\x10\x00\x12\x0f\n\x0bMQTT_CONFIG\x10\x01\x12\x11\n\rSERIAL_CONFIG\x10\x02\x12\x13\n\x0f\x45XTNOTIF_CONFIG\x10\x04\x12\x17\n\x13STOREFORWARD_CONFIG\x10\x08\x12\x14\n\x10RANGETEST_CONFIG\x10\x10\x12\x14\n\x10TELEMETRY_CONFIG\x10 \x12\x14\n\x10\x43\x41NNEDMSG_CONFIG\x10@\x12\x11\n\x0c\x41UDIO_CONFIG\x10\x80\x01\x12\x1a\n\x15REMOTEHARDWARE_CONFIG\x10\x80\x02\x12\x18\n\x13NEIGHBORINFO_CONFIG\x10\x80\x04\x12\x1b\n\x16\x41MBIENTLIGHTING_CONFIG\x10\x80\x08\x12\x1b\n\x16\x44\x45TECTIONSENSOR_CONFIG\x10\x80\x10\x12\x16\n\x11PAXCOUNTER_CONFIG\x10\x80 \x12\x15\n\x10\x42LUETOOTH_CONFIG\x10\x80@\x12\x14\n\x0eNETWORK_CONFIG\x10\x80\x80\x01\x42`\n\x14org.meshtastic.protoB\nMeshProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1emeshtastic/protobuf/mesh.proto\x12\x13meshtastic.protobuf\x1a!meshtastic/protobuf/channel.proto\x1a meshtastic/protobuf/config.proto\x1a#meshtastic/protobuf/device_ui.proto\x1a\'meshtastic/protobuf/module_config.proto\x1a\"meshtastic/protobuf/portnums.proto\x1a#meshtastic/protobuf/telemetry.proto\x1a meshtastic/protobuf/xmodem.proto\x1a meshtastic/protobuf/nanopb.proto\"\x99\x07\n\x08Position\x12\x17\n\nlatitude_i\x18\x01 \x01(\x0fH\x00\x88\x01\x01\x12\x18\n\x0blongitude_i\x18\x02 \x01(\x0fH\x01\x88\x01\x01\x12\x15\n\x08\x61ltitude\x18\x03 \x01(\x05H\x02\x88\x01\x01\x12\x0c\n\x04time\x18\x04 \x01(\x07\x12@\n\x0flocation_source\x18\x05 \x01(\x0e\x32\'.meshtastic.protobuf.Position.LocSource\x12@\n\x0f\x61ltitude_source\x18\x06 \x01(\x0e\x32\'.meshtastic.protobuf.Position.AltSource\x12\x11\n\ttimestamp\x18\x07 \x01(\x07\x12\x1f\n\x17timestamp_millis_adjust\x18\x08 \x01(\x05\x12\x19\n\x0c\x61ltitude_hae\x18\t \x01(\x11H\x03\x88\x01\x01\x12(\n\x1b\x61ltitude_geoidal_separation\x18\n \x01(\x11H\x04\x88\x01\x01\x12\x0c\n\x04PDOP\x18\x0b \x01(\r\x12\x0c\n\x04HDOP\x18\x0c \x01(\r\x12\x0c\n\x04VDOP\x18\r \x01(\r\x12\x14\n\x0cgps_accuracy\x18\x0e \x01(\r\x12\x19\n\x0cground_speed\x18\x0f \x01(\rH\x05\x88\x01\x01\x12\x19\n\x0cground_track\x18\x10 \x01(\rH\x06\x88\x01\x01\x12\x13\n\x0b\x66ix_quality\x18\x11 \x01(\r\x12\x10\n\x08\x66ix_type\x18\x12 \x01(\r\x12\x14\n\x0csats_in_view\x18\x13 \x01(\r\x12\x11\n\tsensor_id\x18\x14 \x01(\r\x12\x13\n\x0bnext_update\x18\x15 \x01(\r\x12\x12\n\nseq_number\x18\x16 \x01(\r\x12\x16\n\x0eprecision_bits\x18\x17 \x01(\r\"N\n\tLocSource\x12\r\n\tLOC_UNSET\x10\x00\x12\x0e\n\nLOC_MANUAL\x10\x01\x12\x10\n\x0cLOC_INTERNAL\x10\x02\x12\x10\n\x0cLOC_EXTERNAL\x10\x03\"b\n\tAltSource\x12\r\n\tALT_UNSET\x10\x00\x12\x0e\n\nALT_MANUAL\x10\x01\x12\x10\n\x0c\x41LT_INTERNAL\x10\x02\x12\x10\n\x0c\x41LT_EXTERNAL\x10\x03\x12\x12\n\x0e\x41LT_BAROMETRIC\x10\x04\x42\r\n\x0b_latitude_iB\x0e\n\x0c_longitude_iB\x0b\n\t_altitudeB\x0f\n\r_altitude_haeB\x1e\n\x1c_altitude_geoidal_separationB\x0f\n\r_ground_speedB\x0f\n\r_ground_track\"\xbf\x02\n\x04User\x12\x11\n\x02id\x18\x01 \x01(\tB\x05\x92?\x02\x08\x10\x12\x18\n\tlong_name\x18\x02 \x01(\tB\x05\x92?\x02\x08(\x12\x19\n\nshort_name\x18\x03 \x01(\tB\x05\x92?\x02\x08\x05\x12\x1a\n\x07macaddr\x18\x04 \x01(\x0c\x42\t\x18\x01\x92?\x04\x08\x06x\x01\x12\x34\n\x08hw_model\x18\x05 \x01(\x0e\x32\".meshtastic.protobuf.HardwareModel\x12\x13\n\x0bis_licensed\x18\x06 \x01(\x08\x12;\n\x04role\x18\x07 \x01(\x0e\x32-.meshtastic.protobuf.Config.DeviceConfig.Role\x12\x19\n\npublic_key\x18\x08 \x01(\x0c\x42\x05\x92?\x02\x08 \x12\x1c\n\x0fis_unmessagable\x18\t \x01(\x08H\x00\x88\x01\x01\x42\x12\n\x10_is_unmessagable\"z\n\x0eRouteDiscovery\x12\x14\n\x05route\x18\x01 \x03(\x07\x42\x05\x92?\x02\x10\x08\x12\x1c\n\x0bsnr_towards\x18\x02 \x03(\x05\x42\x07\x92?\x04\x10\x08\x38\x08\x12\x19\n\nroute_back\x18\x03 \x03(\x07\x42\x05\x92?\x02\x10\x08\x12\x19\n\x08snr_back\x18\x04 \x03(\x05\x42\x07\x92?\x04\x10\x08\x38\x08\"\xb4\x04\n\x07Routing\x12<\n\rroute_request\x18\x01 \x01(\x0b\x32#.meshtastic.protobuf.RouteDiscoveryH\x00\x12:\n\x0broute_reply\x18\x02 \x01(\x0b\x32#.meshtastic.protobuf.RouteDiscoveryH\x00\x12:\n\x0c\x65rror_reason\x18\x03 \x01(\x0e\x32\".meshtastic.protobuf.Routing.ErrorH\x00\"\xe7\x02\n\x05\x45rror\x12\x08\n\x04NONE\x10\x00\x12\x0c\n\x08NO_ROUTE\x10\x01\x12\x0b\n\x07GOT_NAK\x10\x02\x12\x0b\n\x07TIMEOUT\x10\x03\x12\x10\n\x0cNO_INTERFACE\x10\x04\x12\x12\n\x0eMAX_RETRANSMIT\x10\x05\x12\x0e\n\nNO_CHANNEL\x10\x06\x12\r\n\tTOO_LARGE\x10\x07\x12\x0f\n\x0bNO_RESPONSE\x10\x08\x12\x14\n\x10\x44UTY_CYCLE_LIMIT\x10\t\x12\x0f\n\x0b\x42\x41\x44_REQUEST\x10 \x12\x12\n\x0eNOT_AUTHORIZED\x10!\x12\x0e\n\nPKI_FAILED\x10\"\x12\x16\n\x12PKI_UNKNOWN_PUBKEY\x10#\x12\x19\n\x15\x41\x44MIN_BAD_SESSION_KEY\x10$\x12!\n\x1d\x41\x44MIN_PUBLIC_KEY_UNAUTHORIZED\x10%\x12\x17\n\x13RATE_LIMIT_EXCEEDED\x10&\x12\x1c\n\x18PKI_SEND_FAIL_PUBLIC_KEY\x10\'B\t\n\x07variant\"\xe3\x01\n\x04\x44\x61ta\x12-\n\x07portnum\x18\x01 \x01(\x0e\x32\x1c.meshtastic.protobuf.PortNum\x12\x17\n\x07payload\x18\x02 \x01(\x0c\x42\x06\x92?\x03\x08\xe9\x01\x12\x15\n\rwant_response\x18\x03 \x01(\x08\x12\x0c\n\x04\x64\x65st\x18\x04 \x01(\x07\x12\x0e\n\x06source\x18\x05 \x01(\x07\x12\x12\n\nrequest_id\x18\x06 \x01(\x07\x12\x10\n\x08reply_id\x18\x07 \x01(\x07\x12\r\n\x05\x65moji\x18\x08 \x01(\x07\x12\x1c\n\x08\x62itfield\x18\t \x01(\rB\x05\x92?\x02\x38\x08H\x00\x88\x01\x01\x42\x0b\n\t_bitfield\"L\n\x0fKeyVerification\x12\r\n\x05nonce\x18\x01 \x01(\x04\x12\x14\n\x05hash1\x18\x02 \x01(\x0c\x42\x05\x92?\x02\x08 \x12\x14\n\x05hash2\x18\x03 \x01(\x0c\x42\x05\x92?\x02\x08 \"\xf1\x03\n\x14StoreForwardPlusPlus\x12V\n\x11sfpp_message_type\x18\x01 \x01(\x0e\x32;.meshtastic.protobuf.StoreForwardPlusPlus.SFPP_message_type\x12\x1b\n\x0cmessage_hash\x18\x02 \x01(\x0c\x42\x05\x92?\x02\x08 \x12\x1a\n\x0b\x63ommit_hash\x18\x03 \x01(\x0c\x42\x05\x92?\x02\x08 \x12\x18\n\troot_hash\x18\x04 \x01(\x0c\x42\x05\x92?\x02\x08 \x12\x17\n\x07message\x18\x05 \x01(\x0c\x42\x06\x92?\x03\x08\xf0\x01\x12\x17\n\x0f\x65ncapsulated_id\x18\x06 \x01(\r\x12\x17\n\x0f\x65ncapsulated_to\x18\x07 \x01(\r\x12\x19\n\x11\x65ncapsulated_from\x18\x08 \x01(\r\x12\x1b\n\x13\x65ncapsulated_rxtime\x18\t \x01(\r\x12\x13\n\x0b\x63hain_count\x18\n \x01(\r\"\x95\x01\n\x11SFPP_message_type\x12\x12\n\x0e\x43\x41NON_ANNOUNCE\x10\x00\x12\x0f\n\x0b\x43HAIN_QUERY\x10\x01\x12\x10\n\x0cLINK_REQUEST\x10\x03\x12\x10\n\x0cLINK_PROVIDE\x10\x04\x12\x1a\n\x16LINK_PROVIDE_FIRSTHALF\x10\x05\x12\x1b\n\x17LINK_PROVIDE_SECONDHALF\x10\x06\"\xf4\x02\n\x0bRemoteShell\x12\x33\n\x02op\x18\x01 \x01(\x0e\x32\'.meshtastic.protobuf.RemoteShell.OpCode\x12\x12\n\nsession_id\x18\x02 \x01(\r\x12\x0b\n\x03seq\x18\x03 \x01(\r\x12\x0f\n\x07\x61\x63k_seq\x18\x04 \x01(\r\x12\x17\n\x07payload\x18\x05 \x01(\x0c\x42\x06\x92?\x03\x08\xc8\x01\x12\x0c\n\x04\x63ols\x18\x06 \x01(\r\x12\x0c\n\x04rows\x18\x07 \x01(\r\x12\r\n\x05\x66lags\x18\x08 \x01(\r\x12\x13\n\x0blast_tx_seq\x18\t \x01(\r\x12\x13\n\x0blast_rx_seq\x18\n \x01(\r\"\x8f\x01\n\x06OpCode\x12\x0c\n\x08OP_UNSET\x10\x00\x12\x08\n\x04OPEN\x10\x01\x12\t\n\x05INPUT\x10\x02\x12\n\n\x06RESIZE\x10\x03\x12\t\n\x05\x43LOSE\x10\x04\x12\x08\n\x04PING\x10\x05\x12\x07\n\x03\x41\x43K\x10\x06\x12\x0b\n\x07OPEN_OK\x10@\x12\n\n\x06OUTPUT\x10\x41\x12\n\n\x06\x43LOSED\x10\x42\x12\t\n\x05\x45RROR\x10\x43\x12\x08\n\x04PONG\x10\x44\"\xd1\x01\n\x08Waypoint\x12\x11\n\x02id\x18\x01 \x01(\rB\x05\x92?\x02\x08\x10\x12\x17\n\nlatitude_i\x18\x02 \x01(\x0fH\x00\x88\x01\x01\x12\x18\n\x0blongitude_i\x18\x03 \x01(\x0fH\x01\x88\x01\x01\x12\x0e\n\x06\x65xpire\x18\x04 \x01(\r\x12\x11\n\tlocked_to\x18\x05 \x01(\r\x12\x13\n\x04name\x18\x06 \x01(\tB\x05\x92?\x02\x08\x1e\x12\x1a\n\x0b\x64\x65scription\x18\x07 \x01(\tB\x05\x92?\x02\x08\x64\x12\x0c\n\x04icon\x18\x08 \x01(\x07\x42\r\n\x0b_latitude_iB\x0e\n\x0c_longitude_i\"&\n\rStatusMessage\x12\x15\n\x06status\x18\x01 \x01(\tB\x05\x92?\x02\x08P\"\x83\x01\n\x16MqttClientProxyMessage\x12\x14\n\x05topic\x18\x01 \x01(\tB\x05\x92?\x02\x08<\x12\x16\n\x04\x64\x61ta\x18\x02 \x01(\x0c\x42\x06\x92?\x03\x08\xb3\x03H\x00\x12\x16\n\x04text\x18\x03 \x01(\tB\x06\x92?\x03\x08\xb3\x03H\x00\x12\x10\n\x08retained\x18\x04 \x01(\x08\x42\x11\n\x0fpayload_variant\"\x92\x08\n\nMeshPacket\x12\x0c\n\x04\x66rom\x18\x01 \x01(\x07\x12\n\n\x02to\x18\x02 \x01(\x07\x12\x16\n\x07\x63hannel\x18\x03 \x01(\rB\x05\x92?\x02\x38\x08\x12,\n\x07\x64\x65\x63oded\x18\x04 \x01(\x0b\x32\x19.meshtastic.protobuf.DataH\x00\x12\x1b\n\tencrypted\x18\x05 \x01(\x0c\x42\x06\x92?\x03\x08\x80\x02H\x00\x12\x11\n\x02id\x18\x06 \x01(\x07\x42\x05\x92?\x02\x08\x10\x12\x0f\n\x07rx_time\x18\x07 \x01(\x07\x12\x0e\n\x06rx_snr\x18\x08 \x01(\x02\x12\x18\n\thop_limit\x18\t \x01(\rB\x05\x92?\x02\x38\x08\x12\x10\n\x08want_ack\x18\n \x01(\x08\x12:\n\x08priority\x18\x0b \x01(\x0e\x32(.meshtastic.protobuf.MeshPacket.Priority\x12\x0f\n\x07rx_rssi\x18\x0c \x01(\x05\x12<\n\x07\x64\x65layed\x18\r \x01(\x0e\x32\'.meshtastic.protobuf.MeshPacket.DelayedB\x02\x18\x01\x12\x10\n\x08via_mqtt\x18\x0e \x01(\x08\x12\x18\n\thop_start\x18\x0f \x01(\rB\x05\x92?\x02\x38\x08\x12\x19\n\npublic_key\x18\x10 \x01(\x0c\x42\x05\x92?\x02\x08 \x12\x15\n\rpki_encrypted\x18\x11 \x01(\x08\x12\x17\n\x08next_hop\x18\x12 \x01(\rB\x05\x92?\x02\x38\x08\x12\x19\n\nrelay_node\x18\x13 \x01(\rB\x05\x92?\x02\x38\x08\x12\x10\n\x08tx_after\x18\x14 \x01(\r\x12O\n\x13transport_mechanism\x18\x15 \x01(\x0e\x32\x32.meshtastic.protobuf.MeshPacket.TransportMechanism\"~\n\x08Priority\x12\t\n\x05UNSET\x10\x00\x12\x07\n\x03MIN\x10\x01\x12\x0e\n\nBACKGROUND\x10\n\x12\x0b\n\x07\x44\x45\x46\x41ULT\x10@\x12\x0c\n\x08RELIABLE\x10\x46\x12\x0c\n\x08RESPONSE\x10P\x12\x08\n\x04HIGH\x10\x64\x12\t\n\x05\x41LERT\x10n\x12\x07\n\x03\x41\x43K\x10x\x12\x07\n\x03MAX\x10\x7f\"B\n\x07\x44\x65layed\x12\x0c\n\x08NO_DELAY\x10\x00\x12\x15\n\x11\x44\x45LAYED_BROADCAST\x10\x01\x12\x12\n\x0e\x44\x45LAYED_DIRECT\x10\x02\"\xcf\x01\n\x12TransportMechanism\x12\x16\n\x12TRANSPORT_INTERNAL\x10\x00\x12\x12\n\x0eTRANSPORT_LORA\x10\x01\x12\x17\n\x13TRANSPORT_LORA_ALT1\x10\x02\x12\x17\n\x13TRANSPORT_LORA_ALT2\x10\x03\x12\x17\n\x13TRANSPORT_LORA_ALT3\x10\x04\x12\x12\n\x0eTRANSPORT_MQTT\x10\x05\x12\x1b\n\x17TRANSPORT_MULTICAST_UDP\x10\x06\x12\x11\n\rTRANSPORT_API\x10\x07\x42\x11\n\x0fpayload_variant\"\x82\x03\n\x08NodeInfo\x12\x0b\n\x03num\x18\x01 \x01(\r\x12\'\n\x04user\x18\x02 \x01(\x0b\x32\x19.meshtastic.protobuf.User\x12/\n\x08position\x18\x03 \x01(\x0b\x32\x1d.meshtastic.protobuf.Position\x12\x0b\n\x03snr\x18\x04 \x01(\x02\x12\x12\n\nlast_heard\x18\x05 \x01(\x07\x12:\n\x0e\x64\x65vice_metrics\x18\x06 \x01(\x0b\x32\".meshtastic.protobuf.DeviceMetrics\x12\x16\n\x07\x63hannel\x18\x07 \x01(\rB\x05\x92?\x02\x38\x08\x12\x10\n\x08via_mqtt\x18\x08 \x01(\x08\x12\x1d\n\thops_away\x18\t \x01(\rB\x05\x92?\x02\x38\x08H\x00\x88\x01\x01\x12\x13\n\x0bis_favorite\x18\n \x01(\x08\x12\x12\n\nis_ignored\x18\x0b \x01(\x08\x12 \n\x18is_key_manually_verified\x18\x0c \x01(\x08\x12\x10\n\x08is_muted\x18\r \x01(\x08\x42\x0c\n\n_hops_away\"\xe6\x01\n\nMyNodeInfo\x12\x13\n\x0bmy_node_num\x18\x01 \x01(\r\x12\x14\n\x0creboot_count\x18\x08 \x01(\r\x12\x17\n\x0fmin_app_version\x18\x0b \x01(\r\x12\x18\n\tdevice_id\x18\x0c \x01(\x0c\x42\x05\x92?\x02\x08\x10\x12\x16\n\x07pio_env\x18\r \x01(\tB\x05\x92?\x02\x08(\x12\x45\n\x10\x66irmware_edition\x18\x0e \x01(\x0e\x32$.meshtastic.protobuf.FirmwareEditionB\x05\x92?\x02\x38\x08\x12\x1b\n\x0cnodedb_count\x18\x0f \x01(\rB\x05\x92?\x02\x38\x10\"\xd8\x01\n\tLogRecord\x12\x17\n\x07message\x18\x01 \x01(\tB\x06\x92?\x03\x08\x80\x03\x12\x0c\n\x04time\x18\x02 \x01(\x07\x12\x15\n\x06source\x18\x03 \x01(\tB\x05\x92?\x02\x08 \x12\x33\n\x05level\x18\x04 \x01(\x0e\x32$.meshtastic.protobuf.LogRecord.Level\"X\n\x05Level\x12\t\n\x05UNSET\x10\x00\x12\x0c\n\x08\x43RITICAL\x10\x32\x12\t\n\x05\x45RROR\x10(\x12\x0b\n\x07WARNING\x10\x1e\x12\x08\n\x04INFO\x10\x14\x12\t\n\x05\x44\x45\x42UG\x10\n\x12\t\n\x05TRACE\x10\x05\"e\n\x0bQueueStatus\x12\x12\n\x03res\x18\x01 \x01(\x05\x42\x05\x92?\x02\x38\x08\x12\x13\n\x04\x66ree\x18\x02 \x01(\rB\x05\x92?\x02\x38\x08\x12\x15\n\x06maxlen\x18\x03 \x01(\rB\x05\x92?\x02\x38\x08\x12\x16\n\x0emesh_packet_id\x18\x04 \x01(\r\"\xbe\x07\n\tFromRadio\x12\x11\n\x02id\x18\x01 \x01(\rB\x05\x92?\x02\x08\x10\x12\x31\n\x06packet\x18\x02 \x01(\x0b\x32\x1f.meshtastic.protobuf.MeshPacketH\x00\x12\x32\n\x07my_info\x18\x03 \x01(\x0b\x32\x1f.meshtastic.protobuf.MyNodeInfoH\x00\x12\x32\n\tnode_info\x18\x04 \x01(\x0b\x32\x1d.meshtastic.protobuf.NodeInfoH\x00\x12-\n\x06\x63onfig\x18\x05 \x01(\x0b\x32\x1b.meshtastic.protobuf.ConfigH\x00\x12\x34\n\nlog_record\x18\x06 \x01(\x0b\x32\x1e.meshtastic.protobuf.LogRecordH\x00\x12\x1c\n\x12\x63onfig_complete_id\x18\x07 \x01(\rH\x00\x12\x12\n\x08rebooted\x18\x08 \x01(\x08H\x00\x12\x39\n\x0cmoduleConfig\x18\t \x01(\x0b\x32!.meshtastic.protobuf.ModuleConfigH\x00\x12/\n\x07\x63hannel\x18\n \x01(\x0b\x32\x1c.meshtastic.protobuf.ChannelH\x00\x12\x37\n\x0bqueueStatus\x18\x0b \x01(\x0b\x32 .meshtastic.protobuf.QueueStatusH\x00\x12\x33\n\x0cxmodemPacket\x18\x0c \x01(\x0b\x32\x1b.meshtastic.protobuf.XModemH\x00\x12\x37\n\x08metadata\x18\r \x01(\x0b\x32#.meshtastic.protobuf.DeviceMetadataH\x00\x12M\n\x16mqttClientProxyMessage\x18\x0e \x01(\x0b\x32+.meshtastic.protobuf.MqttClientProxyMessageH\x00\x12\x31\n\x08\x66ileInfo\x18\x0f \x01(\x0b\x32\x1d.meshtastic.protobuf.FileInfoH\x00\x12\x45\n\x12\x63lientNotification\x18\x10 \x01(\x0b\x32\'.meshtastic.protobuf.ClientNotificationH\x00\x12=\n\x0e\x64\x65viceuiConfig\x18\x11 \x01(\x0b\x32#.meshtastic.protobuf.DeviceUIConfigH\x00\x12>\n\x0flockdown_status\x18\x12 \x01(\x0b\x32#.meshtastic.protobuf.LockdownStatusH\x00\x42\x11\n\x0fpayload_variant\"\x95\x02\n\x0eLockdownStatus\x12\x38\n\x05state\x18\x01 \x01(\x0e\x32).meshtastic.protobuf.LockdownStatus.State\x12\x1a\n\x0block_reason\x18\x02 \x01(\tB\x05\x92?\x02\x08 \x12\x17\n\x0f\x62oots_remaining\x18\x03 \x01(\r\x12\x19\n\x11valid_until_epoch\x18\x04 \x01(\r\x12\x17\n\x0f\x62\x61\x63koff_seconds\x18\x05 \x01(\r\"`\n\x05State\x12\x15\n\x11STATE_UNSPECIFIED\x10\x00\x12\x13\n\x0fNEEDS_PROVISION\x10\x01\x12\n\n\x06LOCKED\x10\x02\x12\x0c\n\x08UNLOCKED\x10\x03\x12\x11\n\rUNLOCK_FAILED\x10\x04\"\xb8\x04\n\x12\x43lientNotification\x12\x15\n\x08reply_id\x18\x01 \x01(\rH\x01\x88\x01\x01\x12\x0c\n\x04time\x18\x02 \x01(\x07\x12\x33\n\x05level\x18\x03 \x01(\x0e\x32$.meshtastic.protobuf.LogRecord.Level\x12\x17\n\x07message\x18\x04 \x01(\tB\x06\x92?\x03\x08\x90\x03\x12Z\n\x1ekey_verification_number_inform\x18\x0b \x01(\x0b\x32\x30.meshtastic.protobuf.KeyVerificationNumberInformH\x00\x12\\\n\x1fkey_verification_number_request\x18\x0c \x01(\x0b\x32\x31.meshtastic.protobuf.KeyVerificationNumberRequestH\x00\x12K\n\x16key_verification_final\x18\r \x01(\x0b\x32).meshtastic.protobuf.KeyVerificationFinalH\x00\x12I\n\x15\x64uplicated_public_key\x18\x0e \x01(\x0b\x32(.meshtastic.protobuf.DuplicatedPublicKeyH\x00\x12=\n\x0flow_entropy_key\x18\x0f \x01(\x0b\x32\".meshtastic.protobuf.LowEntropyKeyH\x00\x42\x11\n\x0fpayload_variantB\x0b\n\t_reply_id\"e\n\x1bKeyVerificationNumberInform\x12\r\n\x05nonce\x18\x01 \x01(\x04\x12\x1e\n\x0fremote_longname\x18\x02 \x01(\tB\x05\x92?\x02\x08(\x12\x17\n\x0fsecurity_number\x18\x03 \x01(\r\"M\n\x1cKeyVerificationNumberRequest\x12\r\n\x05nonce\x18\x01 \x01(\x04\x12\x1e\n\x0fremote_longname\x18\x02 \x01(\tB\x05\x92?\x02\x08(\"\x7f\n\x14KeyVerificationFinal\x12\r\n\x05nonce\x18\x01 \x01(\x04\x12\x1e\n\x0fremote_longname\x18\x02 \x01(\tB\x05\x92?\x02\x08(\x12\x10\n\x08isSender\x18\x03 \x01(\x08\x12&\n\x17verification_characters\x18\x04 \x01(\tB\x05\x92?\x02\x08\n\"\x15\n\x13\x44uplicatedPublicKey\"\x0f\n\rLowEntropyKey\"9\n\x08\x46ileInfo\x12\x19\n\tfile_name\x18\x01 \x01(\tB\x06\x92?\x03\x08\xe4\x01\x12\x12\n\nsize_bytes\x18\x02 \x01(\r\"\xb8\x02\n\x07ToRadio\x12\x31\n\x06packet\x18\x01 \x01(\x0b\x32\x1f.meshtastic.protobuf.MeshPacketH\x00\x12\x18\n\x0ewant_config_id\x18\x03 \x01(\rH\x00\x12\x14\n\ndisconnect\x18\x04 \x01(\x08H\x00\x12\x33\n\x0cxmodemPacket\x18\x05 \x01(\x0b\x32\x1b.meshtastic.protobuf.XModemH\x00\x12M\n\x16mqttClientProxyMessage\x18\x06 \x01(\x0b\x32+.meshtastic.protobuf.MqttClientProxyMessageH\x00\x12\x33\n\theartbeat\x18\x07 \x01(\x0b\x32\x1e.meshtastic.protobuf.HeartbeatH\x00\x42\x11\n\x0fpayload_variant\"Q\n\nCompressed\x12-\n\x07portnum\x18\x01 \x01(\x0e\x32\x1c.meshtastic.protobuf.PortNum\x12\x14\n\x04\x64\x61ta\x18\x02 \x01(\x0c\x42\x06\x92?\x03\x08\xe9\x01\"\x97\x01\n\x0cNeighborInfo\x12\x0f\n\x07node_id\x18\x01 \x01(\r\x12\x17\n\x0flast_sent_by_id\x18\x02 \x01(\r\x12$\n\x1cnode_broadcast_interval_secs\x18\x03 \x01(\r\x12\x37\n\tneighbors\x18\x04 \x03(\x0b\x32\x1d.meshtastic.protobuf.NeighborB\x05\x92?\x02\x10\n\"d\n\x08Neighbor\x12\x0f\n\x07node_id\x18\x01 \x01(\r\x12\x0b\n\x03snr\x18\x02 \x01(\x02\x12\x14\n\x0clast_rx_time\x18\x03 \x01(\x07\x12$\n\x1cnode_broadcast_interval_secs\x18\x04 \x01(\r\"\xf0\x02\n\x0e\x44\x65viceMetadata\x12\x1f\n\x10\x66irmware_version\x18\x01 \x01(\tB\x05\x92?\x02\x08\x12\x12\x1c\n\x14\x64\x65vice_state_version\x18\x02 \x01(\r\x12\x13\n\x0b\x63\x61nShutdown\x18\x03 \x01(\x08\x12\x0f\n\x07hasWifi\x18\x04 \x01(\x08\x12\x14\n\x0chasBluetooth\x18\x05 \x01(\x08\x12\x13\n\x0bhasEthernet\x18\x06 \x01(\x08\x12;\n\x04role\x18\x07 \x01(\x0e\x32-.meshtastic.protobuf.Config.DeviceConfig.Role\x12\x16\n\x0eposition_flags\x18\x08 \x01(\r\x12\x34\n\x08hw_model\x18\t \x01(\x0e\x32\".meshtastic.protobuf.HardwareModel\x12\x19\n\x11hasRemoteHardware\x18\n \x01(\x08\x12\x0e\n\x06hasPKC\x18\x0b \x01(\x08\x12\x18\n\x10\x65xcluded_modules\x18\x0c \x01(\r\"\x1a\n\tHeartbeat\x12\r\n\x05nonce\x18\x01 \x01(\r\"^\n\x15NodeRemoteHardwarePin\x12\x10\n\x08node_num\x18\x01 \x01(\r\x12\x33\n\x03pin\x18\x02 \x01(\x0b\x32&.meshtastic.protobuf.RemoteHardwarePin\"{\n\x0e\x43hunkedPayload\x12\x12\n\npayload_id\x18\x01 \x01(\r\x12\x1a\n\x0b\x63hunk_count\x18\x02 \x01(\rB\x05\x92?\x02\x38\x10\x12\x1a\n\x0b\x63hunk_index\x18\x03 \x01(\rB\x05\x92?\x02\x38\x10\x12\x1d\n\rpayload_chunk\x18\x04 \x01(\x0c\x42\x06\x92?\x03\x08\xe4\x01\"\x1f\n\rresend_chunks\x12\x0e\n\x06\x63hunks\x18\x01 \x03(\r\"\xb3\x01\n\x16\x43hunkedPayloadResponse\x12\x12\n\npayload_id\x18\x01 \x01(\r\x12\x1a\n\x10request_transfer\x18\x02 \x01(\x08H\x00\x12\x19\n\x0f\x61\x63\x63\x65pt_transfer\x18\x03 \x01(\x08H\x00\x12;\n\rresend_chunks\x18\x04 \x01(\x0b\x32\".meshtastic.protobuf.resend_chunksH\x00\x42\x11\n\x0fpayload_variant*\xac\x15\n\rHardwareModel\x12\t\n\x05UNSET\x10\x00\x12\x0c\n\x08TLORA_V2\x10\x01\x12\x0c\n\x08TLORA_V1\x10\x02\x12\x12\n\x0eTLORA_V2_1_1P6\x10\x03\x12\t\n\x05TBEAM\x10\x04\x12\x0f\n\x0bHELTEC_V2_0\x10\x05\x12\x0e\n\nTBEAM_V0P7\x10\x06\x12\n\n\x06T_ECHO\x10\x07\x12\x10\n\x0cTLORA_V1_1P3\x10\x08\x12\x0b\n\x07RAK4631\x10\t\x12\x0f\n\x0bHELTEC_V2_1\x10\n\x12\r\n\tHELTEC_V1\x10\x0b\x12\x18\n\x14LILYGO_TBEAM_S3_CORE\x10\x0c\x12\x0c\n\x08RAK11200\x10\r\x12\x0b\n\x07NANO_G1\x10\x0e\x12\x12\n\x0eTLORA_V2_1_1P8\x10\x0f\x12\x0f\n\x0bTLORA_T3_S3\x10\x10\x12\x14\n\x10NANO_G1_EXPLORER\x10\x11\x12\x11\n\rNANO_G2_ULTRA\x10\x12\x12\r\n\tLORA_TYPE\x10\x13\x12\x0b\n\x07WIPHONE\x10\x14\x12\x0e\n\nWIO_WM1110\x10\x15\x12\x0b\n\x07RAK2560\x10\x16\x12\x13\n\x0fHELTEC_HRU_3601\x10\x17\x12\x1a\n\x16HELTEC_WIRELESS_BRIDGE\x10\x18\x12\x0e\n\nSTATION_G1\x10\x19\x12\x0c\n\x08RAK11310\x10\x1a\x12\x14\n\x10SENSELORA_RP2040\x10\x1b\x12\x10\n\x0cSENSELORA_S3\x10\x1c\x12\r\n\tCANARYONE\x10\x1d\x12\x0f\n\x0bRP2040_LORA\x10\x1e\x12\x0e\n\nSTATION_G2\x10\x1f\x12\x11\n\rLORA_RELAY_V1\x10 \x12\x0f\n\x0bT_ECHO_PLUS\x10!\x12\x07\n\x03PPR\x10\"\x12\x0f\n\x0bGENIEBLOCKS\x10#\x12\x11\n\rNRF52_UNKNOWN\x10$\x12\r\n\tPORTDUINO\x10%\x12\x0f\n\x0b\x41NDROID_SIM\x10&\x12\n\n\x06\x44IY_V1\x10\'\x12\x15\n\x11NRF52840_PCA10059\x10(\x12\n\n\x06\x44R_DEV\x10)\x12\x0b\n\x07M5STACK\x10*\x12\r\n\tHELTEC_V3\x10+\x12\x11\n\rHELTEC_WSL_V3\x10,\x12\x13\n\x0f\x42\x45TAFPV_2400_TX\x10-\x12\x17\n\x13\x42\x45TAFPV_900_NANO_TX\x10.\x12\x0c\n\x08RPI_PICO\x10/\x12\x1b\n\x17HELTEC_WIRELESS_TRACKER\x10\x30\x12\x19\n\x15HELTEC_WIRELESS_PAPER\x10\x31\x12\n\n\x06T_DECK\x10\x32\x12\x0e\n\nT_WATCH_S3\x10\x33\x12\x11\n\rPICOMPUTER_S3\x10\x34\x12\x0f\n\x0bHELTEC_HT62\x10\x35\x12\x12\n\x0e\x45\x42YTE_ESP32_S3\x10\x36\x12\x11\n\rESP32_S3_PICO\x10\x37\x12\r\n\tCHATTER_2\x10\x38\x12\x1e\n\x1aHELTEC_WIRELESS_PAPER_V1_0\x10\x39\x12 \n\x1cHELTEC_WIRELESS_TRACKER_V1_0\x10:\x12\x0b\n\x07UNPHONE\x10;\x12\x0c\n\x08TD_LORAC\x10<\x12\x13\n\x0f\x43\x44\x45\x42YTE_EORA_S3\x10=\x12\x0f\n\x0bTWC_MESH_V4\x10>\x12\x16\n\x12NRF52_PROMICRO_DIY\x10?\x12\x1f\n\x1bRADIOMASTER_900_BANDIT_NANO\x10@\x12\x1c\n\x18HELTEC_CAPSULE_SENSOR_V3\x10\x41\x12\x1d\n\x19HELTEC_VISION_MASTER_T190\x10\x42\x12\x1d\n\x19HELTEC_VISION_MASTER_E213\x10\x43\x12\x1d\n\x19HELTEC_VISION_MASTER_E290\x10\x44\x12\x19\n\x15HELTEC_MESH_NODE_T114\x10\x45\x12\x16\n\x12SENSECAP_INDICATOR\x10\x46\x12\x13\n\x0fTRACKER_T1000_E\x10G\x12\x0b\n\x07RAK3172\x10H\x12\n\n\x06WIO_E5\x10I\x12\x1a\n\x16RADIOMASTER_900_BANDIT\x10J\x12\x13\n\x0fME25LS01_4Y10TD\x10K\x12\x18\n\x14RP2040_FEATHER_RFM95\x10L\x12\x15\n\x11M5STACK_COREBASIC\x10M\x12\x11\n\rM5STACK_CORE2\x10N\x12\r\n\tRPI_PICO2\x10O\x12\x12\n\x0eM5STACK_CORES3\x10P\x12\x11\n\rSEEED_XIAO_S3\x10Q\x12\x0b\n\x07MS24SF1\x10R\x12\x0c\n\x08TLORA_C6\x10S\x12\x0f\n\x0bWISMESH_TAP\x10T\x12\r\n\tROUTASTIC\x10U\x12\x0c\n\x08MESH_TAB\x10V\x12\x0c\n\x08MESHLINK\x10W\x12\x12\n\x0eXIAO_NRF52_KIT\x10X\x12\x10\n\x0cTHINKNODE_M1\x10Y\x12\x10\n\x0cTHINKNODE_M2\x10Z\x12\x0f\n\x0bT_ETH_ELITE\x10[\x12\x15\n\x11HELTEC_SENSOR_HUB\x10\\\x12\r\n\tMUZI_BASE\x10]\x12\x16\n\x12HELTEC_MESH_POCKET\x10^\x12\x14\n\x10SEEED_SOLAR_NODE\x10_\x12\x18\n\x14NOMADSTAR_METEOR_PRO\x10`\x12\r\n\tCROWPANEL\x10\x61\x12\x0b\n\x07LINK_32\x10\x62\x12\x18\n\x14SEEED_WIO_TRACKER_L1\x10\x63\x12\x1d\n\x19SEEED_WIO_TRACKER_L1_EINK\x10\x64\x12\x0f\n\x0bMUZI_R1_NEO\x10\x65\x12\x0e\n\nT_DECK_PRO\x10\x66\x12\x10\n\x0cT_LORA_PAGER\x10g\x12\x14\n\x10M5STACK_RESERVED\x10h\x12\x0f\n\x0bWISMESH_TAG\x10i\x12\x0b\n\x07RAK3312\x10j\x12\x10\n\x0cTHINKNODE_M5\x10k\x12\x15\n\x11HELTEC_MESH_SOLAR\x10l\x12\x0f\n\x0bT_ECHO_LITE\x10m\x12\r\n\tHELTEC_V4\x10n\x12\x0f\n\x0bM5STACK_C6L\x10o\x12\x19\n\x15M5STACK_CARDPUTER_ADV\x10p\x12\x1e\n\x1aHELTEC_WIRELESS_TRACKER_V2\x10q\x12\x11\n\rT_WATCH_ULTRA\x10r\x12\x10\n\x0cTHINKNODE_M3\x10s\x12\x12\n\x0eWISMESH_TAP_V2\x10t\x12\x0b\n\x07RAK3401\x10u\x12\x0b\n\x07RAK6421\x10v\x12\x10\n\x0cTHINKNODE_M4\x10w\x12\x10\n\x0cTHINKNODE_M6\x10x\x12\x12\n\x0eMESHSTICK_1262\x10y\x12\x10\n\x0cTBEAM_1_WATT\x10z\x12\x14\n\x10T5_S3_EPAPER_PRO\x10{\x12\r\n\tTBEAM_BPF\x10|\x12\x12\n\x0eMINI_EPAPER_S3\x10}\x12\x13\n\x0fTDISPLAY_S3_PRO\x10~\x12\x19\n\x15HELTEC_MESH_NODE_T096\x10\x7f\x12\x18\n\x13TRACKER_T1000_E_PRO\x10\x80\x01\x12\x11\n\x0cTHINKNODE_M7\x10\x81\x01\x12\x11\n\x0cTHINKNODE_M8\x10\x82\x01\x12\x11\n\x0cTHINKNODE_M9\x10\x83\x01\x12\x11\n\x0cHELTEC_V4_R8\x10\x84\x01\x12\x18\n\x13HELTEC_MESH_NODE_T1\x10\x85\x01\x12\x0f\n\nSTATION_G3\x10\x86\x01\x12\x13\n\x0eT_IMPULSE_PLUS\x10\x87\x01\x12\x10\n\x0bT_ECHO_CARD\x10\x88\x01\x12\x19\n\x14SEEED_WIO_TRACKER_L2\x10\x89\x01\x12\x11\n\x0c\x43ROWPANEL_P4\x10\x8a\x01\x12\x19\n\x14HELTEC_MESH_TOWER_V2\x10\x8b\x01\x12\x13\n\x0eMESHNOLOGY_W10\x10\x8c\x01\x12\x0f\n\nPRIVATE_HW\x10\xff\x01*,\n\tConstants\x12\x08\n\x04ZERO\x10\x00\x12\x15\n\x10\x44\x41TA_PAYLOAD_LEN\x10\xe9\x01*\xb4\x02\n\x11\x43riticalErrorCode\x12\x08\n\x04NONE\x10\x00\x12\x0f\n\x0bTX_WATCHDOG\x10\x01\x12\x14\n\x10SLEEP_ENTER_WAIT\x10\x02\x12\x0c\n\x08NO_RADIO\x10\x03\x12\x0f\n\x0bUNSPECIFIED\x10\x04\x12\x15\n\x11UBLOX_UNIT_FAILED\x10\x05\x12\r\n\tNO_AXP192\x10\x06\x12\x19\n\x15INVALID_RADIO_SETTING\x10\x07\x12\x13\n\x0fTRANSMIT_FAILED\x10\x08\x12\x0c\n\x08\x42ROWNOUT\x10\t\x12\x12\n\x0eSX1262_FAILURE\x10\n\x12\x11\n\rRADIO_SPI_BUG\x10\x0b\x12 \n\x1c\x46LASH_CORRUPTION_RECOVERABLE\x10\x0c\x12\"\n\x1e\x46LASH_CORRUPTION_UNRECOVERABLE\x10\r*\x7f\n\x0f\x46irmwareEdition\x12\x0b\n\x07VANILLA\x10\x00\x12\x11\n\rSMART_CITIZEN\x10\x01\x12\x0e\n\nOPEN_SAUCE\x10\x10\x12\n\n\x06\x44\x45\x46\x43ON\x10\x11\x12\x0f\n\x0b\x42URNING_MAN\x10\x12\x12\x0e\n\nHAMVENTION\x10\x13\x12\x0f\n\x0b\x44IY_EDITION\x10\x7f*\x80\x03\n\x0f\x45xcludedModules\x12\x11\n\rEXCLUDED_NONE\x10\x00\x12\x0f\n\x0bMQTT_CONFIG\x10\x01\x12\x11\n\rSERIAL_CONFIG\x10\x02\x12\x13\n\x0f\x45XTNOTIF_CONFIG\x10\x04\x12\x17\n\x13STOREFORWARD_CONFIG\x10\x08\x12\x14\n\x10RANGETEST_CONFIG\x10\x10\x12\x14\n\x10TELEMETRY_CONFIG\x10 \x12\x14\n\x10\x43\x41NNEDMSG_CONFIG\x10@\x12\x11\n\x0c\x41UDIO_CONFIG\x10\x80\x01\x12\x1a\n\x15REMOTEHARDWARE_CONFIG\x10\x80\x02\x12\x18\n\x13NEIGHBORINFO_CONFIG\x10\x80\x04\x12\x1b\n\x16\x41MBIENTLIGHTING_CONFIG\x10\x80\x08\x12\x1b\n\x16\x44\x45TECTIONSENSOR_CONFIG\x10\x80\x10\x12\x16\n\x11PAXCOUNTER_CONFIG\x10\x80 \x12\x15\n\x10\x42LUETOOTH_CONFIG\x10\x80@\x12\x14\n\x0eNETWORK_CONFIG\x10\x80\x80\x01\x42`\n\x14org.meshtastic.protoB\nMeshProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -148,15 +148,15 @@ _CHUNKEDPAYLOAD.fields_by_name['payload_chunk']._options = None _CHUNKEDPAYLOAD.fields_by_name['payload_chunk']._serialized_options = b'\222?\003\010\344\001' _globals['_HARDWAREMODEL']._serialized_start=9550 - _globals['_HARDWAREMODEL']._serialized_end=12188 - _globals['_CONSTANTS']._serialized_start=12190 - _globals['_CONSTANTS']._serialized_end=12234 - _globals['_CRITICALERRORCODE']._serialized_start=12237 - _globals['_CRITICALERRORCODE']._serialized_end=12545 - _globals['_FIRMWAREEDITION']._serialized_start=12547 - _globals['_FIRMWAREEDITION']._serialized_end=12674 - _globals['_EXCLUDEDMODULES']._serialized_start=12677 - _globals['_EXCLUDEDMODULES']._serialized_end=13061 + _globals['_HARDWAREMODEL']._serialized_end=12282 + _globals['_CONSTANTS']._serialized_start=12284 + _globals['_CONSTANTS']._serialized_end=12328 + _globals['_CRITICALERRORCODE']._serialized_start=12331 + _globals['_CRITICALERRORCODE']._serialized_end=12639 + _globals['_FIRMWAREEDITION']._serialized_start=12641 + _globals['_FIRMWAREEDITION']._serialized_end=12768 + _globals['_EXCLUDEDMODULES']._serialized_start=12771 + _globals['_EXCLUDEDMODULES']._serialized_end=13155 _globals['_POSITION']._serialized_start=344 _globals['_POSITION']._serialized_end=1265 _globals['_POSITION_LOCSOURCE']._serialized_start=960 diff --git a/meshtastic/protobuf/mesh_pb2.pyi b/meshtastic/protobuf/mesh_pb2.pyi index 506e78856..1de1ed12b 100644 --- a/meshtastic/protobuf/mesh_pb2.pyi +++ b/meshtastic/protobuf/mesh_pb2.pyi @@ -594,6 +594,22 @@ class _HardwareModelEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._ """ Lilygo T-Echo Card """ + SEEED_WIO_TRACKER_L2: _HardwareModel.ValueType # 137 + """ + Seeed Tracker L2 + """ + CROWPANEL_P4: _HardwareModel.ValueType # 138 + """ + Elecrow CrowPanel Advance P4 models, ESP32-P4 and TFT with SX1262 radio plugin + """ + HELTEC_MESH_TOWER_V2: _HardwareModel.ValueType # 139 + """ + Heltec Mesh Tower V2 + """ + MESHNOLOGY_W10: _HardwareModel.ValueType # 140 + """ + Meshnology W10 + """ PRIVATE_HW: _HardwareModel.ValueType # 255 """ ------------------------------------------------------------------------------------------------------------------------------------------ @@ -1171,6 +1187,22 @@ T_ECHO_CARD: HardwareModel.ValueType # 136 """ Lilygo T-Echo Card """ +SEEED_WIO_TRACKER_L2: HardwareModel.ValueType # 137 +""" +Seeed Tracker L2 +""" +CROWPANEL_P4: HardwareModel.ValueType # 138 +""" +Elecrow CrowPanel Advance P4 models, ESP32-P4 and TFT with SX1262 radio plugin +""" +HELTEC_MESH_TOWER_V2: HardwareModel.ValueType # 139 +""" +Heltec Mesh Tower V2 +""" +MESHNOLOGY_W10: HardwareModel.ValueType # 140 +""" +Meshnology W10 +""" PRIVATE_HW: HardwareModel.ValueType # 255 """ ------------------------------------------------------------------------------------------------------------------------------------------ diff --git a/protobufs b/protobufs index dd6c3f850..da60cee58 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit dd6c3f850a56a00cbc2cc5b93db2fd43b1a2f46a +Subproject commit da60cee584c6dc1efbb4a3809b98666505179b85 From 6d76edf8a7b192c51e3a5d26bc5868da556ac3d9 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 29 Jun 2026 20:45:10 +0000 Subject: [PATCH 17/17] bump version to 2.7.10 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2b58163d8..87c90391c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "meshtastic" -version = "2.7.9" +version = "2.7.10" description = "Python API & client shell for talking to Meshtastic devices" authors = ["Meshtastic Developers "] license = "GPL-3.0-only"