From 2625b4a7de70ee6cc6c2e18a1b768ebb26275cda Mon Sep 17 00:00:00 2001 From: the8thbit Date: Fri, 23 Oct 2026 14:15:20 -0500 Subject: [PATCH] Format remote configuration values consistently with local settings --- meshtastic/node.py | 9 ++++- meshtastic/tests/test_node.py | 68 +++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/meshtastic/node.py b/meshtastic/node.py index 7ce2da084..4340fb538 100644 --- a/meshtastic/node.py +++ b/meshtastic/node.py @@ -18,6 +18,7 @@ message_to_json, generate_channel_hash, to_node_num, + toStr, flags_to_list, ) @@ -156,7 +157,13 @@ def onResponseRequestSettings(self, p): if config_values is not None: raw_config = getattr(getattr(adminMessage['raw'], oneof), camel_to_snake(field)) config_values.CopyFrom(raw_config) - print(f"{str(camel_to_snake(field))}:\n{str(config_values)}") + for pref, value in config_values.ListFields(): + if hasattr(pref, "is_repeated"): + repeated = pref.is_repeated + else: + repeated = pref.label == pref.LABEL_REPEATED + formatted = [toStr(v) for v in value] if repeated else toStr(value) + print(f"{camel_to_snake(field)}.{pref.name}: {formatted}") def requestConfig(self, configType): """Request the config from the node via admin message""" diff --git a/meshtastic/tests/test_node.py b/meshtastic/tests/test_node.py index 425350628..c2879e65f 100644 --- a/meshtastic/tests/test_node.py +++ b/meshtastic/tests/test_node.py @@ -1972,3 +1972,71 @@ def test_start_ota_remote_node_raises_error(): # anode._timeout = Timeout(0.01) # result = anode.waitForConfig() # assert not result + + +@pytest.mark.unit +def test_requested_security_keys_match_local_format(capsys): + """Remote settings should use the same reusable base64 values as local --get.""" + from google.protobuf.json_format import MessageToDict # pylint: disable=import-outside-toplevel + from meshtastic.__main__ import getPref # pylint: disable=import-outside-toplevel + + iface = MagicMock(spec=MeshInterface) + iface._acknowledgment = MagicMock() + node = Node(iface, 12345678) + security = config_pb2.Config.SecurityConfig( + public_key=b"\x00\x80\xff", private_key=b"\x81\x02\x00", + admin_key=[b"\xff\x00\x80", b"\x01\x02\x03", b""], serial_enabled=True, + ) + node.localConfig.security.CopyFrom(security) + assert getPref(node, "security") + local_output = capsys.readouterr().out + node.localConfig.ClearField("security") + message = admin_pb2.AdminMessage() + message.get_config_response.security.CopyFrom(security) + packet = {"decoded": {"admin": {**MessageToDict(message), "raw": message}}} + + node.onResponseRequestSettings(packet) + + remote_output = capsys.readouterr().out + assert remote_output.strip() == local_output.strip() + assert "security.admin_key: ['base64:/wCA', 'base64:AQID', 'base64:']" in remote_output + assert "security.public_key: base64:AID/" in remote_output + assert "security.private_key: base64:gQIA" in remote_output + assert "security.serial_enabled: True" in remote_output + assert node.localConfig.security == security + assert iface._acknowledgment.receivedAck is True + + +@pytest.mark.unit +def test_requested_module_config_matches_local_format(capsys): + """The module response path also preserves scalar setting values.""" + from google.protobuf.json_format import MessageToDict # pylint: disable=import-outside-toplevel + from meshtastic.__main__ import getPref # pylint: disable=import-outside-toplevel + + iface = MagicMock(spec=MeshInterface) + iface._acknowledgment = MagicMock() + node = Node(iface, 12345678) + message = admin_pb2.AdminMessage() + message.get_module_config_response.mqtt.enabled = True + message.get_module_config_response.mqtt.address = "mqtt.example.org" + node.moduleConfig.mqtt.CopyFrom(message.get_module_config_response.mqtt) + assert getPref(node, "mqtt") + local_output = capsys.readouterr().out + node.moduleConfig.ClearField("mqtt") + packet = {"decoded": {"admin": {**MessageToDict(message), "raw": message}}} + + node.onResponseRequestSettings(packet) + + assert capsys.readouterr().out.strip() == local_output.strip() + assert node.moduleConfig.mqtt == message.get_module_config_response.mqtt + + +@pytest.mark.unit +def test_requested_config_nak(capsys): + """A rejected remote settings request still records and reports its NAK.""" + iface = MagicMock(spec=MeshInterface) + iface._acknowledgment = MagicMock() + node = Node(iface, 12345678) + node.onResponseRequestSettings({"decoded": {"routing": {"errorReason": "NOT_AUTHORIZED"}}}) + assert capsys.readouterr().out == "Error on response: NOT_AUTHORIZED\n" + assert iface._acknowledgment.receivedNak is True