From c1cab5e62fcbf1aa539b2c651ded9d508dac9baa Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Fri, 17 Dec 2021 14:37:30 -0800 Subject: [PATCH 1/8] add unit tests for RemoteHardwareClient() --- meshtastic/remote_hardware.py | 9 ++- meshtastic/tests/test_remote_hardware.py | 85 ++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 3 deletions(-) create mode 100644 meshtastic/tests/test_remote_hardware.py diff --git a/meshtastic/remote_hardware.py b/meshtastic/remote_hardware.py index 5fa9f76..58159b0 100644 --- a/meshtastic/remote_hardware.py +++ b/meshtastic/remote_hardware.py @@ -1,5 +1,6 @@ -""" Remote hardware +"""Remote hardware """ +import logging from pubsub import pub from . import portnums_pb2, remote_hardware_pb2 @@ -33,8 +34,7 @@ class RemoteHardwareClient: "to use this (secured) service (--ch-add gpio --info then --seturl)") self.channelIndex = ch.index - pub.subscribe( - onGPIOreceive, "meshtastic.receive.remotehw") + pub.subscribe(onGPIOreceive, "meshtastic.receive.remotehw") def _sendHardware(self, nodeid, r, wantResponse=False, onResponse=None): if not nodeid: @@ -48,6 +48,7 @@ class RemoteHardwareClient: Write the specified vals bits to the device GPIOs. Only bits in mask that are 1 will be changed """ + logging.debug(f'writeGPIOs nodeid:{nodeid} mask:{mask} vals:{vals}') r = remote_hardware_pb2.HardwareMessage() r.typ = remote_hardware_pb2.HardwareMessage.Type.WRITE_GPIOS r.gpio_mask = mask @@ -56,6 +57,7 @@ class RemoteHardwareClient: def readGPIOs(self, nodeid, mask, onResponse = None): """Read the specified bits from GPIO inputs on the device""" + logging.debug(f'readGPIOs nodeid:{nodeid} mask:{mask}') r = remote_hardware_pb2.HardwareMessage() r.typ = remote_hardware_pb2.HardwareMessage.Type.READ_GPIOS r.gpio_mask = mask @@ -63,6 +65,7 @@ class RemoteHardwareClient: def watchGPIOs(self, nodeid, mask): """Watch the specified bits from GPIO inputs on the device for changes""" + logging.debug(f'watchGPIOs nodeid:{nodeid} mask:{mask}') r = remote_hardware_pb2.HardwareMessage() r.typ = remote_hardware_pb2.HardwareMessage.Type.WATCH_GPIOS r.gpio_mask = mask diff --git a/meshtastic/tests/test_remote_hardware.py b/meshtastic/tests/test_remote_hardware.py new file mode 100644 index 0000000..4f738ef --- /dev/null +++ b/meshtastic/tests/test_remote_hardware.py @@ -0,0 +1,85 @@ +"""Meshtastic unit tests for remote_hardware.py""" + +import logging +import re + +from unittest.mock import patch, MagicMock +import pytest + +from ..remote_hardware import RemoteHardwareClient, onGPIOreceive +from ..serial_interface import SerialInterface + + +@pytest.mark.unit +def test_RemoteHardwareClient(): + """Test that we can instantiate a RemoteHardwareClient instance""" + iface = MagicMock(autospec=SerialInterface) + rhw = RemoteHardwareClient(iface) + assert rhw.iface == iface + iface.close() + + +@pytest.mark.unit +def test_onGPIOreceive(capsys): + """Test onGPIOreceive""" + iface = MagicMock(autospec=SerialInterface) + packet = {'decoded': {'remotehw': {'typ': 'foo', 'gpioValue': 'bar' }}} + onGPIOreceive(packet, iface) + out, err = capsys.readouterr() + assert re.search(r'Received RemoteHardware', out) + assert err == '' + + +@pytest.mark.unit +def test_RemoteHardwareClient_no_gpio_channel(): + """Test that we can instantiate a RemoteHardwareClient instance but cannot get channel gpio""" + iface = MagicMock(autospec=SerialInterface) + with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo: + mo.localNode.getChannelByName.return_value = None + with pytest.raises(Exception) as pytest_wrapped_e: + RemoteHardwareClient(mo) + assert pytest_wrapped_e.type == Exception + + +@pytest.mark.unit +def test_readGPIOs(caplog): + """Test readGPIOs""" + iface = MagicMock(autospec=SerialInterface) + rhw = RemoteHardwareClient(iface) + with caplog.at_level(logging.DEBUG): + rhw.readGPIOs('0x10', 123) + assert re.search(r'readGPIOs', caplog.text, re.MULTILINE) + iface.close() + + +@pytest.mark.unit +def test_writeGPIOs(caplog): + """Test writeGPIOs""" + iface = MagicMock(autospec=SerialInterface) + rhw = RemoteHardwareClient(iface) + with caplog.at_level(logging.DEBUG): + rhw.writeGPIOs('0x10', 123, 1) + assert re.search(r'writeGPIOs', caplog.text, re.MULTILINE) + iface.close() + + +@pytest.mark.unit +def test_watchGPIOs(caplog): + """Test watchGPIOs""" + iface = MagicMock(autospec=SerialInterface) + rhw = RemoteHardwareClient(iface) + with caplog.at_level(logging.DEBUG): + rhw.watchGPIOs('0x10', 123) + assert re.search(r'watchGPIOs', caplog.text, re.MULTILINE) + iface.close() + + +@pytest.mark.unit +def test_sendHardware_no_nodeid(): + """Test sending no nodeid to _sendHardware()""" + iface = MagicMock(autospec=SerialInterface) + with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo: + with pytest.raises(Exception) as pytest_wrapped_e: + rhw = RemoteHardwareClient(mo) + rhw._sendHardware(None, None) + assert pytest_wrapped_e.type == Exception From 23d946baaaeb4746fe9351ae8f3778fc390aa00d Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sun, 19 Dec 2021 17:34:39 -0800 Subject: [PATCH 2/8] wip on validating --set-owner on remote node --- meshtastic/__main__.py | 13 +++--- meshtastic/mesh_interface.py | 21 +++++---- meshtastic/node.py | 89 +++++++++++++++++++++++++++--------- 3 files changed, 87 insertions(+), 36 deletions(-) diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index 432e75c..10f4799 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -241,8 +241,7 @@ def onConnected(interface): if args.sendtext: closeNow = True print(f"Sending text message {args.sendtext} to {args.destOrAll}") - interface.sendText(args.sendtext, args.destOrAll, - wantAck=True) + interface.sendText(args.sendtext, args.destOrAll, wantAck=True) if args.sendping: print(f"Sending ping message {args.sendtext} to {args.destOrAll}") @@ -259,8 +258,7 @@ def onConnected(interface): for wrpair in (args.gpio_wrb or []): bitmask |= 1 << int(wrpair[0]) bitval |= int(wrpair[1]) << int(wrpair[0]) - print( - f"Writing GPIO mask 0x{bitmask:x} with value 0x{bitval:x} to {args.dest}") + print(f"Writing GPIO mask 0x{bitmask:x} with value 0x{bitval:x} to {args.dest}") rhc.writeGPIOs(args.dest, bitmask, bitval) closeNow = True @@ -275,6 +273,7 @@ def onConnected(interface): sys.exit(0) # Just force an exit (FIXME - ugly) rhc.readGPIOs(args.dest, bitmask, onResponse) + time.sleep(10) if args.gpio_watch: bitmask = int(args.gpio_watch, 16) @@ -752,13 +751,13 @@ def initParser(): action="store_true") parser.add_argument( - "--gpio-wrb", nargs=2, help="Set a particlar GPIO # to 1 or 0", action='append') + "--gpio-wrb", nargs=2, help="Set a particular GPIO # to 1 or 0", action='append') parser.add_argument( - "--gpio-rd", help="Read from a GPIO mask") + "--gpio-rd", help="Read from a GPIO mask (ex: '0x10')") parser.add_argument( - "--gpio-watch", help="Start watching a GPIO mask for changes") + "--gpio-watch", help="Start watching a GPIO mask for changes (ex: '0x10')") parser.add_argument( "--no-time", help="Suppress sending the current time to the mesh", action="store_true") diff --git a/meshtastic/mesh_interface.py b/meshtastic/mesh_interface.py index e4ba510..e7469d4 100644 --- a/meshtastic/mesh_interface.py +++ b/meshtastic/mesh_interface.py @@ -21,10 +21,6 @@ from . import portnums_pb2, mesh_pb2 from .util import stripnl, Timeout, our_exit from .__init__ import LOCAL_ADDR, BROADCAST_NUM, BROADCAST_ADDR, ResponseHandler, publishingThread, OUR_APP_VERSION, protocols - -defaultHopLimit = 3 - - class MeshInterface: """Interface class for meshtastic devices @@ -56,6 +52,7 @@ class MeshInterface: self.currentPacketId = random.randint(0, 0xffffffff) self.nodesByNum = None self.configId = None + self.defaultHopLimit = 3 def close(self): """Shutdown this interface""" @@ -162,7 +159,7 @@ class MeshInterface: destinationId=BROADCAST_ADDR, wantAck=False, wantResponse=False, - hopLimit=defaultHopLimit, + hopLimit=None, onResponse=None, channelIndex=0): """Send a utf8 string to some other node, if the node has a display it @@ -184,6 +181,9 @@ class MeshInterface: Returns the sent packet. The id field will be populated in this packet and can be used to track future message acks/naks. """ + if hopLimit is None: + hopLimit = self.defaultHopLimit + return self.sendData(text.encode("utf-8"), destinationId, portNum=portnums_pb2.PortNum.TEXT_MESSAGE_APP, wantAck=wantAck, @@ -195,7 +195,7 @@ class MeshInterface: def sendData(self, data, destinationId=BROADCAST_ADDR, portNum=portnums_pb2.PortNum.PRIVATE_APP, wantAck=False, wantResponse=False, - hopLimit=defaultHopLimit, + hopLimit=None, onResponse=None, channelIndex=0): """Send a data packet to some other node @@ -219,6 +219,9 @@ class MeshInterface: Returns the sent packet. The id field will be populated in this packet and can be used to track future message acks/naks. """ + if hopLimit is None: + hopLimit = self.defaultHopLimit + if getattr(data, "SerializeToString", None): logging.debug(f"Serializing protobuf as data: {stripnl(data)}") data = data.SerializeToString() @@ -280,13 +283,15 @@ class MeshInterface: def _sendPacket(self, meshPacket, destinationId=BROADCAST_ADDR, - wantAck=False, hopLimit=defaultHopLimit): + wantAck=False, hopLimit=None): """Send a MeshPacket to the specified node (or if unspecified, broadcast). You probably don't want this - use sendData instead. Returns the sent packet. The id field will be populated in this packet and can be used to track future message acks/naks. """ + if hopLimit is None: + hopLimit = self.defaultHopLimit # We allow users to talk to the local node before we've completed the full connection flow... if(self.myInfo is not None and destinationId != self.myInfo.my_node_num): @@ -365,7 +370,7 @@ class MeshInterface: def _waitConnected(self): """Block until the initial node db download is complete, or timeout and raise an exception""" - if not self.isConnected.wait(10.0): # timeout after 10 seconds + if not self.isConnected.wait(15.0): # timeout after x seconds raise Exception("Timed out waiting for connection completion") # If we failed while connecting, raise the connection to the client diff --git a/meshtastic/node.py b/meshtastic/node.py index 837c06a..e571b2a 100644 --- a/meshtastic/node.py +++ b/meshtastic/node.py @@ -1,6 +1,7 @@ """Node class """ +import re import logging import base64 from google.protobuf.json_format import MessageToJson @@ -20,7 +21,7 @@ class Node: self.nodeNum = nodeNum self.radioConfig = None self.channels = None - self._timeout = Timeout(maxSecs=60) + self._timeout = Timeout(maxSecs=300) self.partialChannels = None self.noProto = noProto @@ -49,6 +50,7 @@ class Node: def requestConfig(self): """Send regular MeshPackets to ask for settings and channels.""" + logging.debug(f"requestConfig for nodeNum:{self.nodeNum}") self.radioConfig = None self.channels = None self.partialChannels = [] # We keep our channels in a temp array until finished @@ -134,6 +136,7 @@ class Node: def setOwner(self, long_name=None, short_name=None, is_licensed=False, team=None): """Set device owner name""" + logging.debug(f"in setOwner nodeNum:{self.nodeNum}") nChars = 3 minChars = 2 if long_name is not None: @@ -227,19 +230,39 @@ class Node: def onResponse(p): """A closure to handle the response packet""" errorFound = False - if 'routing' in p["decoded"]: - if p["decoded"]["routing"]["errorReason"] != "NONE": - errorFound = True - print(f'Error on response: {p["decoded"]["routing"]["errorReason"]}') + if 'routing' in p['decoded']: + if 'errorReason' in p['decoded']: + if p['decoded']['routing']['errorReason'] != 'NONE': + errorFound = True + print(f'Error on response: {p["decoded"]["routing"]["errorReason"]}') if errorFound is False: - self.radioConfig = p["decoded"]["admin"]["raw"].get_radio_response - logging.debug("Received radio config, now fetching channels...") - self._timeout.reset() # We made foreward progress - self._requestChannel(0) # now start fetching channels + logging.debug(f'p:{p}') + if "decoded" in p: + if "admin" in p["decoded"]: + if "raw" in p["decoded"]["admin"]: + self.radioConfig = p["decoded"]["admin"]["raw"].get_radio_response + logging.debug("Received radio config, now fetching channels...") + self._timeout.reset() # We made foreward progress + self._requestChannel(0) # now start fetching channels + else: + print("Warning: There was no 'raw' in packet in onResponse() in _requestSettings()") + print(f"p:{p}") + #else: + #print("Warning: There was no 'admin' in packet in onResponse() in _requestSettings()") + #print(f"p:{p}") + else: + print("Warning: There was no 'decoded' in packet in onResponse() in _requestSettings()") + print(f"p:{p}") # Show progress message for super slow operations if self != self.iface.localNode: - print("Requesting preferences from remote node (this could take a while)") + print("Requesting preferences from remote node.") + print("Be sure:") + print(" 1. There is a SECONDARY channel named 'admin'.") + print(" 2. The '--seturl' was used to configure.") + print(" 3. All devices have the same modem config. (i.e., '--ch-longfast')") + print(" 4. All devices have been rebooted after all of the above. (optional, but recommended)") + print("Note: This could take a while (it requests remote channel configs, then writes config)") return self._sendAdmin(p, wantResponse=True, onResponse=onResponse) @@ -290,25 +313,49 @@ class Node: # Show progress message for super slow operations if self != self.iface.localNode: - logging.info(f"Requesting channel {channelNum} info from remote node (this could take a while)") + print(f"Requesting channel {channelNum} info from remote node (this could take a while)") + logging.debug(f"Requesting channel {channelNum} info from remote node (this could take a while)") else: logging.debug(f"Requesting channel {channelNum}") def onResponse(p): """A closure to handle the response packet for requesting a channel""" - c = p["decoded"]["admin"]["raw"].get_channel_response - self.partialChannels.append(c) - self._timeout.reset() # We made foreward progress - logging.debug(f"Received channel {stripnl(c)}") - index = c.index - # for stress testing, we can always download all channels - fastChannelDownload = True + quitEarly = False - # Once we see a response that has NO settings, assume - # we are at the end of channels and stop fetching - quitEarly = (c.role == channel_pb2.Channel.Role.DISABLED) and fastChannelDownload + index = len(self.partialChannels) + logging.debug(f'index:{index}') + logging.debug(f' p:{p}') + if 'decoded' in p: + if 'admin' in p['decoded']: + if 'raw' in p['decoded']['admin']: + c = p["decoded"]["admin"]["raw"].get_channel_response + # Once we see a response that has NO settings, assume + # we are at the end of channels and stop fetching + tmp = f"{stripnl(c)}" + logging.debug(f'tmp:{tmp}:') + if re.search(r'settings { }', tmp): + quitEarly = True + logging.debug(f'Set quitEarly') + else: + self.partialChannels.append(c) + logging.debug(f"Received channel {stripnl(c)}") + #index = c.index + self._timeout.reset() # We made foreward progress + else: + print("Warning: There was no 'raw' in packet to onResponse() in _requestChannel.") + print(f"p:{p}") + else: + #print("Warning: There was no 'admin' in packet to onResponse() in _requestChannel.") + #print(f"p:{p}") + pass + else: + print("Warning: There was no 'decoded' in packet to onResponse() in _requestChannel.") + print(f"p:{p}") + + # TODO: is myInfo.max_channels the same as the remote's max channels? + logging.debug(f'index:{index} self.iface.myInfo.max_channels:{self.iface.myInfo.max_channels}') if quitEarly or index >= self.iface.myInfo.max_channels - 1: logging.debug("Finished downloading channels") From 4597ea18983bfeb89f0738645b401c156746c44f Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Mon, 20 Dec 2021 14:00:56 -0800 Subject: [PATCH 3/8] add channel index to send text --- meshtastic/__main__.py | 7 +++++-- meshtastic/tests/test_main.py | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index 10f4799..6345065 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -240,11 +240,14 @@ def onConnected(interface): if args.sendtext: closeNow = True + channelIndex = 0 + if args.ch_index is not None: + channelIndex = int(args.ch_index) print(f"Sending text message {args.sendtext} to {args.destOrAll}") - interface.sendText(args.sendtext, args.destOrAll, wantAck=True) + interface.sendText(args.sendtext, args.destOrAll, wantAck=True, channelIndex=channelIndex) if args.sendping: - print(f"Sending ping message {args.sendtext} to {args.destOrAll}") + print(f"Sending ping message {args.sendping} to {args.destOrAll}") payload = str.encode("test string") interface.sendData(payload, args.destOrAll, portNum=portnums_pb2.PortNum.REPLY_APP, wantAck=True, wantResponse=True) diff --git a/meshtastic/tests/test_main.py b/meshtastic/tests/test_main.py index edb5848..f8a6734 100644 --- a/meshtastic/tests/test_main.py +++ b/meshtastic/tests/test_main.py @@ -404,7 +404,7 @@ def test_main_sendtext(capsys, reset_globals): Globals.getInstance().set_args(sys.argv) iface = MagicMock(autospec=SerialInterface) - def mock_sendText(text, dest, wantAck): + def mock_sendText(text, dest, wantAck, channelIndex): print('inside mocked sendText') iface.sendText.side_effect = mock_sendText @@ -425,7 +425,7 @@ def test_main_sendtext_with_dest(capsys, reset_globals): Globals.getInstance().set_args(sys.argv) iface = MagicMock(autospec=SerialInterface) - def mock_sendText(text, dest, wantAck): + def mock_sendText(text, dest, wantAck, channelIndex): print('inside mocked sendText') iface.sendText.side_effect = mock_sendText From 29d372309449aec924549a1029d12db1faead368 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Mon, 20 Dec 2021 21:21:20 -0800 Subject: [PATCH 4/8] not sure what happened to example_config.yaml file --- meshtastic/example_config.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 meshtastic/example_config.yaml diff --git a/meshtastic/example_config.yaml b/meshtastic/example_config.yaml new file mode 100644 index 0000000..231b465 --- /dev/null +++ b/meshtastic/example_config.yaml @@ -0,0 +1,15 @@ +owner: Bob TBeam + +channel_url: https://www.meshtastic.org/d/#CgUYAyIBAQ + +location: + lat: 35.88888 + lon: -93.88888 + alt: 304 + +user_prefs: + region: 1 + is_always_powered: 'true' + send_owner_interval: 2 + screen_on_secs: 31536000 + wait_bluetooth_secs: 31536000 From 1c8a5d8c5bdfee9bf6438c6c1d48a8f3c74098d8 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Mon, 20 Dec 2021 22:11:52 -0800 Subject: [PATCH 5/8] work around for issue 172 --- meshtastic/example_config.yaml | 15 ----------- meshtastic/mesh_interface.py | 5 ++-- meshtastic/tests/test_mesh_interface.py | 33 +++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 17 deletions(-) delete mode 100644 meshtastic/example_config.yaml diff --git a/meshtastic/example_config.yaml b/meshtastic/example_config.yaml deleted file mode 100644 index 231b465..0000000 --- a/meshtastic/example_config.yaml +++ /dev/null @@ -1,15 +0,0 @@ -owner: Bob TBeam - -channel_url: https://www.meshtastic.org/d/#CgUYAyIBAQ - -location: - lat: 35.88888 - lon: -93.88888 - alt: 304 - -user_prefs: - region: 1 - is_always_powered: 'true' - send_owner_interval: 2 - screen_on_secs: 31536000 - wait_bluetooth_secs: 31536000 diff --git a/meshtastic/mesh_interface.py b/meshtastic/mesh_interface.py index e7469d4..fee9166 100644 --- a/meshtastic/mesh_interface.py +++ b/meshtastic/mesh_interface.py @@ -461,7 +461,7 @@ class MeshInterface: Called by subclasses.""" fromRadio = mesh_pb2.FromRadio() fromRadio.ParseFromString(fromRadioBytes) - #logging.debug(f"fromRadioBytes: {fromRadioBytes}") + logging.debug(f"in mesh_interface.py _handleFromRadio() fromRadioBytes: {fromRadioBytes}") asDict = google.protobuf.json_format.MessageToDict(fromRadio) logging.debug(f"Received from radio: {fromRadio}") if fromRadio.HasField("my_info"): @@ -496,7 +496,8 @@ class MeshInterface: self.nodesByNum[node["num"]] = node if "user" in node: # Some nodes might not have user/ids assigned yet - self.nodes[node["user"]["id"]] = node + if "id" in node["user"]: + self.nodes[node["user"]["id"]] = node publishingThread.queueWork(lambda: pub.sendMessage("meshtastic.node.updated", node=node, interface=self)) elif fromRadio.config_complete_id == self.configId: diff --git a/meshtastic/tests/test_mesh_interface.py b/meshtastic/tests/test_mesh_interface.py index 27c2f4e..78225be 100644 --- a/meshtastic/tests/test_mesh_interface.py +++ b/meshtastic/tests/test_mesh_interface.py @@ -182,3 +182,36 @@ def test_handleFromRadio_with_node_info(reset_globals, caplog, capsys): assert re.search(r'│ !28af67cc │ N/A │ N/A │ N/A', out, re.MULTILINE) assert err == '' iface.close() + + +@pytest.mark.unit +def test_handleFromRadio_with_node_info_tbeam1(reset_globals, caplog, capsys): + """Test _handleFromRadio with node_info""" + # Note: Captured the '--debug --info' for the bytes below. + from_radio_bytes = b'"=\x08\x80\xf8\xc8\xf6\x07\x12"\n\t!7ed23c00\x12\x07TBeam 1\x1a\x02T1"\x06\x94\xb9~\xd2<\x000\x04\x1a\x07 ]MN\x01\xbea%\xad\x01\xbea=\x00\x00,A' + iface = MeshInterface(noProto=True) + with caplog.at_level(logging.DEBUG): + iface._startConfig() + iface._handleFromRadio(from_radio_bytes) + assert re.search(r'Received nodeinfo', caplog.text, re.MULTILINE) + assert re.search(r'TBeam 1', caplog.text, re.MULTILINE) + assert re.search(r'2127707136', caplog.text, re.MULTILINE) + # validate some of showNodes() output + iface.showNodes() + out, err = capsys.readouterr() + assert re.search(r' 1 ', out, re.MULTILINE) + assert re.search(r'│ TBeam 1 │ ', out, re.MULTILINE) + assert re.search(r'│ !7ed23c00 │', out, re.MULTILINE) + assert err == '' + iface.close() + + +@pytest.mark.unit +def test_handleFromRadio_with_node_info_tbeam_with_bad_data(reset_globals, caplog, capsys): + """Test _handleFromRadio with node_info with some bad data (issue#172) - ensure we do not throw exception""" + # Note: Captured the '--debug --info' for the bytes below. + from_radio_bytes = b'"\x17\x08\xdc\x8a\x8a\xae\x02\x12\x08"\x06\x00\x00\x00\x00\x00\x00\x1a\x00=\x00\x00\xb8@' + iface = MeshInterface(noProto=True) + with caplog.at_level(logging.DEBUG): + iface._startConfig() + iface._handleFromRadio(from_radio_bytes) From 036ce8258fea45b8d43fd6f4f7f8d27c3069d32d Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Mon, 20 Dec 2021 22:25:19 -0800 Subject: [PATCH 6/8] ignore long binary line --- meshtastic/tests/test_mesh_interface.py | 1 + 1 file changed, 1 insertion(+) diff --git a/meshtastic/tests/test_mesh_interface.py b/meshtastic/tests/test_mesh_interface.py index 78225be..9429911 100644 --- a/meshtastic/tests/test_mesh_interface.py +++ b/meshtastic/tests/test_mesh_interface.py @@ -188,6 +188,7 @@ def test_handleFromRadio_with_node_info(reset_globals, caplog, capsys): def test_handleFromRadio_with_node_info_tbeam1(reset_globals, caplog, capsys): """Test _handleFromRadio with node_info""" # Note: Captured the '--debug --info' for the bytes below. + # pylint: disable=C0301 from_radio_bytes = b'"=\x08\x80\xf8\xc8\xf6\x07\x12"\n\t!7ed23c00\x12\x07TBeam 1\x1a\x02T1"\x06\x94\xb9~\xd2<\x000\x04\x1a\x07 ]MN\x01\xbea%\xad\x01\xbea=\x00\x00,A' iface = MeshInterface(noProto=True) with caplog.at_level(logging.DEBUG): From a35f3a4e7bf87fde75428f7b2ee5bed977f41d88 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Mon, 20 Dec 2021 23:19:38 -0800 Subject: [PATCH 7/8] revert changes to onResponse() methods --- meshtastic/node.py | 76 ++++++++++++---------------------------------- 1 file changed, 20 insertions(+), 56 deletions(-) diff --git a/meshtastic/node.py b/meshtastic/node.py index e571b2a..4b682b3 100644 --- a/meshtastic/node.py +++ b/meshtastic/node.py @@ -1,7 +1,6 @@ """Node class """ -import re import logging import base64 from google.protobuf.json_format import MessageToJson @@ -230,29 +229,15 @@ class Node: def onResponse(p): """A closure to handle the response packet""" errorFound = False - if 'routing' in p['decoded']: - if 'errorReason' in p['decoded']: - if p['decoded']['routing']['errorReason'] != 'NONE': - errorFound = True - print(f'Error on response: {p["decoded"]["routing"]["errorReason"]}') + if 'routing' in p["decoded"]: + if p["decoded"]["routing"]["errorReason"] != "NONE": + errorFound = True + print(f'Error on response: {p["decoded"]["routing"]["errorReason"]}') if errorFound is False: - logging.debug(f'p:{p}') - if "decoded" in p: - if "admin" in p["decoded"]: - if "raw" in p["decoded"]["admin"]: - self.radioConfig = p["decoded"]["admin"]["raw"].get_radio_response - logging.debug("Received radio config, now fetching channels...") - self._timeout.reset() # We made foreward progress - self._requestChannel(0) # now start fetching channels - else: - print("Warning: There was no 'raw' in packet in onResponse() in _requestSettings()") - print(f"p:{p}") - #else: - #print("Warning: There was no 'admin' in packet in onResponse() in _requestSettings()") - #print(f"p:{p}") - else: - print("Warning: There was no 'decoded' in packet in onResponse() in _requestSettings()") - print(f"p:{p}") + self.radioConfig = p["decoded"]["admin"]["raw"].get_radio_response + logging.debug("Received radio config, now fetching channels...") + self._timeout.reset() # We made foreward progress + self._requestChannel(0) # now start fetching channels # Show progress message for super slow operations if self != self.iface.localNode: @@ -320,42 +305,19 @@ class Node: def onResponse(p): """A closure to handle the response packet for requesting a channel""" + c = p["decoded"]["admin"]["raw"].get_channel_response + self.partialChannels.append(c) + self._timeout.reset() # We made foreward progress + logging.debug(f"Received channel {stripnl(c)}") + index = c.index - quitEarly = False + # for stress testing, we can always download all channels + fastChannelDownload = True - index = len(self.partialChannels) - logging.debug(f'index:{index}') - logging.debug(f' p:{p}') + # Once we see a response that has NO settings, assume + # we are at the end of channels and stop fetching + quitEarly = (c.role == channel_pb2.Channel.Role.DISABLED) and fastChannelDownload - if 'decoded' in p: - if 'admin' in p['decoded']: - if 'raw' in p['decoded']['admin']: - c = p["decoded"]["admin"]["raw"].get_channel_response - # Once we see a response that has NO settings, assume - # we are at the end of channels and stop fetching - tmp = f"{stripnl(c)}" - logging.debug(f'tmp:{tmp}:') - if re.search(r'settings { }', tmp): - quitEarly = True - logging.debug(f'Set quitEarly') - else: - self.partialChannels.append(c) - logging.debug(f"Received channel {stripnl(c)}") - #index = c.index - self._timeout.reset() # We made foreward progress - else: - print("Warning: There was no 'raw' in packet to onResponse() in _requestChannel.") - print(f"p:{p}") - else: - #print("Warning: There was no 'admin' in packet to onResponse() in _requestChannel.") - #print(f"p:{p}") - pass - else: - print("Warning: There was no 'decoded' in packet to onResponse() in _requestChannel.") - print(f"p:{p}") - - # TODO: is myInfo.max_channels the same as the remote's max channels? - logging.debug(f'index:{index} self.iface.myInfo.max_channels:{self.iface.myInfo.max_channels}') if quitEarly or index >= self.iface.myInfo.max_channels - 1: logging.debug("Finished downloading channels") @@ -369,6 +331,7 @@ class Node: return self._sendAdmin(p, wantResponse=True, onResponse=onResponse) + # pylint: disable=R1710 def _sendAdmin(self, p: admin_pb2.AdminMessage, wantResponse=False, onResponse=None, adminIndex=0): @@ -379,6 +342,7 @@ class Node: else: if adminIndex == 0: # unless a special channel index was used, we want to use the admin index adminIndex = self.iface.localNode._getAdminChannelIndex() + logging.debug(f'adminIndex:{adminIndex}') return self.iface.sendData(p, self.nodeNum, portNum=portnums_pb2.PortNum.ADMIN_APP, From d82f197c06ba8f0035ab82104dd7abc0ec70c3d4 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Mon, 20 Dec 2021 23:23:27 -0800 Subject: [PATCH 8/8] remove the message from the sendping option --- meshtastic/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index 6345065..6fe4da6 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -247,8 +247,8 @@ def onConnected(interface): interface.sendText(args.sendtext, args.destOrAll, wantAck=True, channelIndex=channelIndex) if args.sendping: - print(f"Sending ping message {args.sendping} to {args.destOrAll}") payload = str.encode("test string") + print(f"Sending ping message to {args.destOrAll}") interface.sendData(payload, args.destOrAll, portNum=portnums_pb2.PortNum.REPLY_APP, wantAck=True, wantResponse=True)