From 9512f91198f862dff0e479c77f8ff550dddaa5ae Mon Sep 17 00:00:00 2001 From: Paul Picazo Date: Tue, 25 Feb 2025 22:02:28 -0800 Subject: [PATCH] Add hoplimit argument to override default hopLimit in config Add command line argument for hopLimit to override the default one set in the config for `--sendtext`, `--traceroute`, `--request-telemetry`, and `--request-position`. --- meshtastic/__main__.py | 14 +- meshtastic/mesh_interface.py | 10 +- meshtastic/tests/test_main.py | 243 +++++++++++++++++++++--- meshtastic/tests/test_mesh_interface.py | 22 +++ 4 files changed, 263 insertions(+), 26 deletions(-) diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index e5a92435..0c9ec462 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -491,7 +491,8 @@ def onConnected(interface): wantAck=True, channelIndex=channelIndex, onResponse=interface.getNode(args.dest, False, **getNode_kwargs).onAckNak, - portNum=portnums_pb2.PortNum.PRIVATE_APP if args.private else portnums_pb2.PortNum.TEXT_MESSAGE_APP + portNum=portnums_pb2.PortNum.PRIVATE_APP if args.private else portnums_pb2.PortNum.TEXT_MESSAGE_APP, + hopLimit=args.hoplimit ) else: meshtastic.util.our_exit( @@ -500,7 +501,7 @@ def onConnected(interface): if args.traceroute: loraConfig = getattr(interface.localNode.localConfig, "lora") - hopLimit = getattr(loraConfig, "hop_limit") + hopLimit = args.hoplimit or getattr(loraConfig, "hop_limit") dest = str(args.traceroute) channelIndex = mt_config.channel_index or 0 if checkChannel(interface, channelIndex): @@ -533,6 +534,7 @@ def onConnected(interface): wantResponse=True, channelIndex=channelIndex, telemetryType=telemType, + hopLimit=args.hoplimit ) if args.request_position: @@ -548,6 +550,7 @@ def onConnected(interface): destinationId=args.dest, wantResponse=True, channelIndex=channelIndex, + hopLimit=args.hoplimit ) if args.gpio_wrb or args.gpio_rd or args.gpio_watch: @@ -1744,6 +1747,13 @@ def addRemoteActionArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentPar "--reply", help="Reply to received messages", action="store_true" ) + group.add_argument( + "--hoplimit", + help="Specify the hop limit for the message. Overrides the default hop limit set in the config.", + type=int, + default=None, + ) + return parser def addRemoteAdminArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentParser: diff --git a/meshtastic/mesh_interface.py b/meshtastic/mesh_interface.py index a3a67b79..3850cf83 100644 --- a/meshtastic/mesh_interface.py +++ b/meshtastic/mesh_interface.py @@ -411,7 +411,8 @@ def sendText( wantResponse: bool = False, onResponse: Optional[Callable[[dict], Any]] = None, channelIndex: int = 0, - portNum: portnums_pb2.PortNum.ValueType = portnums_pb2.PortNum.TEXT_MESSAGE_APP + portNum: portnums_pb2.PortNum.ValueType = portnums_pb2.PortNum.TEXT_MESSAGE_APP, + hopLimit: Optional[int] = None ): """Send a utf8 string to some other node, if the node has a display it will also be shown on the device. @@ -441,6 +442,7 @@ def sendText( wantResponse=wantResponse, onResponse=onResponse, channelIndex=channelIndex, + hopLimit=hopLimit ) @@ -560,6 +562,7 @@ def sendPosition( wantAck: bool = False, wantResponse: bool = False, channelIndex: int = 0, + hopLimit: Optional[int] = None ): """ Send a position packet to some other node (normally a broadcast) @@ -596,6 +599,7 @@ def sendPosition( wantResponse=wantResponse, onResponse=onResponse, channelIndex=channelIndex, + hopLimit=hopLimit ) if wantResponse: self.waitForPosition() @@ -701,7 +705,8 @@ def sendTelemetry( destinationId: Union[int, str] = BROADCAST_ADDR, wantResponse: bool = False, channelIndex: int = 0, - telemetryType: str = "device_metrics" + telemetryType: str = "device_metrics", + hopLimit: Optional[int] = None ): """Send telemetry and optionally ask for a response""" r = telemetry_pb2.Telemetry() @@ -748,6 +753,7 @@ def sendTelemetry( wantResponse=wantResponse, onResponse=onResponse, channelIndex=channelIndex, + hopLimit=hopLimit ) if wantResponse: self.waitForTelemetry() diff --git a/meshtastic/tests/test_main.py b/meshtastic/tests/test_main.py index 8b70a4e8..a7d158d4 100644 --- a/meshtastic/tests/test_main.py +++ b/meshtastic/tests/test_main.py @@ -593,10 +593,10 @@ def test_main_sendtext(capsys): iface = MagicMock(autospec=SerialInterface) def mock_sendText( - text, dest, wantAck=False, wantResponse=False, onResponse=None, channelIndex=0, portNum=0 + text, dest, wantAck=False, wantResponse=False, onResponse=None, channelIndex=0, portNum=0, hopLimit=None ): print("inside mocked sendText") - print(f"{text} {dest} {wantAck} {wantResponse} {channelIndex} {portNum}") + print(f"{text} {dest} {wantAck} {wantResponse} {channelIndex} {portNum} {hopLimit}") iface.sendText.side_effect = mock_sendText @@ -620,10 +620,10 @@ def test_main_sendtext_with_channel(capsys): iface = MagicMock(autospec=SerialInterface) def mock_sendText( - text, dest, wantAck=False, wantResponse=False, onResponse=None, channelIndex=0, portNum=0 + text, dest, wantAck=False, wantResponse=False, onResponse=None, channelIndex=0, portNum=0, hopLimit=None ): print("inside mocked sendText") - print(f"{text} {dest} {wantAck} {wantResponse} {channelIndex} {portNum}") + print(f"{text} {dest} {wantAck} {wantResponse} {channelIndex} {portNum} {hopLimit}") iface.sendText.side_effect = mock_sendText @@ -1360,26 +1360,27 @@ def test_main_ch_enable_primary_channel(capsys): # TODO # @pytest.mark.unit # @pytest.mark.usefixtures("reset_mt_config") -# def test_main_ch_range_options(capsys): -# """Test changing the various range options.""" -# range_options = ['--ch-vlongslow', '--ch-longslow', '--ch-longfast', '--ch-midslow', -# '--ch-midfast', '--ch-shortslow', '--ch-shortfast'] -# for range_option in range_options: -# sys.argv = ['', f"{range_option}" ] -# mt_config.args = sys.argv +# @pytest.mark.parametrize("range_option", [ +# '--ch-vlongslow', '--ch-longslow', '--ch-longfast', '--ch-midslow', +# '--ch-midfast', '--ch-shortslow', '--ch-shortfast' +# ]) +# def test_main_ch_range_options(range_option, capsys): +# """Test changing the various range options.""" +# sys.argv = ['', f"{range_option}"] +# mt_config.args = sys.argv # -# mocked_node = MagicMock(autospec=Node) +# mocked_node = MagicMock(autospec=Node) # -# iface = MagicMock(autospec=SerialInterface) -# iface.getNode.return_value = mocked_node +# iface = MagicMock(autospec=SerialInterface) +# iface.getNode.return_value = mocked_node # -# 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'Writing modified channels', out, re.MULTILINE) -# assert err == '' -# mo.assert_called() +# 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'Writing modified channels', out, re.MULTILINE) +# assert err == '' +# mo.assert_called() @pytest.mark.unit @@ -1828,7 +1829,7 @@ def test_main_export_config(capsys): @pytest.mark.unit @pytest.mark.usefixtures("reset_mt_config") def test_main_gpio_rd_no_gpio_channel(capsys): - """Test --gpio_rd with no named gpio channel""" + """Test --gpio-rd with no named gpio channel""" sys.argv = ["", "--gpio-rd", "0x10", "--dest", "!foo"] mt_config.args = sys.argv @@ -2713,3 +2714,201 @@ 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_main_traceroute_with_hoplimit(capsys): + """Test --traceroute with --hoplimit""" + sys.argv = ["", "--traceroute", "!12345678", "--hoplimit", "5"] + mt_config.args = sys.argv + + iface = MagicMock(autospec=SerialInterface) + + def mock_sendTraceRoute(dest, hopLimit, channelIndex=0): + print("inside mocked sendTraceRoute") + print(f"{dest} {hopLimit} {channelIndex}") + + iface.sendTraceRoute.side_effect = mock_sendTraceRoute + + 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"inside mocked sendTraceRoute", out, re.MULTILINE) + assert re.search(r"!12345678 5 0", out, re.MULTILINE) + assert err == "" + mo.assert_called() + +@pytest.mark.unit +@pytest.mark.usefixtures("reset_mt_config") +def test_main_request_telemetry_with_hoplimit(capsys): + """Test --request-telemetry with --hoplimit""" + sys.argv = ["", "--request-telemetry", "--hoplimit", "5"] + mt_config.args = sys.argv + + iface = MagicMock(autospec=SerialInterface) + + def mock_sendTelemetry(destinationId, wantResponse=False, channelIndex=0, telemetryType="device_metrics", hopLimit=None): + print("inside mocked sendTelemetry") + print(f"{destinationId} {wantResponse} {channelIndex} {telemetryType} {hopLimit}") + + iface.sendTelemetry.side_effect = mock_sendTelemetry + + 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"inside mocked sendTelemetry", out, re.MULTILINE) + assert re.search(r"4294967295 False 0 device_metrics 5", out, re.MULTILINE) + assert err == "" + mo.assert_called() + +@pytest.mark.unit +@pytest.mark.usefixtures("reset_mt_config") +def test_main_request_position_with_hoplimit(capsys): + """Test --request-position with --hoplimit""" + sys.argv = ["", "--request-position", "--hoplimit", "5"] + mt_config.args = sys.argv + + iface = MagicMock(autospec=SerialInterface) + + def mock_sendPosition(latitude=0.0, longitude=0.0, altitude=0, destinationId=4294967295, wantAck=False, wantResponse=False, channelIndex=0, hopLimit=None): + print("inside mocked sendPosition") + print(f"{latitude} {longitude} {altitude} {destinationId} {wantAck} {wantResponse} {channelIndex} {hopLimit}") + + iface.sendPosition.side_effect = mock_sendPosition + + 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"inside mocked sendPosition", out, re.MULTILINE) + assert re.search(r"0.0 0.0 0 4294967295 False True 0 5", out, re.MULTILINE) + assert err == "" + mo.assert_called() + +@pytest.mark.unit +@pytest.mark.usefixtures("reset_mt_config") +def test_main_sendtext_with_hoplimit(capsys): + """Test --sendtext with --hoplimit""" + sys.argv = ["", "--sendtext", "hello", "--hoplimit", "5"] + mt_config.args = sys.argv + + iface = MagicMock(autospec=SerialInterface) + + def mock_sendText( + text, dest, wantAck=False, wantResponse=False, onResponse=None, channelIndex=0, portNum=0, hopLimit=None + ): + print("inside mocked sendText") + print(f"{text} {dest} {wantAck} {wantResponse} {channelIndex} {portNum} {hopLimit}") + + iface.sendText.side_effect = mock_sendText + + 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"Sending text message", out, re.MULTILINE) + assert re.search(r"inside mocked sendText", out, re.MULTILINE) + assert re.search(r"hello 4294967295 False False 0 0 5", out, re.MULTILINE) + assert err == "" + mo.assert_called() + +@pytest.mark.unit +@pytest.mark.usefixtures("reset_mt_config") +def test_main_sendtext_without_hoplimit(capsys): + """Test --sendtext without --hoplimit""" + sys.argv = ["", "--sendtext", "hello"] + mt_config.args = sys.argv + + iface = MagicMock(autospec=SerialInterface) + + def mock_sendText( + text, dest, wantAck=False, wantResponse=False, onResponse=None, channelIndex=0, portNum=0, hopLimit=None + ): + print("inside mocked sendText") + print(f"{text} {dest} {wantAck} {wantResponse} {channelIndex} {portNum} {hopLimit}") + + iface.sendText.side_effect = mock_sendText + + 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"Sending text message", out, re.MULTILINE) + assert re.search(r"inside mocked sendText", out, re.MULTILINE) + assert re.search(r"hello 4294967295 False False 0 0 None", out, re.MULTILINE) + assert err == "" + mo.assert_called() + +@pytest.mark.unit +@pytest.mark.usefixtures("reset_mt_config") +def test_main_traceroute_without_hoplimit(capsys): + """Test --traceroute without --hoplimit""" + sys.argv = ["", "--traceroute", "!12345678"] + mt_config.args = sys.argv + + iface = MagicMock(autospec=SerialInterface) + + def mock_sendTraceRoute(dest, hopLimit, channelIndex=0): + print("inside mocked sendTraceRoute") + print(f"{dest} {hopLimit} {channelIndex}") + + iface.sendTraceRoute.side_effect = mock_sendTraceRoute + + 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"inside mocked sendTraceRoute", out, re.MULTILINE) + assert re.search(r"!12345678 None 0", out, re.MULTILINE) + assert err == "" + mo.assert_called() + +@pytest.mark.unit +@pytest.mark.usefixtures("reset_mt_config") +def test_main_request_telemetry_without_hoplimit(capsys): + """Test --request-telemetry without --hoplimit""" + sys.argv = ["", "--request-telemetry"] + mt_config.args = sys.argv + + iface = MagicMock(autospec=SerialInterface) + + def mock_sendTelemetry(destinationId, wantResponse=False, channelIndex=0, telemetryType="device_metrics", hopLimit=None): + print("inside mocked sendTelemetry") + print(f"{destinationId} {wantResponse} {channelIndex} {telemetryType} {hopLimit}") + + iface.sendTelemetry.side_effect = mock_sendTelemetry + + 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"inside mocked sendTelemetry", out, re.MULTILINE) + assert re.search(r"4294967295 False 0 device_metrics None", out, re.MULTILINE) + assert err == "" + mo.assert_called() + +@pytest.mark.unit +@pytest.mark.usefixtures("reset_mt_config") +def test_main_request_position_without_hoplimit(capsys): + """Test --request-position without --hoplimit""" + sys.argv = ["", "--request-position"] + mt_config.args = sys.argv + + iface = MagicMock(autospec=SerialInterface) + + def mock_sendPosition(latitude=0.0, longitude=0.0, altitude=0, destinationId=4294967295, wantAck=False, wantResponse=False, channelIndex=0, hopLimit=None): + print("inside mocked sendPosition") + print(f"{latitude} {longitude} {altitude} {destinationId} {wantAck} {wantResponse} {channelIndex} {hopLimit}") + + iface.sendPosition.side_effect = mock_sendPosition + + 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"inside mocked sendPosition", out, re.MULTILINE) + assert re.search(r"0.0 0.0 0 4294967295 False True 0 None", out, re.MULTILINE) + assert err == "" + mo.assert_called() diff --git a/meshtastic/tests/test_mesh_interface.py b/meshtastic/tests/test_mesh_interface.py index bacf85cf..d0a8ed0d 100644 --- a/meshtastic/tests/test_mesh_interface.py +++ b/meshtastic/tests/test_mesh_interface.py @@ -726,3 +726,25 @@ 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_sendText_with_hopLimit(caplog): + """Test sendText with hopLimit""" + iface = MeshInterface(noProto=True) + with caplog.at_level(logging.DEBUG): + iface.sendText("hello", hopLimit=3) + assert re.search(r"Sending packet", caplog.text, re.MULTILINE) + assert re.search(r"hop_limit: 3", caplog.text, re.MULTILINE) + + +@pytest.mark.unit +@pytest.mark.usefixtures("reset_mt_config") +def test_sendTelemetry_with_hopLimit(caplog): + """Test sendTelemetry with hopLimit""" + iface = MeshInterface(noProto=True) + with caplog.at_level(logging.DEBUG): + iface.sendTelemetry(hopLimit=3) + assert re.search(r"Sending packet", caplog.text, re.MULTILINE) + assert re.search(r"hop_limit: 3", caplog.text, re.MULTILINE)