From 0bb3389b3b615c0fc2fa4bd0be708b12bf639f37 Mon Sep 17 00:00:00 2001 From: pdxlocations Date: Mon, 23 Jun 2025 22:11:50 -0700 Subject: [PATCH 1/8] init --- meshtastic/__main__.py | 3 +++ meshtastic/mesh_interface.py | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index ec755913..06b99331 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -1097,6 +1097,7 @@ def export_config(interface) -> str: owner_short = interface.getShortName() channel_url = interface.localNode.getURL() myinfo = interface.getMyNodeInfo() + canned_messages = interface.getCannedMessage() pos = myinfo.get("position") lat = None lon = None @@ -1115,6 +1116,8 @@ def export_config(interface) -> str: configObj["channelUrl"] = channel_url else: configObj["channel_url"] = channel_url + # if canned_messages: + # configObj["cannedMessages"] = canned_messages # lat and lon don't make much sense without the other (so fill with 0s), and alt isn't meaningful without both if lat or lon: configObj["location"] = {"lat": lat or float(0), "lon": lon or float(0)} diff --git a/meshtastic/mesh_interface.py b/meshtastic/mesh_interface.py index cf9009ce..9f80c54d 100644 --- a/meshtastic/mesh_interface.py +++ b/meshtastic/mesh_interface.py @@ -1076,6 +1076,13 @@ def getPublicKey(self): if user is not None: return user.get("publicKey", None) return None + + def getCannedMessage(self): + """Fetch and return the canned message from the local node.""" + if hasattr(self, "localNode") and self.localNode: + return self.localNode.get_canned_message() + else: + raise RuntimeError("No local node available.") def _waitConnected(self, timeout=30.0): """Block until the initial node db download is complete, or timeout From 84417f0bb14beba9607fcb01d08a7a0be932618f Mon Sep 17 00:00:00 2001 From: pdxlocations Date: Mon, 23 Jun 2025 22:39:24 -0700 Subject: [PATCH 2/8] working export-config --- meshtastic/__main__.py | 13 +++++++++---- meshtastic/mesh_interface.py | 11 ++++++++--- meshtastic/node.py | 2 -- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index 06b99331..bd042515 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -928,12 +928,14 @@ def setSimpleConfig(modem_preset): if args.get_canned_message: closeNow = True print("") - interface.getNode(args.dest, **getNode_kwargs).get_canned_message() + messages = interface.getNode(args.dest, **getNode_kwargs).get_canned_message() + print(f"canned_plugin_message:{messages}") if args.get_ringtone: closeNow = True print("") - interface.getNode(args.dest, **getNode_kwargs).get_ringtone() + ringtone = interface.getNode(args.dest, **getNode_kwargs).get_ringtone() + print(f"ringtone:{ringtone}") if args.info: print("") @@ -1098,6 +1100,7 @@ def export_config(interface) -> str: channel_url = interface.localNode.getURL() myinfo = interface.getMyNodeInfo() canned_messages = interface.getCannedMessage() + ringtone = interface.getRingtone() pos = myinfo.get("position") lat = None lon = None @@ -1116,8 +1119,10 @@ def export_config(interface) -> str: configObj["channelUrl"] = channel_url else: configObj["channel_url"] = channel_url - # if canned_messages: - # configObj["cannedMessages"] = canned_messages + if canned_messages: + configObj["canned_messages"] = canned_messages + if ringtone: + configObj["ringtone"] = ringtone # lat and lon don't make much sense without the other (so fill with 0s), and alt isn't meaningful without both if lat or lon: configObj["location"] = {"lat": lat or float(0), "lon": lon or float(0)} diff --git a/meshtastic/mesh_interface.py b/meshtastic/mesh_interface.py index 9f80c54d..f2c6fa2c 100644 --- a/meshtastic/mesh_interface.py +++ b/meshtastic/mesh_interface.py @@ -1076,13 +1076,18 @@ def getPublicKey(self): if user is not None: return user.get("publicKey", None) return None - + def getCannedMessage(self): """Fetch and return the canned message from the local node.""" if hasattr(self, "localNode") and self.localNode: return self.localNode.get_canned_message() - else: - raise RuntimeError("No local node available.") + return None + + def getRingtone(self): + """Fetch and return the ringtone from the local node.""" + if hasattr(self, "localNode") and self.localNode: + return self.localNode.get_ringtone() + return None def _waitConnected(self, timeout=30.0): """Block until the initial node db download is complete, or timeout diff --git a/meshtastic/node.py b/meshtastic/node.py index d006075f..deb8b7bc 100644 --- a/meshtastic/node.py +++ b/meshtastic/node.py @@ -454,7 +454,6 @@ def get_ringtone(self): if self.ringtonePart: self.ringtone += self.ringtonePart - print(f"ringtone:{self.ringtone}") logging.debug(f"ringtone:{self.ringtone}") return self.ringtone @@ -530,7 +529,6 @@ def get_canned_message(self): if self.cannedPluginMessageMessages: self.cannedPluginMessage += self.cannedPluginMessageMessages - print(f"canned_plugin_message:{self.cannedPluginMessage}") logging.debug(f"canned_plugin_message:{self.cannedPluginMessage}") return self.cannedPluginMessage From 308ac933994a7a705b2e0be3ea97bf9c526931dc Mon Sep 17 00:00:00 2001 From: pdxlocations Date: Tue, 24 Jun 2025 07:40:11 -0700 Subject: [PATCH 3/8] add to configure --- meshtastic/__main__.py | 12 ++++++++++-- meshtastic/mesh_interface.py | 16 +++++++++------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index bd042515..f751f07a 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -362,7 +362,6 @@ def onConnected(interface): print(f"Setting device owner short to {args.set_owner_short}") interface.getNode(args.dest, False, **getNode_kwargs).setOwner(long_name=args.set_owner, short_name=args.set_owner_short) - # TODO: add to export-config and configure if args.set_canned_message: closeNow = True waitForAckNak = True @@ -371,7 +370,6 @@ def onConnected(interface): args.set_canned_message ) - # TODO: add to export-config and configure if args.set_ringtone: closeNow = True waitForAckNak = True @@ -705,6 +703,16 @@ def onConnected(interface): interface.getNode(args.dest, **getNode_kwargs).setURL(configuration["channelUrl"]) time.sleep(0.5) + if "canned_messages" in configuration: + print("Setting canned message messages to", configuration["canned_messages"]) + interface.getNode(args.dest, **getNode_kwargs).set_canned_message(configuration["canned_messages"]) + time.sleep(0.5) + + if "ringtone" in configuration: + print("Setting ringtone to", configuration["ringtone"]) + interface.getNode(args.dest, **getNode_kwargs).set_ringtone(configuration["ringtone"]) + time.sleep(0.5) + if "location" in configuration: alt = 0 lat = 0.0 diff --git a/meshtastic/mesh_interface.py b/meshtastic/mesh_interface.py index f2c6fa2c..16ec9758 100644 --- a/meshtastic/mesh_interface.py +++ b/meshtastic/mesh_interface.py @@ -1078,15 +1078,17 @@ def getPublicKey(self): return None def getCannedMessage(self): - """Fetch and return the canned message from the local node.""" - if hasattr(self, "localNode") and self.localNode: - return self.localNode.get_canned_message() + """Get canned message""" + node = self.localNode + if node is not None: + return node.get_canned_message() return None - + def getRingtone(self): - """Fetch and return the ringtone from the local node.""" - if hasattr(self, "localNode") and self.localNode: - return self.localNode.get_ringtone() + """Get ringtone""" + node = self.localNode + if node is not None: + return node.get_ringtone() return None def _waitConnected(self, timeout=30.0): From eb453a2e8a46cbae48e5f589c1b514607f34aeac Mon Sep 17 00:00:00 2001 From: pdxlocations Date: Tue, 24 Jun 2025 08:16:45 -0700 Subject: [PATCH 4/8] add to tests --- meshtastic/tests/test_main.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/meshtastic/tests/test_main.py b/meshtastic/tests/test_main.py index 235829d4..e2ca6c13 100644 --- a/meshtastic/tests/test_main.py +++ b/meshtastic/tests/test_main.py @@ -1724,6 +1724,8 @@ def test_main_export_config(capsys): mo.getLongName.return_value = "foo" mo.getShortName.return_value = "oof" mo.localNode.getURL.return_value = "bar" + mo.getCannedMessage.return_value = "foo|bar" + mo.getRingtone.return_value = "24:d=32,o=5" mo.getMyNodeInfo().get.return_value = { "latitudeI": 1100000000, "longitudeI": 1200000000, From d83f7b23070b21e72c7d91f6d0b000eaf9eb3394 Mon Sep 17 00:00:00 2001 From: pdxlocations Date: Tue, 24 Jun 2025 10:12:12 -0700 Subject: [PATCH 5/8] add to example config --- example_config.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/example_config.yaml b/example_config.yaml index 4020525b..acbdc332 100644 --- a/example_config.yaml +++ b/example_config.yaml @@ -4,6 +4,9 @@ owner_short: BOB channel_url: https://www.meshtastic.org/e/#CgMSAQESCDgBQANIAVAe +canned_messages: Hi|Bye|Yes|No|Ok +ringtone: 24:d=32,o=5,b=565:f6,p,f6,4p,p,f6,p,f6,2p,p,b6,p,b6,p,b6,p,b6,p,b,p,b,p,b,p,b,p,b,p,b,p,b,p,b,1p.,2p.,p + location: lat: 35.88888 lon: -93.88888 From 428be9fbcea086ad723091426149498b9cde0b40 Mon Sep 17 00:00:00 2001 From: pdxlocations Date: Tue, 24 Jun 2025 11:16:23 -0700 Subject: [PATCH 6/8] add mesh_interface tests --- meshtastic/tests/test_mesh_interface.py | 35 ++++++++++++++++++++----- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/meshtastic/tests/test_mesh_interface.py b/meshtastic/tests/test_mesh_interface.py index 65c0725f..02a3d137 100644 --- a/meshtastic/tests/test_mesh_interface.py +++ b/meshtastic/tests/test_mesh_interface.py @@ -11,12 +11,12 @@ from .. import BROADCAST_ADDR, LOCAL_ADDR from ..mesh_interface import MeshInterface, _timeago from ..node import Node -try: - # Depends upon the powermon group, not installed by default - from ..slog import LogSet - from ..powermon import SimPowerSupply -except ImportError: - pytest.skip("Can't import LogSet or SimPowerSupply", allow_module_level=True) +# try: +# # Depends upon the powermon group, not installed by default +# from ..slog import LogSet +# from ..powermon import SimPowerSupply +# except ImportError: +# pytest.skip("Can't import LogSet or SimPowerSupply", allow_module_level=True) # TODO # from ..config import Config @@ -525,6 +525,28 @@ def test_getMyNodeInfo(): myinfo = iface.getMyNodeInfo() assert myinfo == anode +@pytest.mark.unit +@pytest.mark.usefixtures("reset_mt_config") +def test_getCannedMessage(): + """Test MeshInterface.getCannedMessage()""" + iface = MeshInterface(noProto=True) + node = MagicMock() + node.get_canned_message.return_value = "Hi|Bye|Yes" + iface.localNode = node + result = iface.getCannedMessage() + assert result == "Hi|Bye|Yes" + + +@pytest.mark.unit +@pytest.mark.usefixtures("reset_mt_config") +def test_getRingtone(): + """Test MeshInterface.getRingtone()""" + iface = MeshInterface(noProto=True) + node = MagicMock() + node.get_ringtone.return_value = "foo,bar" + iface.localNode = node + result = iface.getRingtone() + assert result == "foo,bar" @pytest.mark.unit @pytest.mark.usefixtures("reset_mt_config") @@ -543,7 +565,6 @@ def test_generatePacketId(capsys): assert err == "" assert pytest_wrapped_e.type == MeshInterface.MeshInterfaceError - @pytest.mark.unit @pytest.mark.usefixtures("reset_mt_config") def test_fixupPosition_empty_pos(): From c76e4dac872326489eb31e3d51073ed0bd852b76 Mon Sep 17 00:00:00 2001 From: pdxlocations Date: Tue, 24 Jun 2025 11:17:01 -0700 Subject: [PATCH 7/8] undo comment --- meshtastic/tests/test_mesh_interface.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/meshtastic/tests/test_mesh_interface.py b/meshtastic/tests/test_mesh_interface.py index 02a3d137..c561ab20 100644 --- a/meshtastic/tests/test_mesh_interface.py +++ b/meshtastic/tests/test_mesh_interface.py @@ -11,12 +11,12 @@ from .. import BROADCAST_ADDR, LOCAL_ADDR from ..mesh_interface import MeshInterface, _timeago from ..node import Node -# try: -# # Depends upon the powermon group, not installed by default -# from ..slog import LogSet -# from ..powermon import SimPowerSupply -# except ImportError: -# pytest.skip("Can't import LogSet or SimPowerSupply", allow_module_level=True) +try: + # Depends upon the powermon group, not installed by default + from ..slog import LogSet + from ..powermon import SimPowerSupply +except ImportError: + pytest.skip("Can't import LogSet or SimPowerSupply", allow_module_level=True) # TODO # from ..config import Config From 68a2009e0ee989ef7b8cbb8c4c5360473aac78df Mon Sep 17 00:00:00 2001 From: pdxlocations Date: Tue, 24 Jun 2025 11:47:33 -0700 Subject: [PATCH 8/8] add more tests --- meshtastic/tests/test_main.py | 38 +++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/meshtastic/tests/test_main.py b/meshtastic/tests/test_main.py index e2ca6c13..32447e9b 100644 --- a/meshtastic/tests/test_main.py +++ b/meshtastic/tests/test_main.py @@ -494,6 +494,44 @@ def test_main_get_canned_messages(capsys, caplog, iface_with_nodes): assert err == "" mo.assert_called() +@pytest.mark.unit +@pytest.mark.usefixtures("reset_mt_config") +def test_main_set_ringtone(capsys): + """Test --set-ringtone""" + sys.argv = ["", "--set-ringtone", "foo,bar"] + mt_config.args = sys.argv + + iface = MagicMock(autospec=SerialInterface) + with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: + main() + out, err = capsys.readouterr() + assert re.search(r"Connected to radio", out, re.MULTILINE) + assert re.search(r"Setting ringtone to foo,bar", out, re.MULTILINE) + assert err == "" + mo.assert_called() + +@pytest.mark.unit +@pytest.mark.usefixtures("reset_mt_config") +def test_main_get_ringtone(capsys, caplog, iface_with_nodes): + """Test --get-ringtone""" + sys.argv = ["", "--get-ringtone"] + mt_config.args = sys.argv + + iface = iface_with_nodes + iface.devPath = "bar" + + mocked_node = MagicMock(autospec=Node) + mocked_node.get_ringtone.return_value = "foo,bar" + iface.localNode = mocked_node + + with caplog.at_level(logging.DEBUG): + with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: + main() + out, err = capsys.readouterr() + assert re.search(r"Connected to radio", out, re.MULTILINE) + assert re.search(r"ringtone:foo,bar", out, re.MULTILINE) + assert err == "" + mo.assert_called() @pytest.mark.unit @pytest.mark.usefixtures("reset_mt_config")