From f68e4112e117b7034fdcbdd98168699da228a7e7 Mon Sep 17 00:00:00 2001 From: GUVWAF Date: Sat, 18 Mar 2023 15:20:00 +0100 Subject: [PATCH 1/5] Remote get method works --- meshtastic/__main__.py | 90 ++++++++++++++++++++---------------- meshtastic/mesh_interface.py | 12 ++--- meshtastic/node.py | 38 +++++++++++++-- 3 files changed, 91 insertions(+), 49 deletions(-) diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index 5f68444..5e6c510 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -53,10 +53,11 @@ def onConnection(interface, topic=pub.AUTO_TOPIC): # pylint: disable=W0613 print(f"Connection changed: {topic.getName()}") -def getPref(config, comp_name): +def getPref(interface, dest, comp_name): """Get a channel or preferences value""" name = splitCompoundName(comp_name) + fullConfig = name[0] == name[1] # We want the full config camel_name = meshtastic.util.snake_to_camel(name[1]) # Note: protobufs has the keys in snake_case, so snake internally @@ -64,26 +65,47 @@ def getPref(config, comp_name): logging.debug(f'snake_name:{snake_name} camel_name:{camel_name}') logging.debug(f'use camel:{Globals.getInstance().get_camel_case()}') - objDesc = config.DESCRIPTOR - print() - config_type = objDesc.fields_by_name.get(name[0]) - pref = False - if config_type: - pref = config_type.message_type.fields_by_name.get(snake_name) + localConfig = interface.getNode(BROADCAST_ADDR).localConfig + moduleConfig = interface.getNode(BROADCAST_ADDR).moduleConfig + found = False + for config in [localConfig, moduleConfig]: + objDesc = config.DESCRIPTOR + config_type = objDesc.fields_by_name.get(name[0]) + pref = False + if config_type: + pref = config_type.message_type.fields_by_name.get(snake_name) + if pref or fullConfig: + found = True + break - if (not pref) or (not config_type): + if not found: + if Globals.getInstance().get_camel_case(): + print(f"{localConfig.__class__.__name__} and {moduleConfig.__class__.__name__} do not have an attribute {snake_name}.") + else: + print(f"{localConfig.__class__.__name__} and {moduleConfig.__class__.__name__} do not have attribute {snake_name}.") + print("Choices are...") + printConfig(localConfig) + printConfig(moduleConfig) return False - # read the value - config_values = getattr(config, config_type.name) - pref_value = getattr(config_values, pref.name) - - if Globals.getInstance().get_camel_case(): - print(f"{str(config_type.name)}.{camel_name}: {str(pref_value)}") - logging.debug(f"{str(config_type.name)}.{camel_name}: {str(pref_value)}") - else: - print(f"{str(config_type.name)}.{snake_name}: {str(pref_value)}") - logging.debug(f"{str(config_type.name)}.{snake_name}: {str(pref_value)}") + if dest == BROADCAST_ADDR: + # read the value + config_values = getattr(config, config_type.name) + if not fullConfig: + pref_value = getattr(config_values, pref.name) + if Globals.getInstance().get_camel_case(): + print(f"{str(config_type.name)}.{camel_name}: {str(pref_value)}") + logging.debug(f"{str(config_type.name)}.{camel_name}: {str(pref_value)}") + else: + print(f"{str(config_type.name)}.{snake_name}: {str(pref_value)}") + logging.debug(f"{str(config_type.name)}.{snake_name}: {str(pref_value)}") + else: + print(f"{str(config_type.name)}: {str(config_values)}") + logging.debug(f"{str(config_type.name)}: {str(config_values)}") + else: + # Always request full config for remote node + interface.getNode(dest, False).requestConfig(config_type) + return True def splitCompoundName(comp_name): @@ -298,7 +320,7 @@ def onConnected(interface): if args.device_metadata: closeNow = True - interface.getNode(args.dest).getMetadata() + interface.getNode(args.dest, False).getMetadata() if args.begin_edit: closeNow = True @@ -597,32 +619,20 @@ def onConnected(interface): # If we aren't trying to talk to our local node, don't show it if args.dest == BROADCAST_ADDR: interface.showInfo() - - print("") - interface.getNode(args.dest).showInfo() - closeNow = True # FIXME, for now we leave the link up while talking to remote nodes - print("") + print("") + interface.getNode(args.dest).showInfo() + closeNow = True + print("") + else: + print("Showing info of remote node is not supported.") + print("Use the '--get' command for a specific configuration (e.g. 'lora') instead.") if args.get: closeNow = True - localConfig = interface.getNode(args.dest).localConfig - moduleConfig = interface.getNode(args.dest).moduleConfig - - # Handle the int/float/bool arguments for pref in args.get: - found = getPref(localConfig, pref[0]) - if not found: - found = getPref(moduleConfig, pref[0]) + found = getPref(interface, args.dest, pref[0]) - if not found: - if Globals.getInstance().get_camel_case(): - print(f"{localConfig.__class__.__name__} and {moduleConfig.__class__.__name__} do not have an attribute {pref[0]}.") - else: - print(f"{localConfig.__class__.__name__} and {moduleConfig.__class__.__name__} do not have attribute {pref[0]}.") - print("Choices are...") - printConfig(localConfig) - printConfig(moduleConfig) - else: + if found: print("Completed getting preferences") if args.nodes: diff --git a/meshtastic/mesh_interface.py b/meshtastic/mesh_interface.py index 13ee8cc..8568fb0 100644 --- a/meshtastic/mesh_interface.py +++ b/meshtastic/mesh_interface.py @@ -170,18 +170,18 @@ class MeshInterface: return table - def getNode(self, nodeId, requestConfig=True): + def getNode(self, nodeId, requestChannels=True): """Return a node object which contains device settings and channel info""" if nodeId in (LOCAL_ADDR, BROADCAST_ADDR): return self.localNode else: n = meshtastic.node.Node(self, nodeId) # Only request device settings and channel info when necessary - if requestConfig: - logging.debug("About to requestConfig") - n.requestConfig() + if requestChannels: + logging.debug("About to requestChannels") + n.requestChannels() if not n.waitForConfig(): - our_exit("Error: Timed out waiting for node config") + our_exit("Error: Timed out waiting for channels") return n def sendText(self, text: AnyStr, @@ -522,7 +522,7 @@ class MeshInterface: Done with initial config messages, now send regular MeshPackets to ask for settings and channels """ - self.localNode.requestConfig() + self.localNode.requestChannels() def _handleFromRadio(self, fromRadioBytes): """ diff --git a/meshtastic/node.py b/meshtastic/node.py index 821c191..d3f328a 100644 --- a/meshtastic/node.py +++ b/meshtastic/node.py @@ -61,13 +61,45 @@ class Node: print(f"Module preferences: {prefs}\n") self.showChannels() - def requestConfig(self): - """Send regular MeshPackets to ask for settings and channels.""" - logging.debug(f"requestConfig for nodeNum:{self.nodeNum}") + def requestChannels(self): + """Send regular MeshPackets to ask channels.""" + logging.debug(f"requestChannels for nodeNum:{self.nodeNum}") self.channels = None self.partialChannels = [] # We keep our channels in a temp array until finished self._requestChannel(0) + + def onResponseRequestSettings(self, p): + """Handle the response packets for requesting settings _requestSettings()""" + logging.debug(f'onResponseRequestSetting() p:{p}') + if "routing" in p["decoded"]: + if p["decoded"]["routing"]["errorReason"] != "NONE": + print(f'Error on response: {p["decoded"]["routing"]["errorReason"]}') + self.iface._acknowledgment.receivedNak = True + else: + self.iface._acknowledgment.receivedAck = True + if "getConfigResponse" in p["decoded"]["admin"]: + print("Config is as follows:", p["decoded"]["admin"]['getConfigResponse']) + else: + print("Module Config is as follows:", p["decoded"]["admin"]['getModuleConfigResponse']) + + def requestConfig(self, configType): + print("Requesting config from remote node (this can take a while).") + 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')") + + msgIndex = configType.index + if configType.containing_type.full_name == "LocalConfig": + p = admin_pb2.AdminMessage() + p.get_config_request = msgIndex + self._sendAdmin(p, wantResponse=True, onResponse=self.onResponseRequestSettings) + else: + p = admin_pb2.AdminMessage() + p.get_module_config_request = msgIndex + self._sendAdmin(p, wantResponse=True, onResponse=self.onResponseRequestSettings) + self.iface.waitForAckNak() def turnOffEncryptionOnPrimaryChannel(self): """Turn off encryption on primary channel.""" From 802768e0cc77a380b9e0050c08814b56596b1eb2 Mon Sep 17 00:00:00 2001 From: GUVWAF Date: Sat, 18 Mar 2023 16:40:27 +0100 Subject: [PATCH 2/5] Nicer printing --- meshtastic/__main__.py | 3 ++- meshtastic/node.py | 7 +++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index 5e6c510..c9b9be8 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -65,6 +65,7 @@ def getPref(interface, dest, comp_name): logging.debug(f'snake_name:{snake_name} camel_name:{camel_name}') logging.debug(f'use camel:{Globals.getInstance().get_camel_case()}') + # First validate the input by looking at config of connected node localConfig = interface.getNode(BROADCAST_ADDR).localConfig moduleConfig = interface.getNode(BROADCAST_ADDR).moduleConfig found = False @@ -103,7 +104,7 @@ def getPref(interface, dest, comp_name): print(f"{str(config_type.name)}: {str(config_values)}") logging.debug(f"{str(config_type.name)}: {str(config_values)}") else: - # Always request full config for remote node + # Always show full config for remote node interface.getNode(dest, False).requestConfig(config_type) return True diff --git a/meshtastic/node.py b/meshtastic/node.py index d3f328a..563a245 100644 --- a/meshtastic/node.py +++ b/meshtastic/node.py @@ -78,10 +78,13 @@ class Node: self.iface._acknowledgment.receivedNak = True else: self.iface._acknowledgment.receivedAck = True + print("") if "getConfigResponse" in p["decoded"]["admin"]: - print("Config is as follows:", p["decoded"]["admin"]['getConfigResponse']) + prefs = stripnl(p["decoded"]["admin"]["getConfigResponse"]) + print(f"Preferences: {prefs}\n") else: - print("Module Config is as follows:", p["decoded"]["admin"]['getModuleConfigResponse']) + prefs = stripnl(p["decoded"]["admin"]["getModuleConfigResponse"]) + print(f"Module preferences: {prefs}\n") def requestConfig(self, configType): print("Requesting config from remote node (this can take a while).") From b2cfebc5a7a813920dba178487201e2287a827ff Mon Sep 17 00:00:00 2001 From: GUVWAF Date: Sat, 18 Mar 2023 21:12:05 +0100 Subject: [PATCH 3/5] '--set' works as well, also chained commands --- meshtastic/__main__.py | 43 +++++++++++++++++++++++++----------------- meshtastic/node.py | 40 ++++++++++++++++++++++++++------------- 2 files changed, 53 insertions(+), 30 deletions(-) diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index c9b9be8..a257025 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -53,11 +53,11 @@ def onConnection(interface, topic=pub.AUTO_TOPIC): # pylint: disable=W0613 print(f"Connection changed: {topic.getName()}") -def getPref(interface, dest, comp_name): +def getPref(node, comp_name): """Get a channel or preferences value""" name = splitCompoundName(comp_name) - fullConfig = name[0] == name[1] # We want the full config + wholeField = name[0] == name[1] # We want the whole field camel_name = meshtastic.util.snake_to_camel(name[1]) # Note: protobufs has the keys in snake_case, so snake internally @@ -65,9 +65,9 @@ def getPref(interface, dest, comp_name): logging.debug(f'snake_name:{snake_name} camel_name:{camel_name}') logging.debug(f'use camel:{Globals.getInstance().get_camel_case()}') - # First validate the input by looking at config of connected node - localConfig = interface.getNode(BROADCAST_ADDR).localConfig - moduleConfig = interface.getNode(BROADCAST_ADDR).moduleConfig + # First validate the input + localConfig = node.localConfig + moduleConfig = node.moduleConfig found = False for config in [localConfig, moduleConfig]: objDesc = config.DESCRIPTOR @@ -75,7 +75,7 @@ def getPref(interface, dest, comp_name): pref = False if config_type: pref = config_type.message_type.fields_by_name.get(snake_name) - if pref or fullConfig: + if pref or wholeField: found = True break @@ -89,10 +89,11 @@ def getPref(interface, dest, comp_name): printConfig(moduleConfig) return False - if dest == BROADCAST_ADDR: + # Check if we need to request the config + if len(config.ListFields()) != 0: # read the value config_values = getattr(config, config_type.name) - if not fullConfig: + if not wholeField: pref_value = getattr(config_values, pref.name) if Globals.getInstance().get_camel_case(): print(f"{str(config_type.name)}.{camel_name}: {str(pref_value)}") @@ -101,11 +102,11 @@ def getPref(interface, dest, comp_name): print(f"{str(config_type.name)}.{snake_name}: {str(pref_value)}") logging.debug(f"{str(config_type.name)}.{snake_name}: {str(pref_value)}") else: - print(f"{str(config_type.name)}: {str(config_values)}") + print(f"{str(config_type.name)}:\n{str(config_values)}") logging.debug(f"{str(config_type.name)}: {str(config_values)}") else: - # Always show full config for remote node - interface.getNode(dest, False).requestConfig(config_type) + # Always show whole field for remote node + node.requestConfig(config_type) return True @@ -405,18 +406,25 @@ def onConnected(interface): # handle settings if args.set: closeNow = True - node = interface.getNode(args.dest) + node = interface.getNode(args.dest, False) # Handle the int/float/bool arguments pref = None for pref in args.set: - found = setPref(node.localConfig, pref[0], pref[1]) - if not found: - found = setPref(node.moduleConfig, pref[0], pref[1]) + found = False + field = splitCompoundName(pref[0].lower())[0] + for config in [node.localConfig, node.moduleConfig]: + config_type = config.DESCRIPTOR.fields_by_name.get(field) + if config_type: + if len(config.ListFields()) == 0: + node.requestConfig(config.DESCRIPTOR.fields_by_name.get(field)) + found = setPref(config, pref[0], pref[1]) + if found: + break if found: print("Writing modified preferences to device") - interface.getNode(args.dest).writeConfig(splitCompoundName(pref[0].lower())[0]) + node.writeConfig(field) else: if Globals.getInstance().get_camel_case(): print(f"{node.localConfig.__class__.__name__} and {node.moduleConfig.__class__.__name__} do not have an attribute {pref[0]}.") @@ -630,8 +638,9 @@ def onConnected(interface): if args.get: closeNow = True + node = interface.getNode(args.dest, False) for pref in args.get: - found = getPref(interface, args.dest, pref[0]) + found = getPref(node, pref[0]) if found: print("Completed getting preferences") diff --git a/meshtastic/node.py b/meshtastic/node.py index 563a245..fd51626 100644 --- a/meshtastic/node.py +++ b/meshtastic/node.py @@ -6,7 +6,7 @@ import base64 import time from google.protobuf.json_format import MessageToJson from meshtastic import portnums_pb2, apponly_pb2, admin_pb2, channel_pb2, localonly_pb2 -from meshtastic.util import pskToString, stripnl, Timeout, our_exit, fromPSK +from meshtastic.util import pskToString, stripnl, Timeout, our_exit, fromPSK, camel_to_snake class Node: @@ -80,29 +80,43 @@ class Node: self.iface._acknowledgment.receivedAck = True print("") if "getConfigResponse" in p["decoded"]["admin"]: - prefs = stripnl(p["decoded"]["admin"]["getConfigResponse"]) - print(f"Preferences: {prefs}\n") + resp = p["decoded"]["admin"]["getConfigResponse"] + field = list(resp.keys())[0] + config_type = self.localConfig.DESCRIPTOR.fields_by_name.get(field) + config_values = getattr(self.localConfig, config_type.name) else: - prefs = stripnl(p["decoded"]["admin"]["getModuleConfigResponse"]) - print(f"Module preferences: {prefs}\n") + resp = p["decoded"]["admin"]["getModuleConfigResponse"] + field = list(resp.keys())[0] + config_type = self.moduleConfig.DESCRIPTOR.fields_by_name.get(field) + config_values = getattr(self.moduleConfig, config_type.name) + for key, value in resp[field].items(): + setattr(config_values, camel_to_snake(key), value) + print(f"{str(field)}:\n{str(config_values)}") def requestConfig(self, configType): - print("Requesting config from remote node (this can take a while).") - 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')") + localOnly = False + if self == self.iface.localNode: + localOnly = True + onResponse = None + else: + onResponse = self.onResponseRequestSettings + print("Requesting config from remote node (this can take a while).") + 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')") msgIndex = configType.index if configType.containing_type.full_name == "LocalConfig": p = admin_pb2.AdminMessage() p.get_config_request = msgIndex - self._sendAdmin(p, wantResponse=True, onResponse=self.onResponseRequestSettings) + self._sendAdmin(p, wantResponse=True, onResponse=onResponse) else: p = admin_pb2.AdminMessage() p.get_module_config_request = msgIndex - self._sendAdmin(p, wantResponse=True, onResponse=self.onResponseRequestSettings) - self.iface.waitForAckNak() + self._sendAdmin(p, wantResponse=True, onResponse=onResponse) + if not localOnly: + self.iface.waitForAckNak() def turnOffEncryptionOnPrimaryChannel(self): """Turn off encryption on primary channel.""" From 55f64946818d320fcfdbae23bc3bc577e4901fcc Mon Sep 17 00:00:00 2001 From: GUVWAF Date: Sun, 19 Mar 2023 14:42:12 +0100 Subject: [PATCH 4/5] Wait for ACK after setting remote config --- meshtastic/__main__.py | 1 + meshtastic/node.py | 28 +++++++++++++++------------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index a257025..d82a9a5 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -406,6 +406,7 @@ def onConnected(interface): # handle settings if args.set: closeNow = True + waitForAckNak = True node = interface.getNode(args.dest, False) # Handle the int/float/bool arguments diff --git a/meshtastic/node.py b/meshtastic/node.py index fd51626..d6b2b0f 100644 --- a/meshtastic/node.py +++ b/meshtastic/node.py @@ -79,32 +79,30 @@ class Node: else: self.iface._acknowledgment.receivedAck = True print("") - if "getConfigResponse" in p["decoded"]["admin"]: - resp = p["decoded"]["admin"]["getConfigResponse"] + adminMessage = p["decoded"]["admin"] + if "getConfigResponse" in adminMessage: + resp = adminMessage["getConfigResponse"] field = list(resp.keys())[0] config_type = self.localConfig.DESCRIPTOR.fields_by_name.get(field) config_values = getattr(self.localConfig, config_type.name) - else: - resp = p["decoded"]["admin"]["getModuleConfigResponse"] + elif "getModuleConfigResponse" in adminMessage: + resp = adminMessage["getModuleConfigResponse"] field = list(resp.keys())[0] config_type = self.moduleConfig.DESCRIPTOR.fields_by_name.get(field) config_values = getattr(self.moduleConfig, config_type.name) + else: + print("Did not receive a valid response. Make sure to have a shared channel named 'admin'.") + return for key, value in resp[field].items(): setattr(config_values, camel_to_snake(key), value) print(f"{str(field)}:\n{str(config_values)}") def requestConfig(self, configType): - localOnly = False if self == self.iface.localNode: - localOnly = True onResponse = None else: onResponse = self.onResponseRequestSettings - print("Requesting config from remote node (this can take a while).") - 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("Requesting current config from remote node (this can take a while).") msgIndex = configType.index if configType.containing_type.full_name == "LocalConfig": @@ -115,7 +113,7 @@ class Node: p = admin_pb2.AdminMessage() p.get_module_config_request = msgIndex self._sendAdmin(p, wantResponse=True, onResponse=onResponse) - if not localOnly: + if onResponse: self.iface.waitForAckNak() def turnOffEncryptionOnPrimaryChannel(self): @@ -279,7 +277,11 @@ class Node: our_exit(f"Error: No valid config with name {config_name}") logging.debug(f"Wrote: {config_name}") - self._sendAdmin(p) + if self == self.iface.localNode: + onResponse = None + else: + onResponse = self.onAckNak + self._sendAdmin(p, onResponse=onResponse) def writeChannel(self, channelIndex, adminIndex=0): """Write the current (edited) channel to the device""" From 7de17f7c94f69e21c3e6f2de7949a2ca0309f93a Mon Sep 17 00:00:00 2001 From: GUVWAF Date: Sun, 19 Mar 2023 14:42:44 +0100 Subject: [PATCH 5/5] Display that '--nodes' is not supported for a remote node --- meshtastic/__main__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index d82a9a5..7a5fb5d 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -648,6 +648,9 @@ def onConnected(interface): if args.nodes: closeNow = True + if args.dest != BROADCAST_ADDR: + print("Showing node list of a remote node is not supported.") + return interface.showNodes() if args.qr: