diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index f7dd2fd..48fe55f 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(node, comp_name): """Get a channel or preferences value""" name = splitCompoundName(comp_name) + 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 @@ -64,26 +65,49 @@ 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) + # First validate the input + localConfig = node.localConfig + moduleConfig = node.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 wholeField: + 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)}") + # 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 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)}") + 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)}:\n{str(config_values)}") + logging.debug(f"{str(config_type.name)}: {str(config_values)}") + else: + # Always show whole field for remote node + node.requestConfig(config_type) + return True def splitCompoundName(comp_name): @@ -298,7 +322,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 @@ -382,18 +406,26 @@ def onConnected(interface): # handle settings if args.set: closeNow = True - node = interface.getNode(args.dest) + waitForAckNak = True + 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]}.") @@ -597,36 +629,28 @@ 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 + node = interface.getNode(args.dest, False) for pref in args.get: - found = getPref(localConfig, pref[0]) - if not found: - found = getPref(moduleConfig, pref[0]) + found = getPref(node, 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: closeNow = True + if args.dest != BROADCAST_ADDR: + print("Showing node list of a remote node is not supported.") + return interface.showNodes() if args.qr: 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..d6b2b0f 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: @@ -61,13 +61,60 @@ 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 + print("") + 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) + 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): + if self == self.iface.localNode: + onResponse = None + else: + onResponse = self.onResponseRequestSettings + print("Requesting current config from remote node (this can take a while).") + + 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=onResponse) + else: + p = admin_pb2.AdminMessage() + p.get_module_config_request = msgIndex + self._sendAdmin(p, wantResponse=True, onResponse=onResponse) + if onResponse: + self.iface.waitForAckNak() def turnOffEncryptionOnPrimaryChannel(self): """Turn off encryption on primary channel.""" @@ -230,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"""