From fe8538e1ceda23c2817a03423c81ad2716fb1052 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?j=C3=BCrgenRe?= Date: Wed, 29 Oct 2025 18:07:35 +0100 Subject: [PATCH 01/12] include isUnmessagable property in export rename variables and methods using camel case for uniformity within module --- meshtastic/__main__.py | 45 +++++++++++++++++++---------------- meshtastic/mesh_interface.py | 10 ++++++-- meshtastic/tests/test_main.py | 4 ++-- 3 files changed, 34 insertions(+), 25 deletions(-) diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index 08ecf57cf..8a6cdedce 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -1130,10 +1130,10 @@ def subscribe() -> None: # pub.subscribe(onNode, "meshtastic.node") -def set_missing_flags_false(config_dict: dict, true_defaults: set[tuple[str, str]]) -> None: +def setMissingFlagsFalse(configDict: dict, trueDefaults: set[tuple[str, str]]) -> None: """Ensure that missing default=True keys are present in the config_dict and set to False.""" - for path in true_defaults: - d = config_dict + for path in trueDefaults: + d = configDict for key in path[:-1]: if key not in d or not isinstance(d[key], dict): d[key] = {} @@ -1157,8 +1157,9 @@ def export_config(interface) -> str: } owner = interface.getLongName() - owner_short = interface.getShortName() - channel_url = interface.localNode.getURL() + ownerShort = interface.getShortName() + isUnmessagable = interface.getIsUnmessagable() + channelUrl = interface.localNode.getURL() myinfo = interface.getMyNodeInfo() canned_messages = interface.getCannedMessage() ringtone = interface.getRingtone() @@ -1173,13 +1174,15 @@ def export_config(interface) -> str: if owner: configObj["owner"] = owner - if owner_short: - configObj["owner_short"] = owner_short - if channel_url: + if ownerShort: + configObj["owner_short"] = ownerShort + if isUnmessagable: + configObj["is_unmessagable"] = isUnmessagable + if channelUrl: if mt_config.camel_case: - configObj["channelUrl"] = channel_url + configObj["channelUrl"] = channelUrl else: - configObj["channel_url"] = channel_url + configObj["channel_url"] = channelUrl if canned_messages: configObj["canned_messages"] = canned_messages if ringtone: @@ -1214,27 +1217,27 @@ def export_config(interface) -> str: else: configObj["config"] = config - set_missing_flags_false(configObj["config"], true_defaults) + setMissingFlagsFalse(configObj["config"], true_defaults) - module_config = MessageToDict(interface.localNode.moduleConfig) - if module_config: + moduleConfig = MessageToDict(interface.localNode.moduleConfig) + if moduleConfig: # Convert inner keys to correct snake/camelCase prefs = {} - for pref in module_config: - if len(module_config[pref]) > 0: - prefs[pref] = module_config[pref] + for pref in moduleConfig: + if len(moduleConfig[pref]) > 0: + prefs[pref] = moduleConfig[pref] if mt_config.camel_case: configObj["module_config"] = prefs else: configObj["module_config"] = prefs - config_txt = "# start of Meshtastic configure yaml\n" #checkme - "config" (now changed to config_out) + configTxt = "# start of Meshtastic configure yaml\n" #checkme - "config" (now changed to config_out) #was used as a string here and a Dictionary above - config_txt += yaml.dump(configObj) - return config_txt + configTxt += yaml.dump(configObj) + return configTxt -def create_power_meter(): +def createPowerMeter(): """Setup the power meter.""" global meter # pylint: disable=global-statement @@ -1308,7 +1311,7 @@ def common(): meshtastic.util.our_exit("ERROR: Ham radio callsign cannot be empty or contain only whitespace characters") if have_powermon: - create_power_meter() + createPowerMeter() if args.ch_index is not None: channelIndex = int(args.ch_index) diff --git a/meshtastic/mesh_interface.py b/meshtastic/mesh_interface.py index c269209d0..a776ef8ea 100644 --- a/meshtastic/mesh_interface.py +++ b/meshtastic/mesh_interface.py @@ -261,7 +261,6 @@ def get_human_readable(name): else: return name - def formatFloat(value, precision=2, unit="") -> Optional[str]: """Format a float value with precision.""" return f"{value:.{precision}f}{unit}" if value else None @@ -296,7 +295,7 @@ def getNestedValue(node_dict: Dict[str, Any], key_path: str) -> Any: return value if showFields is None or len(showFields) == 0: - # The default set of fields to show (e.g., the status quo) + # The default set of fields to show (e.g., the status quo) showFields = ["N", "user.longName", "user.id", "user.shortName", "user.hwModel", "user.publicKey", "user.role", "position.latitude", "position.longitude", "position.altitude", "deviceMetrics.batteryLevel", "deviceMetrics.channelUtilization", @@ -1080,6 +1079,13 @@ def getShortName(self): return user.get("shortName", None) return None + def getIsUnmessagable(self): + """Get getIsUnmessagable property""" + user = self.getMyUser() + if user is not None: + return user.get("isUnmessagable", None) + return None + def getPublicKey(self): """Get Public Key""" user = self.getMyUser() diff --git a/meshtastic/tests/test_main.py b/meshtastic/tests/test_main.py index 251de98fe..0f10d9735 100644 --- a/meshtastic/tests/test_main.py +++ b/meshtastic/tests/test_main.py @@ -18,7 +18,7 @@ onNode, onReceive, tunnelMain, - set_missing_flags_false, + setMissingFlagsFalse, ) from meshtastic import mt_config @@ -1911,7 +1911,7 @@ def test_set_missing_flags_false(): ("mqtt", "encryptionEnabled"), } - set_missing_flags_false(config, false_defaults) + setMissingFlagsFalse(config, false_defaults) # Preserved assert config["bluetooth"]["enabled"] is True From aa8705f230b0ed6765e243cf674a116794601bf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?j=C3=BCrgenRe?= Date: Thu, 30 Oct 2025 10:33:30 +0100 Subject: [PATCH 02/12] reworking args.configure clause: - simplifying code, removing copy/paste parts - configure "is_unmessagable" entry value --- meshtastic/__main__.py | 169 ++++++++++++++++++----------------------- meshtastic/node.py | 4 +- 2 files changed, 77 insertions(+), 96 deletions(-) diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index 8a6cdedce..cd8e965dc 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -670,115 +670,96 @@ def onConnected(interface): printConfig(node.moduleConfig) if args.configure: + def fixEntry(cfg: dict, oldKey: str, newKey: str): + """fix configuration entries so only one structure must be handled. + This will be done by copying the previous used key to the now used one. + Abort if both old and new key are present upon entry.""" + if oldKey in cfg: + if newKey in cfg: + meshtastic.util.our_exit( + f"ERROR: Inconsistent settings in configuration: {oldKey} and {newKey} are both present.") + else: + cfg[newKey] = cfg[oldKey] + + def entryToStr(entry) -> Union[str | None]: + """Ensures conversion of an entry to string if it is not None + Failure of conversion will be trapped upstream""" + if entry is not None: + return str(entry).strip() + return None + with open(args.configure[0], encoding="utf8") as file: configuration = yaml.safe_load(file) closeNow = True - interface.getNode(args.dest, False, **getNode_kwargs).beginSettingsTransaction() + # fix configuration structure: older version had entries "ownerShort" and "channelUrl"? + fixEntry(configuration, "ownerShort", "owner_short") + fixEntry(configuration, "channelUrl", "channel_url") - if "owner" in configuration: - # Validate owner name before setting - owner_name = str(configuration["owner"]).strip() - if not owner_name: - meshtastic.util.our_exit("ERROR: Long Name cannot be empty or contain only whitespace characters") - print(f"Setting device owner to {configuration['owner']}") - waitForAckNak = True - interface.getNode(args.dest, False, **getNode_kwargs).setOwner(configuration["owner"]) + # keep always the same node when applying settings + actualNode = interface.getNode(args.dest, True, **getNode_kwargs) + actualNode.beginSettingsTransaction() + waitForAckNak = True + + ownerName = entryToStr(configuration.get("owner", None)) + ownerShortName = entryToStr(configuration.get("owner_short", None)) + isUnmessagable = configuration.get("is_unmessagable", None) + actualNode.setOwner(long_name=ownerName, short_name=ownerShortName, is_unmessagable=isUnmessagable) + time.sleep(0.5) + + if "channel_url" in configuration: + print("Setting channel url to", configuration["channel_url"]) + actualNode.setURL(configuration["channel_url"]) + time.sleep(0.5) + + if "canned_messages" in configuration: + print("Setting canned message messages to", configuration["canned_messages"]) + actualNode.set_canned_message(configuration["canned_messages"]) + time.sleep(0.5) + + if "ringtone" in configuration: + print("Setting ringtone to", configuration["ringtone"]) + actualNode.set_ringtone(configuration["ringtone"]) + time.sleep(0.5) + + if "location" in configuration: + if configuration["location"] is None: + print("No position elements found, skipping") + else: + alt = int(configuration["location"].get("alt", 0)) + lat = float(configuration["location"].get("lat", 0.0)) + lon = float(configuration["location"].get("lon", 0.0)) + print(f"Setting fixed device position to lat {lat} lon {lon} alt {alt}") + interface.localNode.setFixedPosition(lat, lon, alt) time.sleep(0.5) - if "owner_short" in configuration: - # Validate owner short name before setting - owner_short_name = str(configuration["owner_short"]).strip() - if not owner_short_name: - meshtastic.util.our_exit("ERROR: Short Name cannot be empty or contain only whitespace characters") - print( - f"Setting device owner short to {configuration['owner_short']}" + if "config" in configuration: + localConfig = actualNode.localConfig + for section in configuration["config"]: + traverseConfig( + section, configuration["config"][section], localConfig ) - waitForAckNak = True - interface.getNode(args.dest, False, **getNode_kwargs).setOwner( - long_name=None, short_name=configuration["owner_short"] + actualNode.writeConfig( + meshtastic.util.camel_to_snake(section) ) time.sleep(0.5) - if "ownerShort" in configuration: - # Validate owner short name before setting - owner_short_name = str(configuration["ownerShort"]).strip() - if not owner_short_name: - meshtastic.util.our_exit("ERROR: Short Name cannot be empty or contain only whitespace characters") - print( - f"Setting device owner short to {configuration['ownerShort']}" + if "module_config" in configuration: + moduleConfig = actualNode.moduleConfig + for section in configuration["module_config"]: + traverseConfig( + section, + configuration["module_config"][section], + moduleConfig, ) - waitForAckNak = True - interface.getNode(args.dest, False, **getNode_kwargs).setOwner( - long_name=None, short_name=configuration["ownerShort"] + actualNode.writeConfig( + meshtastic.util.camel_to_snake(section) ) time.sleep(0.5) - if "channel_url" in configuration: - print("Setting channel url to", configuration["channel_url"]) - interface.getNode(args.dest, **getNode_kwargs).setURL(configuration["channel_url"]) - time.sleep(0.5) - - if "channelUrl" in configuration: - print("Setting channel url to", configuration["channelUrl"]) - 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 - lon = 0.0 - localConfig = interface.localNode.localConfig - - if "alt" in configuration["location"]: - alt = int(configuration["location"]["alt"] or 0) - print(f"Fixing altitude at {alt} meters") - if "lat" in configuration["location"]: - lat = float(configuration["location"]["lat"] or 0) - print(f"Fixing latitude at {lat} degrees") - if "lon" in configuration["location"]: - lon = float(configuration["location"]["lon"] or 0) - print(f"Fixing longitude at {lon} degrees") - print("Setting device position") - interface.localNode.setFixedPosition(lat, lon, alt) - time.sleep(0.5) - - if "config" in configuration: - localConfig = interface.getNode(args.dest, **getNode_kwargs).localConfig - for section in configuration["config"]: - traverseConfig( - section, configuration["config"][section], localConfig - ) - interface.getNode(args.dest, **getNode_kwargs).writeConfig( - meshtastic.util.camel_to_snake(section) - ) - time.sleep(0.5) - - if "module_config" in configuration: - moduleConfig = interface.getNode(args.dest, **getNode_kwargs).moduleConfig - for section in configuration["module_config"]: - traverseConfig( - section, - configuration["module_config"][section], - moduleConfig, - ) - interface.getNode(args.dest, **getNode_kwargs).writeConfig( - meshtastic.util.camel_to_snake(section) - ) - time.sleep(0.5) - - interface.getNode(args.dest, False, **getNode_kwargs).commitSettingsTransaction() - print("Writing modified configuration to device") + print("Committing modified configuration to device") + actualNode.commitSettingsTransaction() + print("Configuration finished") if args.export_config: if args.dest != BROADCAST_ADDR: diff --git a/meshtastic/node.py b/meshtastic/node.py index b77ad9218..a8e59bb81 100644 --- a/meshtastic/node.py +++ b/meshtastic/node.py @@ -309,8 +309,8 @@ def _getAdminChannelIndex(self): return c.index return 0 - def setOwner(self, long_name: Optional[str]=None, short_name: Optional[str]=None, is_licensed: bool=False, is_unmessagable: Optional[bool]=None): - """Set device owner name""" + def setOwner(self, long_name: Optional[str] = None, short_name: Optional[str] = None, is_licensed: bool = False, is_unmessagable: Optional[bool] = None): + """Set device owner properties""" logger.debug(f"in setOwner nodeNum:{self.nodeNum}") self.ensureSessionKey() p = admin_pb2.AdminMessage() From b1529497a4eb4c38ba4b0a4867e222b829cb245f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?j=C3=BCrgenRe?= Date: Sat, 1 Nov 2025 11:03:10 +0100 Subject: [PATCH 03/12] refactor writing configuration to nodes: extract common code in separate function --- meshtastic/__main__.py | 76 +++++++++++++++++------------------------- 1 file changed, 31 insertions(+), 45 deletions(-) diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index cd8e965dc..e00745a07 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -688,6 +688,13 @@ def entryToStr(entry) -> Union[str | None]: return str(entry).strip() return None + def writeSectionConfig(newConfig: dict, actConfig: dict, actNode: meshtastic.node.Node): + """Traverses a configuration dict and writes each part to the node""" + for sectionName, sectionElements in newConfig.items(): + traverseConfig(sectionName, sectionElements, actConfig) + actNode.writeConfig(meshtastic.util.camel_to_snake(sectionName)) + time.sleep(0.5) + with open(args.configure[0], encoding="utf8") as file: configuration = yaml.safe_load(file) closeNow = True @@ -697,7 +704,7 @@ def entryToStr(entry) -> Union[str | None]: fixEntry(configuration, "channelUrl", "channel_url") # keep always the same node when applying settings - actualNode = interface.getNode(args.dest, True, **getNode_kwargs) + actualNode: meshtastic.node.Node = interface.getNode(args.dest, True, **getNode_kwargs) actualNode.beginSettingsTransaction() waitForAckNak = True @@ -734,28 +741,10 @@ def entryToStr(entry) -> Union[str | None]: time.sleep(0.5) if "config" in configuration: - localConfig = actualNode.localConfig - for section in configuration["config"]: - traverseConfig( - section, configuration["config"][section], localConfig - ) - actualNode.writeConfig( - meshtastic.util.camel_to_snake(section) - ) - time.sleep(0.5) + writeSectionConfig(configuration["config"], actualNode.localConfig, actualNode) if "module_config" in configuration: - moduleConfig = actualNode.moduleConfig - for section in configuration["module_config"]: - traverseConfig( - section, - configuration["module_config"][section], - moduleConfig, - ) - actualNode.writeConfig( - meshtastic.util.camel_to_snake(section) - ) - time.sleep(0.5) + writeSectionConfig(configuration["module_config"], actualNode.moduleConfig, actualNode) print("Committing modified configuration to device") actualNode.commitSettingsTransaction() @@ -1111,10 +1100,10 @@ def subscribe() -> None: # pub.subscribe(onNode, "meshtastic.node") -def setMissingFlagsFalse(configDict: dict, trueDefaults: set[tuple[str, str]]) -> None: +def set_missing_flags_false(config_dict: dict, true_defaults: set[tuple[str, str]]) -> None: """Ensure that missing default=True keys are present in the config_dict and set to False.""" - for path in trueDefaults: - d = configDict + for path in true_defaults: + d = config_dict for key in path[:-1]: if key not in d or not isinstance(d[key], dict): d[key] = {} @@ -1138,9 +1127,8 @@ def export_config(interface) -> str: } owner = interface.getLongName() - ownerShort = interface.getShortName() - isUnmessagable = interface.getIsUnmessagable() - channelUrl = interface.localNode.getURL() + owner_short = interface.getShortName() + channel_url = interface.localNode.getURL() myinfo = interface.getMyNodeInfo() canned_messages = interface.getCannedMessage() ringtone = interface.getRingtone() @@ -1155,15 +1143,13 @@ def export_config(interface) -> str: if owner: configObj["owner"] = owner - if ownerShort: - configObj["owner_short"] = ownerShort - if isUnmessagable: - configObj["is_unmessagable"] = isUnmessagable - if channelUrl: + if owner_short: + configObj["owner_short"] = owner_short + if channel_url: if mt_config.camel_case: - configObj["channelUrl"] = channelUrl + configObj["channelUrl"] = channel_url else: - configObj["channel_url"] = channelUrl + configObj["channel_url"] = channel_url if canned_messages: configObj["canned_messages"] = canned_messages if ringtone: @@ -1198,27 +1184,27 @@ def export_config(interface) -> str: else: configObj["config"] = config - setMissingFlagsFalse(configObj["config"], true_defaults) + set_missing_flags_false(configObj["config"], true_defaults) - moduleConfig = MessageToDict(interface.localNode.moduleConfig) - if moduleConfig: + module_config = MessageToDict(interface.localNode.moduleConfig) + if module_config: # Convert inner keys to correct snake/camelCase prefs = {} - for pref in moduleConfig: - if len(moduleConfig[pref]) > 0: - prefs[pref] = moduleConfig[pref] + for pref in module_config: + if len(module_config[pref]) > 0: + prefs[pref] = module_config[pref] if mt_config.camel_case: configObj["module_config"] = prefs else: configObj["module_config"] = prefs - configTxt = "# start of Meshtastic configure yaml\n" #checkme - "config" (now changed to config_out) + config_txt = "# start of Meshtastic configure yaml\n" #checkme - "config" (now changed to config_out) #was used as a string here and a Dictionary above - configTxt += yaml.dump(configObj) - return configTxt + config_txt += yaml.dump(configObj) + return config_txt -def createPowerMeter(): +def create_power_meter(): """Setup the power meter.""" global meter # pylint: disable=global-statement @@ -1292,7 +1278,7 @@ def common(): meshtastic.util.our_exit("ERROR: Ham radio callsign cannot be empty or contain only whitespace characters") if have_powermon: - createPowerMeter() + create_power_meter() if args.ch_index is not None: channelIndex = int(args.ch_index) From 14819a05ac6ba74381e04eeb71b0225e8114ae3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?j=C3=BCrgenRe?= Date: Wed, 5 Nov 2025 16:55:59 +0100 Subject: [PATCH 04/12] - updating and extending smoke test 1 tests. Also updating used URLs and example.yaml - configuring channels via URL will delete all channel prior to load the new settings. - adding --fmt parameter for --nodes, otherwise pytest will always fail (problem seems to be located within tabulate library) - minor editing to avoid mix of camel and snake case --- example_config.yaml | 7 +- meshtastic/__main__.py | 75 ++++--- meshtastic/mesh_interface.py | 14 +- meshtastic/node.py | 43 +++- meshtastic/tests/test_smoke1.py | 358 ++++++++++++++++++++------------ 5 files changed, 315 insertions(+), 182 deletions(-) diff --git a/example_config.yaml b/example_config.yaml index acbdc332b..7cb3880cd 100644 --- a/example_config.yaml +++ b/example_config.yaml @@ -1,8 +1,9 @@ # example configuration file with snake_case keys owner: Bob TBeam owner_short: BOB +is_unmessagable: true -channel_url: https://www.meshtastic.org/e/#CgMSAQESCDgBQANIAVAe +channel_url: https://www.meshtastic.org/e/#CgI6ABIPCAE4AUADSAFQHmgBwAYB 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 @@ -19,7 +20,7 @@ config: device: serialEnabled: true display: - screenOnSecs: 600 + screenOnSecs: 781 lora: region: US hopLimit: 3 @@ -39,7 +40,7 @@ config: meshSdsTimeoutSecs: 7200 minWakeSecs: 10 sdsSecs: 4294967295 - + waitBluetoothSecs: 344 module_config: telemetry: deviceUpdateInterval: 900 diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index e00745a07..f48cec430 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -711,11 +711,13 @@ def writeSectionConfig(newConfig: dict, actConfig: dict, actNode: meshtastic.nod ownerName = entryToStr(configuration.get("owner", None)) ownerShortName = entryToStr(configuration.get("owner_short", None)) isUnmessagable = configuration.get("is_unmessagable", None) + print(f"Setting owner properties: {ownerName} - {ownerShortName} - {isUnmessagable}") actualNode.setOwner(long_name=ownerName, short_name=ownerShortName, is_unmessagable=isUnmessagable) time.sleep(0.5) if "channel_url" in configuration: print("Setting channel url to", configuration["channel_url"]) + actualNode.deleteAllSecondaryChannels() actualNode.setURL(configuration["channel_url"]) time.sleep(0.5) @@ -830,7 +832,7 @@ def writeSectionConfig(newConfig: dict, actConfig: dict, actNode: meshtastic.nod ) else: print(f"Deleting channel {channelIndex}") - ch = interface.getNode(args.dest, **getNode_kwargs).deleteChannel(channelIndex) + interface.getNode(args.dest, **getNode_kwargs).deleteChannel(channelIndex) def setSimpleConfig(modem_preset): """Set one of the simple modem_config""" @@ -982,12 +984,16 @@ def setSimpleConfig(modem_preset): if args.dest != BROADCAST_ADDR: print("Showing node list of a remote node is not supported.") return - interface.showNodes(True, args.show_fields) + interface.showNodes(True, showFields=args.show_fields, printFmt=args.fmt) if args.show_fields and not args.nodes: print("--show-fields can only be used with --nodes") return + if args.fmt and not args.nodes: + print("--fmt can only be used with --nodes") + return + if args.qr or args.qr_all: closeNow = True url = interface.getNode(args.dest, True, **getNode_kwargs).getURL(includeAll=args.qr_all) @@ -1100,10 +1106,11 @@ def subscribe() -> None: # pub.subscribe(onNode, "meshtastic.node") -def set_missing_flags_false(config_dict: dict, true_defaults: set[tuple[str, str]]) -> None: + +def setMissingFlagsFalse(configDict: dict, trueDefaults: set[tuple[str, str]]) -> None: """Ensure that missing default=True keys are present in the config_dict and set to False.""" - for path in true_defaults: - d = config_dict + for path in trueDefaults: + d = configDict for key in path[:-1]: if key not in d or not isinstance(d[key], dict): d[key] = {} @@ -1111,9 +1118,10 @@ def set_missing_flags_false(config_dict: dict, true_defaults: set[tuple[str, str if path[-1] not in d: d[path[-1]] = False + def export_config(interface) -> str: - """used in --export-config""" - configObj = {} + """implements --export-config option""" + configObj = {} # main object collecting all settings to be exported # A list of configuration keys that should be set to False if they are missing true_defaults = { @@ -1127,10 +1135,12 @@ def export_config(interface) -> str: } owner = interface.getLongName() - owner_short = interface.getShortName() - channel_url = interface.localNode.getURL() + ownerShort = interface.getShortName() + channelUrl = interface.localNode.getURL() + isUnmessagable = interface.getIsUnmessagable() + myinfo = interface.getMyNodeInfo() - canned_messages = interface.getCannedMessage() + cannedMessages = interface.getCannedMessage() ringtone = interface.getRingtone() pos = myinfo.get("position") lat = None @@ -1143,15 +1153,17 @@ def export_config(interface) -> str: if owner: configObj["owner"] = owner - if owner_short: - configObj["owner_short"] = owner_short - if channel_url: + if ownerShort: + configObj["owner_short"] = ownerShort + if isUnmessagable: + configObj["is_unmessagable"] = isUnmessagable + if channelUrl: if mt_config.camel_case: - configObj["channelUrl"] = channel_url + configObj["channelUrl"] = channelUrl else: - configObj["channel_url"] = channel_url - if canned_messages: - configObj["canned_messages"] = canned_messages + configObj["channel_url"] = channelUrl + if cannedMessages: + configObj["canned_messages"] = cannedMessages 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 @@ -1160,8 +1172,7 @@ def export_config(interface) -> str: if alt: configObj["location"]["alt"] = alt - config = MessageToDict(interface.localNode.localConfig) #checkme - Used as a dictionary here and a string below - #was used as a string here and a Dictionary above + config: dict = MessageToDict(interface.localNode.localConfig) if config: # Convert inner keys to correct snake/camelCase prefs = {} @@ -1184,24 +1195,23 @@ def export_config(interface) -> str: else: configObj["config"] = config - set_missing_flags_false(configObj["config"], true_defaults) + setMissingFlagsFalse(configObj["config"], true_defaults) - module_config = MessageToDict(interface.localNode.moduleConfig) - if module_config: + moduleConfig: dict = MessageToDict(interface.localNode.moduleConfig) + if moduleConfig: # Convert inner keys to correct snake/camelCase prefs = {} - for pref in module_config: - if len(module_config[pref]) > 0: - prefs[pref] = module_config[pref] + for pref in moduleConfig: + if len(moduleConfig[pref]) > 0: + prefs[pref] = moduleConfig[pref] if mt_config.camel_case: configObj["module_config"] = prefs else: configObj["module_config"] = prefs - config_txt = "# start of Meshtastic configure yaml\n" #checkme - "config" (now changed to config_out) - #was used as a string here and a Dictionary above - config_txt += yaml.dump(configObj) - return config_txt + configTxt: str = "# start of Meshtastic configure yaml\n" + configTxt += yaml.dump(configObj) + return configTxt def create_power_meter(): @@ -1796,6 +1806,13 @@ def addLocalActionArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentPars default=None ) + group.add_argument( + "--fmt", + help="Specify format to show when using --nodes", + type=str, + default=None + ) + return parser def addRemoteActionArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentParser: diff --git a/meshtastic/mesh_interface.py b/meshtastic/mesh_interface.py index a776ef8ea..5085763bf 100644 --- a/meshtastic/mesh_interface.py +++ b/meshtastic/mesh_interface.py @@ -225,13 +225,14 @@ def showInfo(self, file=sys.stdout) -> str: # pylint: disable=W0613 return infos def showNodes( - self, includeSelf: bool = True, showFields: Optional[List[str]] = None + self, includeSelf: bool = True, showFields: Optional[List[str]] = None, printFmt: Optional[str] = None ) -> str: # pylint: disable=W0613 """Show table summary of nodes in mesh Args: includeSelf (bool): Include ourself in the output? showFields (List[str]): List of fields to show in output + printFmt (str): name of format to use """ def get_human_readable(name): @@ -371,7 +372,16 @@ def getNestedValue(node_dict: Dict[str, Any], key_path: str) -> Any: for i, row in enumerate(rows): row["N"] = i + 1 - table = tabulate(rows, headers="keys", missingval="N/A", tablefmt="fancy_grid") + if not printFmt or len(printFmt) == 0: + printFmt = "fancy_grid" + if printFmt.lower() == 'json': + headers = [] + if len(rows) > 0: + headers = list(rows[0].keys()) + outDict = {'headers': headers, 'nodes': rows} + table = json.dumps(outDict) + else: + table = tabulate(rows, headers="keys", missingval="N/A", tablefmt=printFmt) print(table) return table diff --git a/meshtastic/node.py b/meshtastic/node.py index a8e59bb81..dddfc7013 100644 --- a/meshtastic/node.py +++ b/meshtastic/node.py @@ -260,6 +260,37 @@ def getChannelByChannelIndex(self, channelIndex): ch = self.channels[channelIndex] return ch + def rewriteChannelList(self, idxStart: int, admIndex: int) -> None: + """write back current channels list to device starting from index idxStart + channels before this index have not changed, so we don't need to rewrite.""" + index = idxStart + while index < 8: + self.writeChannel(index, adminIndex=admIndex) + index += 1 + + # if we are updating the local node, we might end up + # *moving* the admin channel index as we are writing + if (self.iface.localNode == self) and index >= admIndex: + # We've now passed the old location for admin index + # (and written it), so we can start finding it by name again + admIndex = 0 + + def deleteAllSecondaryChannels(self) -> None: + """Remove all secondary or disbled channels in order to be able to rewrite channel config during + configuration, only keep primary channel""" + if self.channels: + adminIndex = self.iface.localNode._getAdminChannelIndex() + + idx2Delete = [c.index for c in self.channels + if c.role == channel_pb2.Channel.Role.SECONDARY and not c.index == adminIndex + ] + if len(idx2Delete) > 0: + idx2Delete.reverse() + for idx in idx2Delete: + self.channels.pop(idx) + self._fixupChannels() + self.rewriteChannelList(idx2Delete[-1], adminIndex) + def deleteChannel(self, channelIndex): """Delete the specified channelIndex and shift other channels up""" ch = self.channels[channelIndex] @@ -276,17 +307,7 @@ def deleteChannel(self, channelIndex): self.channels.pop(channelIndex) self._fixupChannels() # expand back to 8 channels - index = channelIndex - while index < 8: - self.writeChannel(index, adminIndex=adminIndex) - index += 1 - - # if we are updating the local node, we might end up - # *moving* the admin channel index as we are writing - if (self.iface.localNode == self) and index >= adminIndex: - # We've now passed the old location for admin index - # (and written it), so we can start finding it by name again - adminIndex = 0 + self.rewriteChannelList(channelIndex, adminIndex) def getChannelByName(self, name): """Try to find the named channel or return None""" diff --git a/meshtastic/tests/test_smoke1.py b/meshtastic/tests/test_smoke1.py index 6a5709e6c..5cd40c58a 100644 --- a/meshtastic/tests/test_smoke1.py +++ b/meshtastic/tests/test_smoke1.py @@ -1,9 +1,11 @@ """Meshtastic smoke tests with a single device via USB""" -import os -import platform +import io import re import subprocess +import sys import time +from pathlib import Path +import tempfile # Do not like using hard coded sleeps, but it probably makes # sense to pause for the radio at appropriate times @@ -13,7 +15,7 @@ # seconds to pause after running a meshtastic command PAUSE_AFTER_COMMAND = 2 -PAUSE_AFTER_REBOOT = 7 +PAUSE_AFTER_REBOOT = 10 @pytest.mark.smoke1 @@ -22,7 +24,7 @@ def test_smoke1_reboot(): return_value, _ = subprocess.getstatusoutput("meshtastic --reboot") assert return_value == 0 # pause for the radio to reset (10 seconds for the pause, and a few more seconds to be back up) - time.sleep(18) + time.sleep(2*PAUSE_AFTER_REBOOT) @pytest.mark.smoke1 @@ -35,54 +37,70 @@ def test_smoke1_info(): assert re.search(r"^Nodes in mesh", out, re.MULTILINE) assert re.search(r"^Preferences", out, re.MULTILINE) assert re.search(r"^Channels", out, re.MULTILINE) - assert re.search(r"^ PRIMARY", out, re.MULTILINE) + assert re.search(r"^\s*Index 0: PRIMARY", out, re.MULTILINE) assert re.search(r"^Primary channel URL", out, re.MULTILINE) assert return_value == 0 + time.sleep(PAUSE_AFTER_COMMAND) + + +@pytest.mark.smoke1 +def test_smoke1_nodes(): + """Test --nodes""" + return_value, out = subprocess.getstatusoutput('meshtastic --nodes --fmt json') + assert re.match(r"Connected to radio", out) + assert re.search(r"N.+User", out, re.MULTILINE) + assert re.search(r'"N": 1, "User":', out, re.MULTILINE) + assert return_value == 0 + time.sleep(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 def test_get_with_invalid_setting(): """Test '--get a_bad_setting'.""" return_value, out = subprocess.getstatusoutput("meshtastic --get a_bad_setting") - assert re.search(r"Choices in sorted order", out) + assert re.search(r"Choices are...", out) assert return_value == 0 + time.sleep(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 def test_set_with_invalid_setting(): """Test '--set a_bad_setting'.""" return_value, out = subprocess.getstatusoutput("meshtastic --set a_bad_setting foo") - assert re.search(r"Choices in sorted order", out) + assert re.search(r"Choices are...", out) assert return_value == 0 + time.sleep(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 -def test_ch_set_with_invalid_settingpatch_find_ports(): +def test_ch_set_with_invalid_setting(): """Test '--ch-set with a_bad_setting'.""" return_value, out = subprocess.getstatusoutput( "meshtastic --ch-set invalid_setting foo --ch-index 0" ) - assert re.search(r"Choices in sorted order", out) + assert re.search(r"Choices are...", out) assert return_value == 0 + time.sleep(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 def test_smoke1_pos_fields(): """Test --pos-fields (with some values POS_ALTITUDE POS_ALT_MSL POS_BATTERY)""" return_value, out = subprocess.getstatusoutput( - "meshtastic --pos-fields POS_ALTITUDE POS_ALT_MSL POS_BATTERY" + "meshtastic --pos-fields ALTITUDE ALTITUDE_MSL HEADING" ) assert re.match(r"Connected to radio", out) - assert re.search(r"^Setting position fields to 35", out, re.MULTILINE) + assert re.search(r"^Setting position fields to 259", out, re.MULTILINE) assert return_value == 0 # pause for the radio - time.sleep(PAUSE_AFTER_COMMAND) + time.sleep(2*PAUSE_AFTER_REBOOT) return_value, out = subprocess.getstatusoutput("meshtastic --pos-fields") assert re.match(r"Connected to radio", out) - assert re.search(r"POS_ALTITUDE", out, re.MULTILINE) - assert re.search(r"POS_ALT_MSL", out, re.MULTILINE) - assert re.search(r"POS_BATTERY", out, re.MULTILINE) + assert re.search(r"ALTITUDE", out, re.MULTILINE) + assert re.search(r"ALTITUDE_MSL", out, re.MULTILINE) + assert re.search(r"HEADING", out, re.MULTILINE) assert return_value == 0 + time.sleep(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -93,6 +111,7 @@ def test_smoke1_test_with_arg_but_no_hardware(): return_value, out = subprocess.getstatusoutput("meshtastic --test") assert re.search(r"^Warning: Must have at least two devices", out, re.MULTILINE) assert return_value == 1 + time.sleep(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -102,46 +121,19 @@ def test_smoke1_debug(): assert re.search(r"^Owner", out, re.MULTILINE) assert re.search(r"^DEBUG file", out, re.MULTILINE) assert return_value == 0 + time.sleep(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 def test_smoke1_seriallog_to_file(): """Test --seriallog to a file creates a file""" - filename = "tmpoutput.txt" - if os.path.exists(f"{filename}"): - os.remove(f"{filename}") - return_value, _ = subprocess.getstatusoutput( - f"meshtastic --info --seriallog {filename}" - ) - assert os.path.exists(f"{filename}") - assert return_value == 0 - os.remove(f"{filename}") - - -@pytest.mark.smoke1 -def test_smoke1_qr(): - """Test --qr""" - filename = "tmpqr" - if os.path.exists(f"{filename}"): - os.remove(f"{filename}") - return_value, _ = subprocess.getstatusoutput(f"meshtastic --qr > {filename}") - assert os.path.exists(f"{filename}") - # not really testing that a valid qr code is created, just that the file size - # is reasonably big enough for a qr code - assert os.stat(f"{filename}").st_size > 20000 - assert return_value == 0 - os.remove(f"{filename}") - - -@pytest.mark.smoke1 -def test_smoke1_nodes(): - """Test --nodes""" - return_value, out = subprocess.getstatusoutput("meshtastic --nodes") - assert re.match(r"Connected to radio", out) - if platform.system() != "Windows": - assert re.search(r" User ", out, re.MULTILINE) - assert re.search(r" 1 ", out, re.MULTILINE) - assert return_value == 0 + with tempfile.NamedTemporaryFile('w+t', encoding='utf-8', delete=True, delete_on_close=False) as f: + return_value, _ = subprocess.getstatusoutput(f"meshtastic --info --seriallog {f.name}") + f.seek(0) + data = f.read() + assert len(data) > 2000 + assert return_value == 0 + time.sleep(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -151,6 +143,7 @@ def test_smoke1_send_hello(): assert re.match(r"Connected to radio", out) assert re.search(r"^Sending text message hello to \^all", out, re.MULTILINE) assert return_value == 0 + time.sleep(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -165,6 +158,7 @@ def test_smoke1_port(): assert re.match(r"Connected to radio", out) assert re.search(r"^Owner", out, re.MULTILINE) assert return_value == 0 + time.sleep(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -185,6 +179,7 @@ def test_smoke1_set_location_info(): assert re.search(r"32.7767", out2, re.MULTILINE) assert re.search(r"-96.797", out2, re.MULTILINE) assert return_value == 0 + time.sleep(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -211,61 +206,61 @@ def test_smoke1_set_owner(): return_value, out = subprocess.getstatusoutput("meshtastic --info") assert re.search(r"Owner: Joe", out, re.MULTILINE) assert return_value == 0 - - -@pytest.mark.smoke1 -def test_smoke1_ch_set_modem_config(): - """Test --ch-set modem_config""" - return_value, out = subprocess.getstatusoutput( - "meshtastic --ch-set modem_config MedFast" - ) - assert re.search(r"Warning: Need to specify", out, re.MULTILINE) - assert return_value == 1 - # pause for the radio - time.sleep(PAUSE_AFTER_COMMAND) - return_value, out = subprocess.getstatusoutput("meshtastic --info") - assert not re.search(r"MedFast", out, re.MULTILINE) - assert return_value == 0 - # pause for the radio time.sleep(PAUSE_AFTER_COMMAND) - return_value, out = subprocess.getstatusoutput( - "meshtastic --ch-set modem_config MedFast --ch-index 0" - ) - assert re.match(r"Connected to radio", out) - assert re.search(r"^Set modem_config to MedFast", out, re.MULTILINE) - assert return_value == 0 - # pause for the radio - time.sleep(PAUSE_AFTER_REBOOT) - return_value, out = subprocess.getstatusoutput("meshtastic --info") - assert re.search(r"MedFast", out, re.MULTILINE) - assert return_value == 0 - -@pytest.mark.smoke1 -def test_smoke1_ch_values(): +# This test seems to be obsolete: interface has changed +# @pytest.mark.smoke1 +# def test_smoke1_ch_set_modem_config(): +# """Test --ch-set modem_config""" +# return_value, out = subprocess.getstatusoutput( +# "meshtastic --ch-set modem_config MedFast" +# ) +# assert re.search(r"Warning: Need to specify", out, re.MULTILINE) +# assert return_value == 1 +# # pause for the radio +# time.sleep(PAUSE_AFTER_COMMAND) +# return_value, out = subprocess.getstatusoutput("meshtastic --info") +# assert not re.search(r"MedFast", out, re.MULTILINE) +# assert return_value == 0 +# # pause for the radio +# time.sleep(PAUSE_AFTER_COMMAND) +# return_value, out = subprocess.getstatusoutput( +# "meshtastic --ch-set modem_config MedFast --ch-index 0" +# ) +# assert re.match(r"Connected to radio", out) +# assert re.search(r"^Set modem_config to MedFast", out, re.MULTILINE) +# assert return_value == 0 +# # pause for the radio +# time.sleep(PAUSE_AFTER_REBOOT) +# return_value, out = subprocess.getstatusoutput("meshtastic --info") +# assert re.search(r"MedFast", out, re.MULTILINE) +# assert return_value == 0 + + +@pytest.mark.smoke1 +def test_smoke1_ch_modem_presets(): """Test --ch-vlongslow --ch-longslow, --ch-longfast, --ch-mediumslow, --ch-mediumsfast, --ch-shortslow, and --ch-shortfast arguments """ exp = { - "--ch-vlongslow": '{ "psk": "AQ==" }', - "--ch-longslow": "LongSlow", - "--ch-longfast": "LongFast", - "--ch-medslow": "MedSlow", - "--ch-medfast": "MedFast", - "--ch-shortslow": "ShortSlow", - "--ch-shortfast": "ShortFast", + "--ch-vlongslow": 'VERY_LONG_SLOW', + "--ch-longslow": "LONG_SLOW", + "--ch-longfast": "LONG_FAST", + "--ch-medslow": "MEDIUM_SLOW", + "--ch-medfast": "MEDIUM_FAST", + "--ch-shortslow": "SHORT_SLOW", + "--ch-shortfast": "SHORT_FAST", } for key, val in exp.items(): print(key, val) return_value, out = subprocess.getstatusoutput(f"meshtastic {key}") assert re.match(r"Connected to radio", out) - assert re.search(r"Writing modified channels to device", out, re.MULTILINE) assert return_value == 0 # pause for the radio (might reboot) - time.sleep(PAUSE_AFTER_REBOOT) + time.sleep(2*PAUSE_AFTER_REBOOT) # Radio tends to stall with many LoRa changes return_value, out = subprocess.getstatusoutput("meshtastic --info") - assert re.search(val, out, re.MULTILINE) + assert re.search(f'"modemPreset":\\s*"{val}"', out, re.MULTILINE) assert return_value == 0 # pause for the radio time.sleep(PAUSE_AFTER_COMMAND) @@ -296,6 +291,7 @@ def test_smoke1_ch_set_name(): return_value, out = subprocess.getstatusoutput("meshtastic --info") assert re.search(r"MyChannel", out, re.MULTILINE) assert return_value == 0 + time.sleep(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -317,8 +313,8 @@ def test_smoke1_ch_set_downlink_and_uplink(): # pause for the radio time.sleep(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --info") - assert not re.search(r"uplinkEnabled", out, re.MULTILINE) - assert not re.search(r"downlinkEnabled", out, re.MULTILINE) + assert re.search(r'("uplinkEnabled")\s*:\s*(false)', out, re.MULTILINE) + assert re.search(r'("downlinkEnabled")\s*:\s*(false)', out, re.MULTILINE) assert return_value == 0 # pause for the radio time.sleep(PAUSE_AFTER_COMMAND) @@ -332,14 +328,16 @@ def test_smoke1_ch_set_downlink_and_uplink(): # pause for the radio time.sleep(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --info") - assert re.search(r"uplinkEnabled", out, re.MULTILINE) - assert re.search(r"downlinkEnabled", out, re.MULTILINE) + assert re.search(r'("uplinkEnabled")\s*:\s*(true)', out, re.MULTILINE) + assert re.search(r'("downlinkEnabled")\s*:\s*(true)', out, re.MULTILINE) assert return_value == 0 + time.sleep(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 def test_smoke1_ch_add_and_ch_del(): """Test --ch-add""" + setAndTestUrl("CgI6ABIPCAE4A0ADSAFQG2gBwAYB") # ensure we have only primary channel configured. return_value, out = subprocess.getstatusoutput("meshtastic --ch-add testing") assert re.search(r"Writing modified channels to device", out, re.MULTILINE) assert return_value == 0 @@ -357,12 +355,13 @@ def test_smoke1_ch_add_and_ch_del(): assert return_value == 0 # pause for the radio time.sleep(PAUSE_AFTER_REBOOT) - # make sure the secondar channel is not there + # make sure the secondary channel is not there return_value, out = subprocess.getstatusoutput("meshtastic --info") assert re.match(r"Connected to radio", out) assert not re.search(r"SECONDARY", out, re.MULTILINE) assert not re.search(r"testing", out, re.MULTILINE) assert return_value == 0 + time.sleep(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -375,8 +374,7 @@ def test_smoke1_ch_enable_and_disable(): time.sleep(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --info") assert re.match(r"Connected to radio", out) - assert re.search(r"SECONDARY", out, re.MULTILINE) - assert re.search(r"testing", out, re.MULTILINE) + assert re.search(r'(Index\s*1:\s* SECONDARY).*("name"\s*:\s*"testing")', out, re.MULTILINE) assert return_value == 0 # pause for the radio time.sleep(PAUSE_AFTER_COMMAND) @@ -393,8 +391,7 @@ def test_smoke1_ch_enable_and_disable(): time.sleep(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --info") assert re.match(r"Connected to radio", out) - assert re.search(r"DISABLED", out, re.MULTILINE) - assert re.search(r"testing", out, re.MULTILINE) + assert not re.search(r'SECONDARY', out, re.MULTILINE) assert return_value == 0 # pause for the radio time.sleep(PAUSE_AFTER_COMMAND) @@ -406,12 +403,13 @@ def test_smoke1_ch_enable_and_disable(): time.sleep(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --info") assert re.match(r"Connected to radio", out) - assert re.search(r"SECONDARY", out, re.MULTILINE) - assert re.search(r"testing", out, re.MULTILINE) + assert re.search(r'(Index\s*1:\s* SECONDARY).*("name"\s*:\s*"testing")', out, re.MULTILINE) assert return_value == 0 # pause for the radio time.sleep(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --ch-del --ch-index 1") + assert re.match(r"Connected to radio", out) + assert re.search(r"Deleting\schannel\s1", out) assert return_value == 0 # pause for the radio time.sleep(PAUSE_AFTER_COMMAND) @@ -427,8 +425,7 @@ def test_smoke1_ch_del_a_disabled_non_primary_channel(): time.sleep(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --info") assert re.match(r"Connected to radio", out) - assert re.search(r"SECONDARY", out, re.MULTILINE) - assert re.search(r"testing", out, re.MULTILINE) + assert re.search(r'(Index\s*1:\s* SECONDARY).*("name"\s*:\s*"testing")', out, re.MULTILINE) assert return_value == 0 # pause for the radio time.sleep(PAUSE_AFTER_COMMAND) @@ -437,7 +434,17 @@ def test_smoke1_ch_del_a_disabled_non_primary_channel(): assert return_value == 1 # pause for the radio time.sleep(PAUSE_AFTER_COMMAND) + return_value, out = subprocess.getstatusoutput( + "meshtastic --ch-disable --ch-index 1" + ) + assert return_value == 0 + return_value, out = subprocess.getstatusoutput("meshtastic --info") + assert re.match(r"Connected to radio", out) + assert not re.search(r"SECONDARY", out, re.MULTILINE) + # pause for the radio + time.sleep(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --ch-del --ch-index 1") + assert re.search(r"Deleting\schannel\s1", out) assert return_value == 0 # pause for the radio time.sleep(PAUSE_AFTER_COMMAND) @@ -573,6 +580,30 @@ def test_smoke1_ensure_ch_del_third_of_three_channels(): time.sleep(PAUSE_AFTER_COMMAND) +def checkQr(f, qrCompare: str) -> None: + """checks binary file containing url""" + f.seek(0) + qrData = f.read() + assert len(qrData) > 20000 # file containing qr does not contain enough data + qrSplit = qrData.split(b'\r\n', 3) + assert len(qrSplit) == 4 + assert re.search(qrCompare, qrSplit[1].decode('utf-8'), re.MULTILINE) + + +def setAndTestUrl(pat: str) -> None: + """transmits set-url command with pattern "pat" and then checks if has been set correctly""" + url = f"https://meshtastic.org/e/#{pat}" + return_value, out = subprocess.getstatusoutput(f"meshtastic --seturl {url}") + assert re.match(r"Connected to radio", out) + assert return_value == 0 + # pause for the radio + time.sleep(PAUSE_AFTER_COMMAND) + return_value, out = subprocess.getstatusoutput("meshtastic --info") + assert re.search(pat, out, re.MULTILINE) + assert return_value == 0 + time.sleep(PAUSE_AFTER_COMMAND) + + @pytest.mark.smoke1 def test_smoke1_seturl_default(): """Test --seturl with default value""" @@ -585,17 +616,9 @@ def test_smoke1_seturl_default(): time.sleep(PAUSE_AFTER_COMMAND) # ensure we no longer have a default primary channel return_value, out = subprocess.getstatusoutput("meshtastic --info") - assert not re.search("CgUYAyIBAQ", out, re.MULTILINE) - assert return_value == 0 - url = "https://www.meshtastic.org/d/#CgUYAyIBAQ" - return_value, out = subprocess.getstatusoutput(f"meshtastic --seturl {url}") - assert re.match(r"Connected to radio", out) - assert return_value == 0 - # pause for the radio - time.sleep(PAUSE_AFTER_COMMAND) - return_value, out = subprocess.getstatusoutput("meshtastic --info") - assert re.search("CgUYAyIBAQ", out, re.MULTILINE) + assert not re.search("CgI6ABIPCAE4A0ADSAFQG2gBwAYB", out, re.MULTILINE) assert return_value == 0 + setAndTestUrl("CgI6ABIPCAE4A0ADSAFQG2gBwAYB") @pytest.mark.smoke1 @@ -611,23 +634,82 @@ def test_smoke1_seturl_invalid_url(): time.sleep(PAUSE_AFTER_COMMAND) +@pytest.mark.smoke1 +def test_smoke1_seturl_2chan(): + """Test --seturl with 2 channels""" + pat = "CgcSAQE6AggNCjASIOKjX3f5UXnz8zkcXi6MxfIsnNof5sUAW4FQQi_IXsLdGgRUZXN0KAEwAToCCBESDwgBOANAA0gBUBtoAcAGAQ" + setAndTestUrl(pat) + # check qr output + with tempfile.NamedTemporaryFile('w+b', delete=True, delete_on_close=False) as f: + return_value, _ = subprocess.getstatusoutput(f"meshtastic --qr-all >{f.name}") + assert return_value == 0 + checkQr(f, pat) + # reset to standard url + setAndTestUrl("CgI6ABIPCAE4A0ADSAFQG2gBwAYB") + + +@pytest.mark.smoke1 +def test_smoke1_seturl_3_to_2_chan(): + """Test --seturl with 3 channels, then go back to 1 channel""" + pat = "CgcSAQE6AggNCjASIOKjX3f5UXnz8zkcXi6MxfIsnNof5sUAW4FQQi_IXsLdGgRUZXN0KAEwAToCCBESDwgBOANAA0gBUBtoAcAGAQ" + setAndTestUrl(pat) + # check qr output + with tempfile.NamedTemporaryFile('w+b', delete=True, delete_on_close=False) as f: + return_value, _ = subprocess.getstatusoutput(f"meshtastic --qr-all >{f.name}") + assert return_value == 0 + checkQr(f, pat) + # now configure 2 channels only + pat = "CgcSAQE6AggNCjASIOKjX3f5UXnz8zkcXi6MxfIsnNof5sUAW4FQQi_IXsLdGgRUZXN0KAEwAToCCBESDwgBOANAA0gBUBtoAcAGAQ" + setAndTestUrl(pat) + # check qr output + with tempfile.NamedTemporaryFile('w+b', delete=True, delete_on_close=False) as f: + return_value, _ = subprocess.getstatusoutput(f"meshtastic --qr-all >{f.name}") + assert return_value == 0 + checkQr(f, pat) + # reset to standard url + setAndTestUrl("CgI6ABIPCAE4A0ADSAFQG2gBwAYB") + +@pytest.mark.smoke1 +def test_smoke1_qr(): + """Test --qr""" + # reset to standard url + pat = "CgI6ABIPCAE4A0ADSAFQG2gBwAYB" + setAndTestUrl(pat) + with tempfile.NamedTemporaryFile('w+b', delete=True, delete_on_close=False) as f: + return_value, _ = subprocess.getstatusoutput(f"meshtastic --qr >{f.name}") + assert return_value == 0 + checkQr(f, pat) + time.sleep(PAUSE_AFTER_COMMAND) + + @pytest.mark.smoke1 def test_smoke1_configure(): """Test --configure""" + if not Path('example_config.yaml').exists(): + pytest.fail(f"Cannot access config: actual path: {Path.cwd()}. Execute tests from base folder.") + _, out = subprocess.getstatusoutput(f"meshtastic --configure example_config.yaml") - assert re.match(r"Connected to radio", out) - assert re.search("^Setting device owner to Bob TBeam", out, re.MULTILINE) - assert re.search("^Fixing altitude at 304 meters", out, re.MULTILINE) - assert re.search("^Fixing latitude at 35.8", out, re.MULTILINE) - assert re.search("^Fixing longitude at -93.8", out, re.MULTILINE) - assert re.search("^Setting device position", out, re.MULTILINE) - assert re.search("^Set region to 1", out, re.MULTILINE) - assert re.search("^Set is_always_powered to true", out, re.MULTILINE) - assert re.search("^Set screen_on_secs to 31536000", out, re.MULTILINE) - assert re.search("^Set wait_bluetooth_secs to 31536000", out, re.MULTILINE) - assert re.search("^Writing modified preferences to device", out, re.MULTILINE) - # pause for the radio - time.sleep(PAUSE_AFTER_REBOOT) + assert re.search("Connected to radio", out) + assert re.search("^Setting owner properties: Bob TBeam - BOB - True", out, re.MULTILINE) + assert re.search("^Setting channel url to https://www.meshtastic.org/e/#CgI6ABIPCAE4AUADSAFQHmgBwAYB", out, re.MULTILINE) + assert re.search("^Setting fixed device position to lat 35.88888 lon -93.88888 alt 304", out, re.MULTILINE) + assert re.search("^Set lora.region to US", out, re.MULTILINE) + assert re.search("^Set display.screen_on_secs to 781", out, re.MULTILINE) + assert re.search("^Set power.wait_bluetooth_secs to 344", out, re.MULTILINE) + assert re.search("^Committing modified configuration to device", out, re.MULTILINE) + # pause for the radio + time.sleep(2*PAUSE_AFTER_REBOOT) + return_value, out = subprocess.getstatusoutput("meshtastic --info") + assert re.search('Bob TBeam', out, re.MULTILINE) + assert re.search('"latitude": 35.8', out, re.MULTILINE) + assert re.search('"longitude": -93.8', out, re.MULTILINE) + assert re.search('"fixedPosition": true', out, re.MULTILINE) + assert re.search('"region": "US"', out, re.MULTILINE) + assert re.search('"screenOnSecs": 781', out, re.MULTILINE) + assert re.search('"waitBluetoothSecs": 344', out, re.MULTILINE) + assert re.search("CgI6ABIPCAE4AUADSAFQHmgBwAYB", out, re.MULTILINE) + assert return_value == 0 + time.sleep(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -643,37 +725,39 @@ def test_smoke1_set_ham(): return_value, out = subprocess.getstatusoutput("meshtastic --info") assert re.search(r"Owner: KI1234", out, re.MULTILINE) assert return_value == 0 + time.sleep(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 def test_smoke1_set_wifi_settings(): """Test --set wifi_ssid and --set wifi_password""" return_value, out = subprocess.getstatusoutput( - 'meshtastic --set wifi_ssid "some_ssid" --set wifi_password "temp1234"' + 'meshtastic --set network.wifi_ssid "some_ssid" --set network.wifi_psk "temp1234"' ) assert re.match(r"Connected to radio", out) - assert re.search(r"^Set wifi_ssid to some_ssid", out, re.MULTILINE) - assert re.search(r"^Set wifi_password to temp1234", out, re.MULTILINE) + assert re.search(r"^Set network.wifi_ssid to some_ssid", out, re.MULTILINE) + assert re.search(r"^Set network.wifi_psk to temp1234", out, re.MULTILINE) assert return_value == 0 # pause for the radio - time.sleep(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_REBOOT) return_value, out = subprocess.getstatusoutput( - "meshtastic --get wifi_ssid --get wifi_password" + "meshtastic --get network.wifi_ssid --get network.wifi_psk" ) - assert re.search(r"^wifi_ssid: some_ssid", out, re.MULTILINE) - assert re.search(r"^wifi_password: sekrit", out, re.MULTILINE) + assert re.search(r"^network.wifi_ssid:\s*some_ssid", out, re.MULTILINE) + assert re.search(r"^network.wifi_psk:\s*temp1234", out, re.MULTILINE) assert return_value == 0 + time.sleep(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 def test_smoke1_factory_reset(): """Test factory reset""" return_value, out = subprocess.getstatusoutput( - "meshtastic --set factory_reset true" + "meshtastic --factory-reset" ) - assert re.match(r"Connected to radio", out) - assert re.search(r"^Set factory_reset to true", out, re.MULTILINE) - assert re.search(r"^Writing modified preferences to device", out, re.MULTILINE) + assert re.search("Connected to radio", out) + assert re.search(r"(factory reset).+config\sreset", out, re.MULTILINE) assert return_value == 0 # NOTE: The radio may not be responsive after this, may need to do a manual reboot # by pressing the button + time.sleep(PAUSE_AFTER_REBOOT) From d1dbfec242a9f585c5b8a451e974b15028fbeb90 Mon Sep 17 00:00:00 2001 From: karadoc Date: Thu, 6 Nov 2025 10:32:47 +0100 Subject: [PATCH 05/12] modifying waiting routine after execution of command/reboot so it works also with WSL using serial line. (You have to auto-attach serial within WSL USB Manager) --- meshtastic/tests/test_smoke1.py | 199 +++++++++++++++----------------- 1 file changed, 91 insertions(+), 108 deletions(-) diff --git a/meshtastic/tests/test_smoke1.py b/meshtastic/tests/test_smoke1.py index 5cd40c58a..7b75f42c4 100644 --- a/meshtastic/tests/test_smoke1.py +++ b/meshtastic/tests/test_smoke1.py @@ -16,18 +16,30 @@ # seconds to pause after running a meshtastic command PAUSE_AFTER_COMMAND = 2 PAUSE_AFTER_REBOOT = 10 +WAIT_FOR_REBOOT = -1 -@pytest.mark.smoke1 +def waitFor(EventOrTime: int) -> None: + if EventOrTime > 0: + time.sleep(EventOrTime) + elif EventOrTime == WAIT_FOR_REBOOT: + while True: + time.sleep(PAUSE_AFTER_REBOOT) + return_value, out = subprocess.getstatusoutput("meshtastic --device-metadata") + if return_value == 0 and re.search("firmware_version", out, re.MULTILINE) is not None: + break + + +@pytest.mark.smoke1T def test_smoke1_reboot(): """Test reboot""" return_value, _ = subprocess.getstatusoutput("meshtastic --reboot") assert return_value == 0 # pause for the radio to reset (10 seconds for the pause, and a few more seconds to be back up) - time.sleep(2*PAUSE_AFTER_REBOOT) + waitFor(WAIT_FOR_REBOOT) -@pytest.mark.smoke1 +@pytest.mark.smoke1T def test_smoke1_info(): """Test --info""" return_value, out = subprocess.getstatusoutput("meshtastic --info") @@ -40,10 +52,10 @@ def test_smoke1_info(): assert re.search(r"^\s*Index 0: PRIMARY", out, re.MULTILINE) assert re.search(r"^Primary channel URL", out, re.MULTILINE) assert return_value == 0 - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) -@pytest.mark.smoke1 +@pytest.mark.smoke1T def test_smoke1_nodes(): """Test --nodes""" return_value, out = subprocess.getstatusoutput('meshtastic --nodes --fmt json') @@ -51,7 +63,7 @@ def test_smoke1_nodes(): assert re.search(r"N.+User", out, re.MULTILINE) assert re.search(r'"N": 1, "User":', out, re.MULTILINE) assert return_value == 0 - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -60,7 +72,7 @@ def test_get_with_invalid_setting(): return_value, out = subprocess.getstatusoutput("meshtastic --get a_bad_setting") assert re.search(r"Choices are...", out) assert return_value == 0 - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -69,7 +81,7 @@ def test_set_with_invalid_setting(): return_value, out = subprocess.getstatusoutput("meshtastic --set a_bad_setting foo") assert re.search(r"Choices are...", out) assert return_value == 0 - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -80,7 +92,7 @@ def test_ch_set_with_invalid_setting(): ) assert re.search(r"Choices are...", out) assert return_value == 0 - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -100,7 +112,7 @@ def test_smoke1_pos_fields(): assert re.search(r"ALTITUDE_MSL", out, re.MULTILINE) assert re.search(r"HEADING", out, re.MULTILINE) assert return_value == 0 - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -111,7 +123,7 @@ def test_smoke1_test_with_arg_but_no_hardware(): return_value, out = subprocess.getstatusoutput("meshtastic --test") assert re.search(r"^Warning: Must have at least two devices", out, re.MULTILINE) assert return_value == 1 - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -121,7 +133,7 @@ def test_smoke1_debug(): assert re.search(r"^Owner", out, re.MULTILINE) assert re.search(r"^DEBUG file", out, re.MULTILINE) assert return_value == 0 - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -133,7 +145,7 @@ def test_smoke1_seriallog_to_file(): data = f.read() assert len(data) > 2000 assert return_value == 0 - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -143,7 +155,7 @@ def test_smoke1_send_hello(): assert re.match(r"Connected to radio", out) assert re.search(r"^Sending text message hello to \^all", out, re.MULTILINE) assert return_value == 0 - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -158,7 +170,7 @@ def test_smoke1_port(): assert re.match(r"Connected to radio", out) assert re.search(r"^Owner", out, re.MULTILINE) assert return_value == 0 - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -173,13 +185,13 @@ def test_smoke1_set_location_info(): assert re.search(r"^Fixing longitude", out, re.MULTILINE) assert return_value == 0 # pause for the radio - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) return_value, out2 = subprocess.getstatusoutput("meshtastic --info") assert re.search(r"1337", out2, re.MULTILINE) assert re.search(r"32.7767", out2, re.MULTILINE) assert re.search(r"-96.797", out2, re.MULTILINE) assert return_value == 0 - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -191,53 +203,24 @@ def test_smoke1_set_owner(): assert re.search(r"^Setting device owner to Bob", out, re.MULTILINE) assert return_value == 0 # pause for the radio - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --info") assert not re.search(r"Owner: Joe", out, re.MULTILINE) assert return_value == 0 # pause for the radio - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --set-owner Joe") assert re.match(r"Connected to radio", out) assert re.search(r"^Setting device owner to Joe", out, re.MULTILINE) assert return_value == 0 # pause for the radio - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --info") assert re.search(r"Owner: Joe", out, re.MULTILINE) assert return_value == 0 - time.sleep(PAUSE_AFTER_COMMAND) - -# This test seems to be obsolete: interface has changed -# @pytest.mark.smoke1 -# def test_smoke1_ch_set_modem_config(): -# """Test --ch-set modem_config""" -# return_value, out = subprocess.getstatusoutput( -# "meshtastic --ch-set modem_config MedFast" -# ) -# assert re.search(r"Warning: Need to specify", out, re.MULTILINE) -# assert return_value == 1 -# # pause for the radio -# time.sleep(PAUSE_AFTER_COMMAND) -# return_value, out = subprocess.getstatusoutput("meshtastic --info") -# assert not re.search(r"MedFast", out, re.MULTILINE) -# assert return_value == 0 -# # pause for the radio -# time.sleep(PAUSE_AFTER_COMMAND) -# return_value, out = subprocess.getstatusoutput( -# "meshtastic --ch-set modem_config MedFast --ch-index 0" -# ) -# assert re.match(r"Connected to radio", out) -# assert re.search(r"^Set modem_config to MedFast", out, re.MULTILINE) -# assert return_value == 0 -# # pause for the radio -# time.sleep(PAUSE_AFTER_REBOOT) -# return_value, out = subprocess.getstatusoutput("meshtastic --info") -# assert re.search(r"MedFast", out, re.MULTILINE) -# assert return_value == 0 - + waitFor(PAUSE_AFTER_COMMAND) -@pytest.mark.smoke1 +@pytest.mark.smoke1T def test_smoke1_ch_modem_presets(): """Test --ch-vlongslow --ch-longslow, --ch-longfast, --ch-mediumslow, --ch-mediumsfast, --ch-shortslow, and --ch-shortfast arguments @@ -258,12 +241,12 @@ def test_smoke1_ch_modem_presets(): assert re.match(r"Connected to radio", out) assert return_value == 0 # pause for the radio (might reboot) - time.sleep(2*PAUSE_AFTER_REBOOT) # Radio tends to stall with many LoRa changes + waitFor(WAIT_FOR_REBOOT) # Radio tends to stall with many LoRa changes return_value, out = subprocess.getstatusoutput("meshtastic --info") assert re.search(f'"modemPreset":\\s*"{val}"', out, re.MULTILINE) assert return_value == 0 # pause for the radio - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -273,13 +256,13 @@ def test_smoke1_ch_set_name(): assert not re.search(r"MyChannel", out, re.MULTILINE) assert return_value == 0 # pause for the radio - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --ch-set name MyChannel") assert re.match(r"Connected to radio", out) assert re.search(r"Warning: Need to specify", out, re.MULTILINE) assert return_value == 1 # pause for the radio - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput( "meshtastic --ch-set name MyChannel --ch-index 0" ) @@ -287,11 +270,11 @@ def test_smoke1_ch_set_name(): assert re.search(r"^Set name to MyChannel", out, re.MULTILINE) assert return_value == 0 # pause for the radio - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --info") assert re.search(r"MyChannel", out, re.MULTILINE) assert return_value == 0 - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -304,20 +287,20 @@ def test_smoke1_ch_set_downlink_and_uplink(): assert re.search(r"Warning: Need to specify", out, re.MULTILINE) assert return_value == 1 # pause for the radio - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput( "meshtastic --ch-set downlink_enabled false --ch-set uplink_enabled false --ch-index 0" ) assert re.match(r"Connected to radio", out) assert return_value == 0 # pause for the radio - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --info") assert re.search(r'("uplinkEnabled")\s*:\s*(false)', out, re.MULTILINE) assert re.search(r'("downlinkEnabled")\s*:\s*(false)', out, re.MULTILINE) assert return_value == 0 # pause for the radio - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput( "meshtastic --ch-set downlink_enabled true --ch-set uplink_enabled true --ch-index 0" ) @@ -326,12 +309,12 @@ def test_smoke1_ch_set_downlink_and_uplink(): assert re.search(r"^Set uplink_enabled to true", out, re.MULTILINE) assert return_value == 0 # pause for the radio - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --info") assert re.search(r'("uplinkEnabled")\s*:\s*(true)', out, re.MULTILINE) assert re.search(r'("downlinkEnabled")\s*:\s*(true)', out, re.MULTILINE) assert return_value == 0 - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -342,26 +325,26 @@ def test_smoke1_ch_add_and_ch_del(): assert re.search(r"Writing modified channels to device", out, re.MULTILINE) assert return_value == 0 # pause for the radio - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --info") assert re.match(r"Connected to radio", out) assert re.search(r"SECONDARY", out, re.MULTILINE) assert re.search(r"testing", out, re.MULTILINE) assert return_value == 0 # pause for the radio - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --ch-index 1 --ch-del") assert re.search(r"Deleting channel 1", out, re.MULTILINE) assert return_value == 0 # pause for the radio - time.sleep(PAUSE_AFTER_REBOOT) + waitFor(WAIT_FOR_REBOOT) # make sure the secondary channel is not there return_value, out = subprocess.getstatusoutput("meshtastic --info") assert re.match(r"Connected to radio", out) assert not re.search(r"SECONDARY", out, re.MULTILINE) assert not re.search(r"testing", out, re.MULTILINE) assert return_value == 0 - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -371,48 +354,48 @@ def test_smoke1_ch_enable_and_disable(): assert re.search(r"Writing modified channels to device", out, re.MULTILINE) assert return_value == 0 # pause for the radio - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --info") assert re.match(r"Connected to radio", out) assert re.search(r'(Index\s*1:\s* SECONDARY).*("name"\s*:\s*"testing")', out, re.MULTILINE) assert return_value == 0 # pause for the radio - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) # ensure they need to specify a --ch-index return_value, out = subprocess.getstatusoutput("meshtastic --ch-disable") assert return_value == 1 # pause for the radio - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput( "meshtastic --ch-disable --ch-index 1" ) assert return_value == 0 # pause for the radio - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --info") assert re.match(r"Connected to radio", out) assert not re.search(r'SECONDARY', out, re.MULTILINE) assert return_value == 0 # pause for the radio - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput( "meshtastic --ch-enable --ch-index 1" ) assert return_value == 0 # pause for the radio - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --info") assert re.match(r"Connected to radio", out) assert re.search(r'(Index\s*1:\s* SECONDARY).*("name"\s*:\s*"testing")', out, re.MULTILINE) assert return_value == 0 # pause for the radio - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --ch-del --ch-index 1") assert re.match(r"Connected to radio", out) assert re.search(r"Deleting\schannel\s1", out) assert return_value == 0 # pause for the radio - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -422,18 +405,18 @@ def test_smoke1_ch_del_a_disabled_non_primary_channel(): assert re.search(r"Writing modified channels to device", out, re.MULTILINE) assert return_value == 0 # pause for the radio - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --info") assert re.match(r"Connected to radio", out) assert re.search(r'(Index\s*1:\s* SECONDARY).*("name"\s*:\s*"testing")', out, re.MULTILINE) assert return_value == 0 # pause for the radio - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) # ensure they need to specify a --ch-index return_value, out = subprocess.getstatusoutput("meshtastic --ch-disable") assert return_value == 1 # pause for the radio - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput( "meshtastic --ch-disable --ch-index 1" ) @@ -442,12 +425,12 @@ def test_smoke1_ch_del_a_disabled_non_primary_channel(): assert re.match(r"Connected to radio", out) assert not re.search(r"SECONDARY", out, re.MULTILINE) # pause for the radio - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --ch-del --ch-index 1") assert re.search(r"Deleting\schannel\s1", out) assert return_value == 0 # pause for the radio - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --info") assert re.match(r"Connected to radio", out) assert not re.search(r"DISABLED", out, re.MULTILINE) @@ -455,7 +438,7 @@ def test_smoke1_ch_del_a_disabled_non_primary_channel(): assert not re.search(r"testing", out, re.MULTILINE) assert return_value == 0 # pause for the radio - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -465,7 +448,7 @@ def test_smoke1_attempt_to_delete_primary_channel(): assert re.search(r"Warning: Cannot delete primary channel", out, re.MULTILINE) assert return_value == 1 # pause for the radio - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -477,7 +460,7 @@ def test_smoke1_attempt_to_disable_primary_channel(): assert re.search(r"Warning: Cannot enable", out, re.MULTILINE) assert return_value == 1 # pause for the radio - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -489,7 +472,7 @@ def test_smoke1_attempt_to_enable_primary_channel(): assert re.search(r"Warning: Cannot enable", out, re.MULTILINE) assert return_value == 1 # pause for the radio - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -499,41 +482,41 @@ def test_smoke1_ensure_ch_del_second_of_three_channels(): assert re.match(r"Connected to radio", out) assert return_value == 0 # pause for the radio - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --info") assert re.match(r"Connected to radio", out) assert re.search(r"SECONDARY", out, re.MULTILINE) assert re.search(r"testing1", out, re.MULTILINE) assert return_value == 0 # pause for the radio - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --ch-add testing2") assert re.match(r"Connected to radio", out) assert return_value == 0 # pause for the radio - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --info") assert re.match(r"Connected to radio", out) assert re.search(r"testing2", out, re.MULTILINE) assert return_value == 0 # pause for the radio - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --ch-del --ch-index 1") assert re.match(r"Connected to radio", out) assert return_value == 0 # pause for the radio - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --info") assert re.match(r"Connected to radio", out) assert re.search(r"testing2", out, re.MULTILINE) assert return_value == 0 # pause for the radio - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --ch-del --ch-index 1") assert re.match(r"Connected to radio", out) assert return_value == 0 # pause for the radio - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -543,41 +526,41 @@ def test_smoke1_ensure_ch_del_third_of_three_channels(): assert re.match(r"Connected to radio", out) assert return_value == 0 # pause for the radio - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --info") assert re.match(r"Connected to radio", out) assert re.search(r"SECONDARY", out, re.MULTILINE) assert re.search(r"testing1", out, re.MULTILINE) assert return_value == 0 # pause for the radio - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --ch-add testing2") assert re.match(r"Connected to radio", out) assert return_value == 0 # pause for the radio - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --info") assert re.match(r"Connected to radio", out) assert re.search(r"testing2", out, re.MULTILINE) assert return_value == 0 # pause for the radio - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --ch-del --ch-index 2") assert re.match(r"Connected to radio", out) assert return_value == 0 # pause for the radio - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --info") assert re.match(r"Connected to radio", out) assert re.search(r"testing1", out, re.MULTILINE) assert return_value == 0 # pause for the radio - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --ch-del --ch-index 1") assert re.match(r"Connected to radio", out) assert return_value == 0 # pause for the radio - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) def checkQr(f, qrCompare: str) -> None: @@ -597,11 +580,11 @@ def setAndTestUrl(pat: str) -> None: assert re.match(r"Connected to radio", out) assert return_value == 0 # pause for the radio - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --info") assert re.search(pat, out, re.MULTILINE) assert return_value == 0 - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -613,7 +596,7 @@ def test_smoke1_seturl_default(): ) assert return_value == 0 # pause for the radio - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) # ensure we no longer have a default primary channel return_value, out = subprocess.getstatusoutput("meshtastic --info") assert not re.search("CgI6ABIPCAE4A0ADSAFQG2gBwAYB", out, re.MULTILINE) @@ -631,7 +614,7 @@ def test_smoke1_seturl_invalid_url(): assert re.search("Warning: There were no settings", out, re.MULTILINE) assert return_value == 1 # pause for the radio - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -679,7 +662,7 @@ def test_smoke1_qr(): return_value, _ = subprocess.getstatusoutput(f"meshtastic --qr >{f.name}") assert return_value == 0 checkQr(f, pat) - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -709,7 +692,7 @@ def test_smoke1_configure(): assert re.search('"waitBluetoothSecs": 344', out, re.MULTILINE) assert re.search("CgI6ABIPCAE4AUADSAFQHmgBwAYB", out, re.MULTILINE) assert return_value == 0 - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -721,11 +704,11 @@ def test_smoke1_set_ham(): assert re.search(r"Setting Ham ID", out, re.MULTILINE) assert return_value == 0 # pause for the radio - time.sleep(PAUSE_AFTER_REBOOT) + waitFor(WAIT_FOR_REBOOT) return_value, out = subprocess.getstatusoutput("meshtastic --info") assert re.search(r"Owner: KI1234", out, re.MULTILINE) assert return_value == 0 - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -739,14 +722,14 @@ def test_smoke1_set_wifi_settings(): assert re.search(r"^Set network.wifi_psk to temp1234", out, re.MULTILINE) assert return_value == 0 # pause for the radio - time.sleep(PAUSE_AFTER_REBOOT) + waitFor(WAIT_FOR_REBOOT) return_value, out = subprocess.getstatusoutput( "meshtastic --get network.wifi_ssid --get network.wifi_psk" ) assert re.search(r"^network.wifi_ssid:\s*some_ssid", out, re.MULTILINE) assert re.search(r"^network.wifi_psk:\s*temp1234", out, re.MULTILINE) assert return_value == 0 - time.sleep(PAUSE_AFTER_COMMAND) + waitFor(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -760,4 +743,4 @@ def test_smoke1_factory_reset(): assert return_value == 0 # NOTE: The radio may not be responsive after this, may need to do a manual reboot # by pressing the button - time.sleep(PAUSE_AFTER_REBOOT) + waitFor(WAIT_FOR_REBOOT) From d7ee851988dadddcdfd192317934e8c54981e25d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?j=C3=BCrgenRe?= Date: Sun, 9 Nov 2025 11:56:03 +0100 Subject: [PATCH 06/12] Reverting smoke test to prior commit, changes are only needed for Linux testing within WSL. We should have no special approach when testing on Linux. Moving send text message to a later point and using a private channel to not disturb everybody. --- meshtastic/tests/test_smoke1.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/meshtastic/tests/test_smoke1.py b/meshtastic/tests/test_smoke1.py index 7b75f42c4..4f500d30c 100644 --- a/meshtastic/tests/test_smoke1.py +++ b/meshtastic/tests/test_smoke1.py @@ -30,7 +30,7 @@ def waitFor(EventOrTime: int) -> None: break -@pytest.mark.smoke1T +@pytest.mark.smoke1 def test_smoke1_reboot(): """Test reboot""" return_value, _ = subprocess.getstatusoutput("meshtastic --reboot") @@ -39,7 +39,7 @@ def test_smoke1_reboot(): waitFor(WAIT_FOR_REBOOT) -@pytest.mark.smoke1T +@pytest.mark.smoke1 def test_smoke1_info(): """Test --info""" return_value, out = subprocess.getstatusoutput("meshtastic --info") @@ -55,7 +55,7 @@ def test_smoke1_info(): waitFor(PAUSE_AFTER_COMMAND) -@pytest.mark.smoke1T +@pytest.mark.smoke1 def test_smoke1_nodes(): """Test --nodes""" return_value, out = subprocess.getstatusoutput('meshtastic --nodes --fmt json') @@ -148,16 +148,6 @@ def test_smoke1_seriallog_to_file(): waitFor(PAUSE_AFTER_COMMAND) -@pytest.mark.smoke1 -def test_smoke1_send_hello(): - """Test --sendtext hello""" - return_value, out = subprocess.getstatusoutput("meshtastic --sendtext hello") - assert re.match(r"Connected to radio", out) - assert re.search(r"^Sending text message hello to \^all", out, re.MULTILINE) - assert return_value == 0 - waitFor(PAUSE_AFTER_COMMAND) - - @pytest.mark.smoke1 def test_smoke1_port(): """Test --port""" @@ -220,7 +210,7 @@ def test_smoke1_set_owner(): assert return_value == 0 waitFor(PAUSE_AFTER_COMMAND) -@pytest.mark.smoke1T +@pytest.mark.smoke1 def test_smoke1_ch_modem_presets(): """Test --ch-vlongslow --ch-longslow, --ch-longfast, --ch-mediumslow, --ch-mediumsfast, --ch-shortslow, and --ch-shortfast arguments @@ -631,9 +621,19 @@ def test_smoke1_seturl_2chan(): setAndTestUrl("CgI6ABIPCAE4A0ADSAFQG2gBwAYB") +@pytest.mark.smoke1 +def test_smoke1_send_hello(): + """Test --sendtext hello, use channel 1 to not bother other participants with testing messages""" + return_value, out = subprocess.getstatusoutput('meshtastic --sendtext "hello from smoke test" --ch-index 1') + assert re.match(r"Connected to radio", out) + assert re.search(r"^Sending text message hello from smoke test to \^all on channelIndex:1", out, re.MULTILINE) + assert return_value == 0 + waitFor(PAUSE_AFTER_COMMAND) + + @pytest.mark.smoke1 def test_smoke1_seturl_3_to_2_chan(): - """Test --seturl with 3 channels, then go back to 1 channel""" + """Test --seturl with 3 channels, then reconfigure back to 1 channel""" pat = "CgcSAQE6AggNCjASIOKjX3f5UXnz8zkcXi6MxfIsnNof5sUAW4FQQi_IXsLdGgRUZXN0KAEwAToCCBESDwgBOANAA0gBUBtoAcAGAQ" setAndTestUrl(pat) # check qr output From cae8d2e46de6197cb0e42431d3213eec93f0e64c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?j=C3=BCrgenRe?= Date: Mon, 10 Nov 2025 14:33:56 +0100 Subject: [PATCH 07/12] revert on smoke1 did not work as expected, correcting stuff here --- meshtastic/tests/test_smoke1.py | 162 +++++++++++++++----------------- 1 file changed, 75 insertions(+), 87 deletions(-) diff --git a/meshtastic/tests/test_smoke1.py b/meshtastic/tests/test_smoke1.py index 4f500d30c..1e36cd7cd 100644 --- a/meshtastic/tests/test_smoke1.py +++ b/meshtastic/tests/test_smoke1.py @@ -16,27 +16,15 @@ # seconds to pause after running a meshtastic command PAUSE_AFTER_COMMAND = 2 PAUSE_AFTER_REBOOT = 10 -WAIT_FOR_REBOOT = -1 -def waitFor(EventOrTime: int) -> None: - if EventOrTime > 0: - time.sleep(EventOrTime) - elif EventOrTime == WAIT_FOR_REBOOT: - while True: - time.sleep(PAUSE_AFTER_REBOOT) - return_value, out = subprocess.getstatusoutput("meshtastic --device-metadata") - if return_value == 0 and re.search("firmware_version", out, re.MULTILINE) is not None: - break - - @pytest.mark.smoke1 def test_smoke1_reboot(): """Test reboot""" return_value, _ = subprocess.getstatusoutput("meshtastic --reboot") assert return_value == 0 # pause for the radio to reset (10 seconds for the pause, and a few more seconds to be back up) - waitFor(WAIT_FOR_REBOOT) + time.sleep(2*PAUSE_AFTER_REBOOT) @pytest.mark.smoke1 @@ -52,7 +40,7 @@ def test_smoke1_info(): assert re.search(r"^\s*Index 0: PRIMARY", out, re.MULTILINE) assert re.search(r"^Primary channel URL", out, re.MULTILINE) assert return_value == 0 - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -63,7 +51,7 @@ def test_smoke1_nodes(): assert re.search(r"N.+User", out, re.MULTILINE) assert re.search(r'"N": 1, "User":', out, re.MULTILINE) assert return_value == 0 - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -72,7 +60,7 @@ def test_get_with_invalid_setting(): return_value, out = subprocess.getstatusoutput("meshtastic --get a_bad_setting") assert re.search(r"Choices are...", out) assert return_value == 0 - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -81,7 +69,7 @@ def test_set_with_invalid_setting(): return_value, out = subprocess.getstatusoutput("meshtastic --set a_bad_setting foo") assert re.search(r"Choices are...", out) assert return_value == 0 - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -92,7 +80,7 @@ def test_ch_set_with_invalid_setting(): ) assert re.search(r"Choices are...", out) assert return_value == 0 - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -112,7 +100,7 @@ def test_smoke1_pos_fields(): assert re.search(r"ALTITUDE_MSL", out, re.MULTILINE) assert re.search(r"HEADING", out, re.MULTILINE) assert return_value == 0 - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -123,7 +111,7 @@ def test_smoke1_test_with_arg_but_no_hardware(): return_value, out = subprocess.getstatusoutput("meshtastic --test") assert re.search(r"^Warning: Must have at least two devices", out, re.MULTILINE) assert return_value == 1 - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -133,7 +121,7 @@ def test_smoke1_debug(): assert re.search(r"^Owner", out, re.MULTILINE) assert re.search(r"^DEBUG file", out, re.MULTILINE) assert return_value == 0 - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -145,7 +133,7 @@ def test_smoke1_seriallog_to_file(): data = f.read() assert len(data) > 2000 assert return_value == 0 - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -160,7 +148,7 @@ def test_smoke1_port(): assert re.match(r"Connected to radio", out) assert re.search(r"^Owner", out, re.MULTILINE) assert return_value == 0 - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -175,13 +163,13 @@ def test_smoke1_set_location_info(): assert re.search(r"^Fixing longitude", out, re.MULTILINE) assert return_value == 0 # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) return_value, out2 = subprocess.getstatusoutput("meshtastic --info") assert re.search(r"1337", out2, re.MULTILINE) assert re.search(r"32.7767", out2, re.MULTILINE) assert re.search(r"-96.797", out2, re.MULTILINE) assert return_value == 0 - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -193,22 +181,22 @@ def test_smoke1_set_owner(): assert re.search(r"^Setting device owner to Bob", out, re.MULTILINE) assert return_value == 0 # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --info") assert not re.search(r"Owner: Joe", out, re.MULTILINE) assert return_value == 0 # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --set-owner Joe") assert re.match(r"Connected to radio", out) assert re.search(r"^Setting device owner to Joe", out, re.MULTILINE) assert return_value == 0 # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --info") assert re.search(r"Owner: Joe", out, re.MULTILINE) assert return_value == 0 - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 def test_smoke1_ch_modem_presets(): @@ -231,12 +219,12 @@ def test_smoke1_ch_modem_presets(): assert re.match(r"Connected to radio", out) assert return_value == 0 # pause for the radio (might reboot) - waitFor(WAIT_FOR_REBOOT) # Radio tends to stall with many LoRa changes + time.sleep(2*PAUSE_AFTER_REBOOT) # Radio tends to stall with many LoRa changes return_value, out = subprocess.getstatusoutput("meshtastic --info") assert re.search(f'"modemPreset":\\s*"{val}"', out, re.MULTILINE) assert return_value == 0 # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -246,13 +234,13 @@ def test_smoke1_ch_set_name(): assert not re.search(r"MyChannel", out, re.MULTILINE) assert return_value == 0 # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --ch-set name MyChannel") assert re.match(r"Connected to radio", out) assert re.search(r"Warning: Need to specify", out, re.MULTILINE) assert return_value == 1 # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput( "meshtastic --ch-set name MyChannel --ch-index 0" ) @@ -260,11 +248,11 @@ def test_smoke1_ch_set_name(): assert re.search(r"^Set name to MyChannel", out, re.MULTILINE) assert return_value == 0 # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --info") assert re.search(r"MyChannel", out, re.MULTILINE) assert return_value == 0 - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -277,20 +265,20 @@ def test_smoke1_ch_set_downlink_and_uplink(): assert re.search(r"Warning: Need to specify", out, re.MULTILINE) assert return_value == 1 # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput( "meshtastic --ch-set downlink_enabled false --ch-set uplink_enabled false --ch-index 0" ) assert re.match(r"Connected to radio", out) assert return_value == 0 # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --info") assert re.search(r'("uplinkEnabled")\s*:\s*(false)', out, re.MULTILINE) assert re.search(r'("downlinkEnabled")\s*:\s*(false)', out, re.MULTILINE) assert return_value == 0 # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput( "meshtastic --ch-set downlink_enabled true --ch-set uplink_enabled true --ch-index 0" ) @@ -299,12 +287,12 @@ def test_smoke1_ch_set_downlink_and_uplink(): assert re.search(r"^Set uplink_enabled to true", out, re.MULTILINE) assert return_value == 0 # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --info") assert re.search(r'("uplinkEnabled")\s*:\s*(true)', out, re.MULTILINE) assert re.search(r'("downlinkEnabled")\s*:\s*(true)', out, re.MULTILINE) assert return_value == 0 - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -315,26 +303,26 @@ def test_smoke1_ch_add_and_ch_del(): assert re.search(r"Writing modified channels to device", out, re.MULTILINE) assert return_value == 0 # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --info") assert re.match(r"Connected to radio", out) assert re.search(r"SECONDARY", out, re.MULTILINE) assert re.search(r"testing", out, re.MULTILINE) assert return_value == 0 # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --ch-index 1 --ch-del") assert re.search(r"Deleting channel 1", out, re.MULTILINE) assert return_value == 0 # pause for the radio - waitFor(WAIT_FOR_REBOOT) + time.sleep(PAUSE_AFTER_REBOOT) # make sure the secondary channel is not there return_value, out = subprocess.getstatusoutput("meshtastic --info") assert re.match(r"Connected to radio", out) assert not re.search(r"SECONDARY", out, re.MULTILINE) assert not re.search(r"testing", out, re.MULTILINE) assert return_value == 0 - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -344,48 +332,48 @@ def test_smoke1_ch_enable_and_disable(): assert re.search(r"Writing modified channels to device", out, re.MULTILINE) assert return_value == 0 # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --info") assert re.match(r"Connected to radio", out) assert re.search(r'(Index\s*1:\s* SECONDARY).*("name"\s*:\s*"testing")', out, re.MULTILINE) assert return_value == 0 # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) # ensure they need to specify a --ch-index return_value, out = subprocess.getstatusoutput("meshtastic --ch-disable") assert return_value == 1 # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput( "meshtastic --ch-disable --ch-index 1" ) assert return_value == 0 # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --info") assert re.match(r"Connected to radio", out) assert not re.search(r'SECONDARY', out, re.MULTILINE) assert return_value == 0 # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput( "meshtastic --ch-enable --ch-index 1" ) assert return_value == 0 # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --info") assert re.match(r"Connected to radio", out) assert re.search(r'(Index\s*1:\s* SECONDARY).*("name"\s*:\s*"testing")', out, re.MULTILINE) assert return_value == 0 # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --ch-del --ch-index 1") assert re.match(r"Connected to radio", out) assert re.search(r"Deleting\schannel\s1", out) assert return_value == 0 # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -395,18 +383,18 @@ def test_smoke1_ch_del_a_disabled_non_primary_channel(): assert re.search(r"Writing modified channels to device", out, re.MULTILINE) assert return_value == 0 # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --info") assert re.match(r"Connected to radio", out) assert re.search(r'(Index\s*1:\s* SECONDARY).*("name"\s*:\s*"testing")', out, re.MULTILINE) assert return_value == 0 # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) # ensure they need to specify a --ch-index return_value, out = subprocess.getstatusoutput("meshtastic --ch-disable") assert return_value == 1 # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput( "meshtastic --ch-disable --ch-index 1" ) @@ -415,12 +403,12 @@ def test_smoke1_ch_del_a_disabled_non_primary_channel(): assert re.match(r"Connected to radio", out) assert not re.search(r"SECONDARY", out, re.MULTILINE) # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --ch-del --ch-index 1") assert re.search(r"Deleting\schannel\s1", out) assert return_value == 0 # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --info") assert re.match(r"Connected to radio", out) assert not re.search(r"DISABLED", out, re.MULTILINE) @@ -428,7 +416,7 @@ def test_smoke1_ch_del_a_disabled_non_primary_channel(): assert not re.search(r"testing", out, re.MULTILINE) assert return_value == 0 # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -438,7 +426,7 @@ def test_smoke1_attempt_to_delete_primary_channel(): assert re.search(r"Warning: Cannot delete primary channel", out, re.MULTILINE) assert return_value == 1 # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -450,7 +438,7 @@ def test_smoke1_attempt_to_disable_primary_channel(): assert re.search(r"Warning: Cannot enable", out, re.MULTILINE) assert return_value == 1 # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -462,7 +450,7 @@ def test_smoke1_attempt_to_enable_primary_channel(): assert re.search(r"Warning: Cannot enable", out, re.MULTILINE) assert return_value == 1 # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -472,41 +460,41 @@ def test_smoke1_ensure_ch_del_second_of_three_channels(): assert re.match(r"Connected to radio", out) assert return_value == 0 # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --info") assert re.match(r"Connected to radio", out) assert re.search(r"SECONDARY", out, re.MULTILINE) assert re.search(r"testing1", out, re.MULTILINE) assert return_value == 0 # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --ch-add testing2") assert re.match(r"Connected to radio", out) assert return_value == 0 # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --info") assert re.match(r"Connected to radio", out) assert re.search(r"testing2", out, re.MULTILINE) assert return_value == 0 # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --ch-del --ch-index 1") assert re.match(r"Connected to radio", out) assert return_value == 0 # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --info") assert re.match(r"Connected to radio", out) assert re.search(r"testing2", out, re.MULTILINE) assert return_value == 0 # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --ch-del --ch-index 1") assert re.match(r"Connected to radio", out) assert return_value == 0 # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -516,41 +504,41 @@ def test_smoke1_ensure_ch_del_third_of_three_channels(): assert re.match(r"Connected to radio", out) assert return_value == 0 # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --info") assert re.match(r"Connected to radio", out) assert re.search(r"SECONDARY", out, re.MULTILINE) assert re.search(r"testing1", out, re.MULTILINE) assert return_value == 0 # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --ch-add testing2") assert re.match(r"Connected to radio", out) assert return_value == 0 # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --info") assert re.match(r"Connected to radio", out) assert re.search(r"testing2", out, re.MULTILINE) assert return_value == 0 # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --ch-del --ch-index 2") assert re.match(r"Connected to radio", out) assert return_value == 0 # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --info") assert re.match(r"Connected to radio", out) assert re.search(r"testing1", out, re.MULTILINE) assert return_value == 0 # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --ch-del --ch-index 1") assert re.match(r"Connected to radio", out) assert return_value == 0 # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) def checkQr(f, qrCompare: str) -> None: @@ -570,11 +558,11 @@ def setAndTestUrl(pat: str) -> None: assert re.match(r"Connected to radio", out) assert return_value == 0 # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) return_value, out = subprocess.getstatusoutput("meshtastic --info") assert re.search(pat, out, re.MULTILINE) assert return_value == 0 - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -586,7 +574,7 @@ def test_smoke1_seturl_default(): ) assert return_value == 0 # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) # ensure we no longer have a default primary channel return_value, out = subprocess.getstatusoutput("meshtastic --info") assert not re.search("CgI6ABIPCAE4A0ADSAFQG2gBwAYB", out, re.MULTILINE) @@ -604,7 +592,7 @@ def test_smoke1_seturl_invalid_url(): assert re.search("Warning: There were no settings", out, re.MULTILINE) assert return_value == 1 # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -628,7 +616,7 @@ def test_smoke1_send_hello(): assert re.match(r"Connected to radio", out) assert re.search(r"^Sending text message hello from smoke test to \^all on channelIndex:1", out, re.MULTILINE) assert return_value == 0 - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -662,7 +650,7 @@ def test_smoke1_qr(): return_value, _ = subprocess.getstatusoutput(f"meshtastic --qr >{f.name}") assert return_value == 0 checkQr(f, pat) - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -692,7 +680,7 @@ def test_smoke1_configure(): assert re.search('"waitBluetoothSecs": 344', out, re.MULTILINE) assert re.search("CgI6ABIPCAE4AUADSAFQHmgBwAYB", out, re.MULTILINE) assert return_value == 0 - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -704,11 +692,11 @@ def test_smoke1_set_ham(): assert re.search(r"Setting Ham ID", out, re.MULTILINE) assert return_value == 0 # pause for the radio - waitFor(WAIT_FOR_REBOOT) + time.sleep(PAUSE_AFTER_REBOOT) return_value, out = subprocess.getstatusoutput("meshtastic --info") assert re.search(r"Owner: KI1234", out, re.MULTILINE) assert return_value == 0 - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -722,14 +710,14 @@ def test_smoke1_set_wifi_settings(): assert re.search(r"^Set network.wifi_psk to temp1234", out, re.MULTILINE) assert return_value == 0 # pause for the radio - waitFor(WAIT_FOR_REBOOT) + time.sleep(PAUSE_AFTER_REBOOT) return_value, out = subprocess.getstatusoutput( "meshtastic --get network.wifi_ssid --get network.wifi_psk" ) assert re.search(r"^network.wifi_ssid:\s*some_ssid", out, re.MULTILINE) assert re.search(r"^network.wifi_psk:\s*temp1234", out, re.MULTILINE) assert return_value == 0 - waitFor(PAUSE_AFTER_COMMAND) + time.sleep(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 @@ -743,4 +731,4 @@ def test_smoke1_factory_reset(): assert return_value == 0 # NOTE: The radio may not be responsive after this, may need to do a manual reboot # by pressing the button - waitFor(WAIT_FOR_REBOOT) + time.sleep(PAUSE_AFTER_REBOOT) From 757e708f49adc0732c7c85ca79cfb228c28303dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?j=C3=BCrgenRe?= Date: Sun, 16 Nov 2025 13:24:24 +0100 Subject: [PATCH 08/12] Getting smoke test finally running on RAK and Heltec devices. Added addional checks and waiting times to ensure that RAK can sustain those many requests and will able to finish internal operations. Improving logging: add capability to log into file, add timestamp of call and the command itself and visually clear separations of call to CLI. Deleting channels in the mid had some strange effects, added work-around to handle this case correctly. However, this might have side-effects when using deprecated admin channel. To be checked further! --- example_config.yaml | 2 +- meshtastic/__main__.py | 62 +++- meshtastic/node.py | 68 ++-- meshtastic/tests/ch_reset_config.yaml | 2 + meshtastic/tests/test_smoke1.py | 466 +++++++++++++++----------- 5 files changed, 368 insertions(+), 232 deletions(-) create mode 100644 meshtastic/tests/ch_reset_config.yaml diff --git a/example_config.yaml b/example_config.yaml index 7cb3880cd..aff184ec0 100644 --- a/example_config.yaml +++ b/example_config.yaml @@ -3,7 +3,7 @@ owner: Bob TBeam owner_short: BOB is_unmessagable: true -channel_url: https://www.meshtastic.org/e/#CgI6ABIPCAE4AUADSAFQHmgBwAYB +channel_url: https://www.meshtastic.org/e/#CgQ6AggNEg8IATgBQANIAVAeaAHABgE 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 diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index f48cec430..859ccdf1a 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -21,6 +21,7 @@ import platform import sys import time +import datetime as dt try: import pyqrcode # type: ignore[import-untyped] @@ -289,6 +290,7 @@ def setPref(config, comp_name, raw_val) -> bool: def onConnected(interface): """Callback invoked when we connect to a radio""" + logger.debug("--- onConnected Entry") closeNow = False # Should we drop the connection after we finish? waitForAckNak = ( False # Should we wait for an acknowledgment if we send to a remote node? @@ -691,6 +693,7 @@ def entryToStr(entry) -> Union[str | None]: def writeSectionConfig(newConfig: dict, actConfig: dict, actNode: meshtastic.node.Node): """Traverses a configuration dict and writes each part to the node""" for sectionName, sectionElements in newConfig.items(): + logger.debug(f"writeSectionConfig for {sectionName} on node {actNode}") traverseConfig(sectionName, sectionElements, actConfig) actNode.writeConfig(meshtastic.util.camel_to_snake(sectionName)) time.sleep(0.5) @@ -712,22 +715,26 @@ def writeSectionConfig(newConfig: dict, actConfig: dict, actNode: meshtastic.nod ownerShortName = entryToStr(configuration.get("owner_short", None)) isUnmessagable = configuration.get("is_unmessagable", None) print(f"Setting owner properties: {ownerName} - {ownerShortName} - {isUnmessagable}") + logger.debug(f"Setting owner properties: {ownerName} - {ownerShortName} - {isUnmessagable}") actualNode.setOwner(long_name=ownerName, short_name=ownerShortName, is_unmessagable=isUnmessagable) time.sleep(0.5) if "channel_url" in configuration: - print("Setting channel url to", configuration["channel_url"]) + print(f"Setting channel url to {configuration.get("channel_url","---")}") + logger.debug(f"Setting channel url to {configuration.get("channel_url","---")}") actualNode.deleteAllSecondaryChannels() actualNode.setURL(configuration["channel_url"]) time.sleep(0.5) if "canned_messages" in configuration: - print("Setting canned message messages to", configuration["canned_messages"]) + print(f"Setting canned message messages to {configuration.get("canned_messages", "---")}") + logger.debug(f"Setting canned message messages to {configuration.get("canned_messages", "---")}") actualNode.set_canned_message(configuration["canned_messages"]) time.sleep(0.5) if "ringtone" in configuration: - print("Setting ringtone to", configuration["ringtone"]) + print(f"Setting ringtone to {configuration.get("ringtone", "---")}") + logger.debug(f"Setting ringtone to {configuration.get("ringtone", "---")}") actualNode.set_ringtone(configuration["ringtone"]) time.sleep(0.5) @@ -739,6 +746,7 @@ def writeSectionConfig(newConfig: dict, actConfig: dict, actNode: meshtastic.nod lat = float(configuration["location"].get("lat", 0.0)) lon = float(configuration["location"].get("lon", 0.0)) print(f"Setting fixed device position to lat {lat} lon {lon} alt {alt}") + logger.debug(f"Setting fixed device position to lat {lat} lon {lon} alt {alt}") interface.localNode.setFixedPosition(lat, lon, alt) time.sleep(0.5) @@ -749,8 +757,10 @@ def writeSectionConfig(newConfig: dict, actConfig: dict, actNode: meshtastic.nod writeSectionConfig(configuration["module_config"], actualNode.moduleConfig, actualNode) print("Committing modified configuration to device") + logger.debug("Committing modified configuration to device") actualNode.commitSettingsTransaction() - print("Configuration finished") + print(f"Configuration finished [{args.configure[0]}]") + logger.debug(f"Configuration finished [{args.configure[0]}]") if args.export_config: if args.dest != BROADCAST_ADDR: @@ -813,7 +823,7 @@ def writeSectionConfig(newConfig: dict, actConfig: dict, actNode: meshtastic.nod n.writeChannel(ch.index) if channelIndex is None: print( - f"Setting newly-added channel's {ch.index} as '--ch-index' for further modifications" + f"Setting newly-added channel {ch.index} as '--ch-index' for further modifications" ) mt_config.channel_index = ch.index @@ -870,6 +880,10 @@ def setSimpleConfig(modem_preset): if args.ch_shortfast: setSimpleConfig(config_pb2.Config.LoRaConfig.ModemPreset.SHORT_FAST) + if args.ch_info: + node = interface.getNode(args.dest, **getNode_kwargs) + node.showChannels(True) + if args.ch_set or args.ch_enable or args.ch_disable: closeNow = True @@ -1060,6 +1074,7 @@ def setSimpleConfig(modem_preset): print(f"Waiting {args.wait_to_disconnect} seconds before disconnecting") time.sleep(int(args.wait_to_disconnect)) + logger.debug("--- onConnected: Leaving") # if the user didn't ask for serial debugging output, we might want to exit after we've done our operation if (not args.seriallog) and closeNow: interface.close() # after running command then exit @@ -1069,6 +1084,7 @@ def setSimpleConfig(modem_preset): log_set.close() except Exception as ex: + logger.debug(f"Aborting due to: {ex}") print(f"Aborting due to: {ex}") interface.close() # close the connection now, so that our app exits sys.exit(1) @@ -1254,10 +1270,15 @@ def common(): logfile = None args = mt_config.args parser = mt_config.parser - logging.basicConfig( - level=logging.DEBUG if (args.debug or args.listen) else logging.INFO, - format="%(levelname)s file:%(filename)s %(funcName)s line:%(lineno)s %(message)s", - ) + logConfig = { + "level": logging.DEBUG if (args.debug or args.listen) else logging.INFO, + "format": "%(levelname)s file:%(filename)s %(funcName)s line:%(lineno)s %(message)s" + } + if args.logTo != "stdout": + logConfig["filename"] = args.logTo # define a file handle to log to + logConfig["encoding"] = "utf-8" + + logging.basicConfig(**logConfig) # set all meshtastic loggers to DEBUG if not (args.debug or args.listen) and args.debuglib: @@ -1271,6 +1292,8 @@ def common(): meshtastic.util.support_info() meshtastic.util.our_exit("", 0) + ts = dt.datetime.now().isoformat() + logger.debug(f"=== CMD: [{ts}] meshtastic {' '.join(sys.argv[1:])}") # Early validation for owner names before attempting device connection if hasattr(args, 'set_owner') and args.set_owner is not None: stripped_long_name = args.set_owner.strip() @@ -1431,6 +1454,7 @@ def common(): except KeyboardInterrupt: logger.info("Exiting due to keyboard interrupt") + logger.debug(f"--- Leaving...\n{60*'='}\n\n") # don't call exit, background threads might be running still # sys.exit(0) @@ -1737,6 +1761,15 @@ def addChannelConfigArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentPa default=False, ) + # ToDo: just for easier debug, to be removed later + group.add_argument( + "--ch-info", + help="List all channels", + action="store_true", + dest="ch_info", + default=False, + ) + return parser def addPositionConfigArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentParser: @@ -2040,7 +2073,16 @@ def initParser(): ) group.add_argument( - "--debug", help="Show API library debug log messages", action="store_true" + "--debug", + help="Show API library debug log messages'", + action="store_true" + ) + + group.add_argument( + "--logTo", + help="Defines where to log messages. Default is stdout", + default="stdout", + type=str ) group.add_argument( diff --git a/meshtastic/node.py b/meshtastic/node.py index dddfc7013..c561febc3 100644 --- a/meshtastic/node.py +++ b/meshtastic/node.py @@ -62,17 +62,17 @@ def module_available(self, excluded_bit: int) -> bool: except Exception: return True - def showChannels(self): - """Show human readable description of our channels.""" + def showChannels(self, fullDisplay=False): + """Show human readable description of our channels, which can also easily parsed by machines""" print("Channels:") if self.channels: logger.debug(f"self.channels:{self.channels}") for c in self.channels: cStr = message_to_json(c.settings) # don't show disabled channels - if channel_pb2.Channel.Role.Name(c.role) != "DISABLED": + if fullDisplay or channel_pb2.Channel.Role.Name(c.role) != "DISABLED": print( - f" Index {c.index}: {channel_pb2.Channel.Role.Name(c.role)} psk={pskToString(c.settings.psk)} {cStr}" + f" Index {c.index}: {channel_pb2.Channel.Role.Name(c.role)}, psk: {pskToString(c.settings.psk)}, settings: {cStr}" ) publicURL = self.getURL(includeAll=False) adminURL = self.getURL(includeAll=True) @@ -262,21 +262,25 @@ def getChannelByChannelIndex(self, channelIndex): def rewriteChannelList(self, idxStart: int, admIndex: int) -> None: """write back current channels list to device starting from index idxStart - channels before this index have not changed, so we don't need to rewrite.""" - index = idxStart - while index < 8: - self.writeChannel(index, adminIndex=admIndex) - index += 1 - - # if we are updating the local node, we might end up - # *moving* the admin channel index as we are writing - if (self.iface.localNode == self) and index >= admIndex: - # We've now passed the old location for admin index - # (and written it), so we can start finding it by name again - admIndex = 0 + channels (before idxStart channels have not changed, so we don't need to rewrite those).""" + + for idx in range(len(self.channels)-1, idxStart-1, -1): + self.writeChannel(idx) + + # index = idxStart + # while index < 8: + # self.writeChannel(index, adminIndex=admIndex) + # index += 1 + # + # # if we are updating the local node, we might end up + # # *moving* the admin channel index as we are writing + # if (self.iface.localNode == self) and index >= admIndex: + # # We've now passed the old location for admin index + # # (and written it), so we can start finding it by name again + # admIndex = 0 def deleteAllSecondaryChannels(self) -> None: - """Remove all secondary or disbled channels in order to be able to rewrite channel config during + """Remove all secondary or disabled channels in order to be able to rewrite channel config during configuration, only keep primary channel""" if self.channels: adminIndex = self.iface.localNode._getAdminChannelIndex() @@ -284,6 +288,7 @@ def deleteAllSecondaryChannels(self) -> None: idx2Delete = [c.index for c in self.channels if c.role == channel_pb2.Channel.Role.SECONDARY and not c.index == adminIndex ] + logger.debug(f"Deleting secondary channels. Idx found: {idx2Delete}") if len(idx2Delete) > 0: idx2Delete.reverse() for idx in idx2Delete: @@ -432,8 +437,8 @@ def setURL(self, url: str, addOnly: bool = False): print(f"Adding new channel '{chs.name}' to device") self.writeChannel(ch.index) else: - i = 0 - for chs in channelSet.settings: + self.deleteAllSecondaryChannels() # first clean up all channels before setting the new ones. + for i, chs in enumerate(channelSet.settings): ch = channel_pb2.Channel() ch.role = ( channel_pb2.Channel.Role.PRIMARY @@ -445,7 +450,6 @@ def setURL(self, url: str, addOnly: bool = False): self.channels[ch.index] = ch logger.debug(f"Channel i:{i} ch:{ch}") self.writeChannel(ch.index) - i = i + 1 p = admin_pb2.AdminMessage() p.set_config.lora.CopyFrom(channelSet.lora_config) @@ -624,7 +628,8 @@ def reboot(self, secs: int = 10): self.ensureSessionKey() p = admin_pb2.AdminMessage() p.reboot_seconds = secs - logger.info(f"Telling node to reboot in {secs} seconds") + print(f"Telling node to reboot in {secs} seconds") + logger.debug(f"Telling node to reboot in {secs} seconds") # If sending to a remote node, wait for ACK/NAK if self == self.iface.localNode: @@ -638,7 +643,8 @@ def beginSettingsTransaction(self): self.ensureSessionKey() p = admin_pb2.AdminMessage() p.begin_edit_settings = True - logger.info(f"Telling open a transaction to edit settings") + print("Telling open a transaction to edit settings") + logger.debug("Telling open a transaction to edit settings") # If sending to a remote node, wait for ACK/NAK if self == self.iface.localNode: @@ -652,7 +658,8 @@ def commitSettingsTransaction(self): self.ensureSessionKey() p = admin_pb2.AdminMessage() p.commit_edit_settings = True - logger.info(f"Telling node to commit open transaction for editing settings") + print("Telling node to commit open transaction for editing settings") + logger.debug("Telling node to commit open transaction for editing settings") # If sending to a remote node, wait for ACK/NAK if self == self.iface.localNode: @@ -666,7 +673,8 @@ def rebootOTA(self, secs: int = 10): self.ensureSessionKey() p = admin_pb2.AdminMessage() p.reboot_ota_seconds = secs - logger.info(f"Telling node to reboot to OTA in {secs} seconds") + print(f"Telling node to reboot to OTA in {secs} seconds") + logger.debug(f"Telling node to reboot to OTA in {secs} seconds") # If sending to a remote node, wait for ACK/NAK if self == self.iface.localNode: @@ -680,7 +688,8 @@ def enterDFUMode(self): self.ensureSessionKey() p = admin_pb2.AdminMessage() p.enter_dfu_mode_request = True - logger.info(f"Telling node to enable DFU mode") + print("Telling node to enable DFU mode") + logger.debug("Telling node to enable DFU mode") # If sending to a remote node, wait for ACK/NAK if self == self.iface.localNode: @@ -694,7 +703,8 @@ def shutdown(self, secs: int = 10): self.ensureSessionKey() p = admin_pb2.AdminMessage() p.shutdown_seconds = secs - logger.info(f"Telling node to shutdown in {secs} seconds") + print(f"Telling node to shutdown in {secs} seconds") + logger.debug(f"Telling node to shutdown in {secs} seconds") # If sending to a remote node, wait for ACK/NAK if self == self.iface.localNode: @@ -720,10 +730,12 @@ def factoryReset(self, full: bool = False): p = admin_pb2.AdminMessage() if full: p.factory_reset_device = True - logger.info(f"Telling node to factory reset (full device reset)") + print("Telling node to factory reset (full device reset)") + logger.debug("Telling node to factory reset (full device reset)") else: p.factory_reset_config = True - logger.info(f"Telling node to factory reset (config reset)") + print("Telling node to factory reset (config reset)") + logger.debug("Telling node to factory reset (config reset)") # If sending to a remote node, wait for ACK/NAK if self == self.iface.localNode: diff --git a/meshtastic/tests/ch_reset_config.yaml b/meshtastic/tests/ch_reset_config.yaml new file mode 100644 index 000000000..20adf55f7 --- /dev/null +++ b/meshtastic/tests/ch_reset_config.yaml @@ -0,0 +1,2 @@ +# configuration file for resetting channel config to a known value +channel_url: https://www.meshtastic.org/e/#CgI6ABIPCAE4AUADSAFQHmgBwAYB diff --git a/meshtastic/tests/test_smoke1.py b/meshtastic/tests/test_smoke1.py index 4f500d30c..f8f21b055 100644 --- a/meshtastic/tests/test_smoke1.py +++ b/meshtastic/tests/test_smoke1.py @@ -2,7 +2,6 @@ import io import re import subprocess -import sys import time from pathlib import Path import tempfile @@ -18,22 +17,88 @@ PAUSE_AFTER_REBOOT = 10 WAIT_FOR_REBOOT = -1 - -def waitFor(EventOrTime: int) -> None: - if EventOrTime > 0: - time.sleep(EventOrTime) - elif EventOrTime == WAIT_FOR_REBOOT: +DEBUG = '--debug --logTo smoke1.log' + +# Helper functions used in executing tests +def communicate(cmd: str, repeatTimes: int = 2) -> tuple[int, str]: + """Communicate to the radio. Repeat request in case serial line is not operational""" + k = 0 + print(f'---COM: "{cmd}", r: {repeatTimes}') + while k < repeatTimes: + return_value, out = subprocess.getstatusoutput(f"{cmd} {DEBUG}") + k += 1 + + if return_value == 0 \ + and not re.search("Input/output error", out, re.MULTILINE) \ + and not re.search("MeshInterfaceError: Timed out", out, re.MULTILINE): + break + print(f"k: {k} ret: {return_value} out: {out}") + return return_value, out + +def waitFor(eventOrTime: int, repeatTimes: int = 5) -> None: + """Wait for a dedicated time (positive integer input) or for a reboot. The latter will ensure that the + serial line is back operational so we can safely send the next command.""" + print(f"---WAI {eventOrTime}") + if eventOrTime > 0: + time.sleep(eventOrTime) + elif eventOrTime == WAIT_FOR_REBOOT: + k = 0 while True: - time.sleep(PAUSE_AFTER_REBOOT) - return_value, out = subprocess.getstatusoutput("meshtastic --device-metadata") + time.sleep(2*PAUSE_AFTER_REBOOT) + return_value, out = communicate("meshtastic --device-metadata") + print(f"ret: {return_value} out: {out}") + k += 1 if return_value == 0 and re.search("firmware_version", out, re.MULTILINE) is not None: + break + if k > repeatTimes: + print("Reboot failed") break +def checkQr(f, qrCompare: str) -> None: + """checks binary file containing url""" + f.seek(0) + qrData = f.read() + assert len(qrData) > 20000 # file containing qr does not contain enough data + qrSplit = qrData.splitlines(keepends=True) + print(f"checkQr: found lines: {len(qrSplit)}") + assert len(qrSplit) >= 4 + assert re.search(qrCompare, qrSplit[1].decode('utf-8'), re.MULTILINE) + +def setAndTestUrl(pat: str) -> None: + """transmits set-url command with pattern "pat" and then checks if has been set correctly""" + url = f"https://meshtastic.org/e/#{pat}" + return_value, out = communicate(f"meshtastic --seturl {url}") + assert re.match(r"Connected to radio", out) + assert return_value == 0 + # pause for the radio + waitFor(PAUSE_AFTER_COMMAND) + return_value, out = communicate("meshtastic --info") + assert re.search(pat, out, re.MULTILINE) + assert return_value == 0 + waitFor(PAUSE_AFTER_COMMAND) + + +TEMPFILE = None + + +# Fixtures +@pytest.fixture +def temporaryCfgFile(scope="module"): + """Return a temp file valid throughout the whole test. + Purpose: store the exported data for later reconfigure""" + global TEMPFILE + if TEMPFILE is None: + TEMPFILE = tempfile.NamedTemporaryFile(mode='w+t', encoding='utf-8', delete=False) + print(f"created file {TEMPFILE.name}") + else: + open(TEMPFILE.name, 'r+t', encoding='utf-8') + yield TEMPFILE + TEMPFILE.close() @pytest.mark.smoke1 def test_smoke1_reboot(): """Test reboot""" - return_value, _ = subprocess.getstatusoutput("meshtastic --reboot") + return_value, _ = communicate("meshtastic --reboot") assert return_value == 0 # pause for the radio to reset (10 seconds for the pause, and a few more seconds to be back up) waitFor(WAIT_FOR_REBOOT) @@ -42,7 +107,7 @@ def test_smoke1_reboot(): @pytest.mark.smoke1 def test_smoke1_info(): """Test --info""" - return_value, out = subprocess.getstatusoutput("meshtastic --info") + return_value, out = communicate("meshtastic --info") assert re.match(r"Connected to radio", out) assert re.search(r"^Owner", out, re.MULTILINE) assert re.search(r"^My info", out, re.MULTILINE) @@ -55,10 +120,22 @@ def test_smoke1_info(): waitFor(PAUSE_AFTER_COMMAND) +@pytest.mark.smoke1 +def test_smoke1_export_config(temporaryCfgFile): + """Test exporting current config, then later reimport and check if things are back as before + Store this config in a temporary file to be used later""" + print(f"Got temp file: {temporaryCfgFile.name}") + return_value, out = communicate(f'meshtastic --export-config {temporaryCfgFile.name}') + temporaryCfgFile.seek(0) + print(f"ret: {return_value} out: {out}") + pat = f"Exported configuration to {temporaryCfgFile.name}".replace('\\', '\\\\') + assert re.match(pat, out) + + @pytest.mark.smoke1 def test_smoke1_nodes(): """Test --nodes""" - return_value, out = subprocess.getstatusoutput('meshtastic --nodes --fmt json') + return_value, out = communicate('meshtastic --nodes --fmt json') assert re.match(r"Connected to radio", out) assert re.search(r"N.+User", out, re.MULTILINE) assert re.search(r'"N": 1, "User":', out, re.MULTILINE) @@ -69,7 +146,7 @@ def test_smoke1_nodes(): @pytest.mark.smoke1 def test_get_with_invalid_setting(): """Test '--get a_bad_setting'.""" - return_value, out = subprocess.getstatusoutput("meshtastic --get a_bad_setting") + return_value, out = communicate("meshtastic --get a_bad_setting") assert re.search(r"Choices are...", out) assert return_value == 0 waitFor(PAUSE_AFTER_COMMAND) @@ -78,7 +155,7 @@ def test_get_with_invalid_setting(): @pytest.mark.smoke1 def test_set_with_invalid_setting(): """Test '--set a_bad_setting'.""" - return_value, out = subprocess.getstatusoutput("meshtastic --set a_bad_setting foo") + return_value, out = communicate("meshtastic --set a_bad_setting foo") assert re.search(r"Choices are...", out) assert return_value == 0 waitFor(PAUSE_AFTER_COMMAND) @@ -87,7 +164,7 @@ def test_set_with_invalid_setting(): @pytest.mark.smoke1 def test_ch_set_with_invalid_setting(): """Test '--ch-set with a_bad_setting'.""" - return_value, out = subprocess.getstatusoutput( + return_value, out = communicate( "meshtastic --ch-set invalid_setting foo --ch-index 0" ) assert re.search(r"Choices are...", out) @@ -98,15 +175,15 @@ def test_ch_set_with_invalid_setting(): @pytest.mark.smoke1 def test_smoke1_pos_fields(): """Test --pos-fields (with some values POS_ALTITUDE POS_ALT_MSL POS_BATTERY)""" - return_value, out = subprocess.getstatusoutput( + return_value, out = communicate( "meshtastic --pos-fields ALTITUDE ALTITUDE_MSL HEADING" ) assert re.match(r"Connected to radio", out) assert re.search(r"^Setting position fields to 259", out, re.MULTILINE) assert return_value == 0 # pause for the radio - time.sleep(2*PAUSE_AFTER_REBOOT) - return_value, out = subprocess.getstatusoutput("meshtastic --pos-fields") + waitFor(WAIT_FOR_REBOOT) + return_value, out = communicate("meshtastic --pos-fields") assert re.match(r"Connected to radio", out) assert re.search(r"ALTITUDE", out, re.MULTILINE) assert re.search(r"ALTITUDE_MSL", out, re.MULTILINE) @@ -120,16 +197,16 @@ def test_smoke1_test_with_arg_but_no_hardware(): """Test --test Note: Since only one device is connected, it will not do much. """ - return_value, out = subprocess.getstatusoutput("meshtastic --test") + return_value, out = communicate("meshtastic --test", repeatTimes=1) assert re.search(r"^Warning: Must have at least two devices", out, re.MULTILINE) assert return_value == 1 waitFor(PAUSE_AFTER_COMMAND) -@pytest.mark.smoke1 +@pytest.mark.smoke1D def test_smoke1_debug(): """Test --debug""" - return_value, out = subprocess.getstatusoutput("meshtastic --info --debug") + return_value, out = communicate("meshtastic --info --debug") assert re.search(r"^Owner", out, re.MULTILINE) assert re.search(r"^DEBUG file", out, re.MULTILINE) assert return_value == 0 @@ -140,7 +217,7 @@ def test_smoke1_debug(): def test_smoke1_seriallog_to_file(): """Test --seriallog to a file creates a file""" with tempfile.NamedTemporaryFile('w+t', encoding='utf-8', delete=True, delete_on_close=False) as f: - return_value, _ = subprocess.getstatusoutput(f"meshtastic --info --seriallog {f.name}") + return_value, _ = communicate(f"meshtastic --info --seriallog {f.name}") f.seek(0) data = f.read() assert len(data) > 2000 @@ -156,7 +233,7 @@ def test_smoke1_port(): # hopefully there is just one assert len(ports) == 1 port = ports[0] - return_value, out = subprocess.getstatusoutput(f"meshtastic --port {port} --info") + return_value, out = communicate(f"meshtastic --port {port} --info") assert re.match(r"Connected to radio", out) assert re.search(r"^Owner", out, re.MULTILINE) assert return_value == 0 @@ -166,7 +243,7 @@ def test_smoke1_port(): @pytest.mark.smoke1 def test_smoke1_set_location_info(): """Test --setlat, --setlon and --setalt""" - return_value, out = subprocess.getstatusoutput( + return_value, out = communicate( "meshtastic --setlat 32.7767 --setlon -96.7970 --setalt 1337" ) assert re.match(r"Connected to radio", out) @@ -176,7 +253,7 @@ def test_smoke1_set_location_info(): assert return_value == 0 # pause for the radio waitFor(PAUSE_AFTER_COMMAND) - return_value, out2 = subprocess.getstatusoutput("meshtastic --info") + return_value, out2 = communicate("meshtastic --info") assert re.search(r"1337", out2, re.MULTILINE) assert re.search(r"32.7767", out2, re.MULTILINE) assert re.search(r"-96.797", out2, re.MULTILINE) @@ -188,24 +265,24 @@ def test_smoke1_set_location_info(): def test_smoke1_set_owner(): """Test --set-owner name""" # make sure the owner is not Joe - return_value, out = subprocess.getstatusoutput("meshtastic --set-owner Bob") + return_value, out = communicate("meshtastic --set-owner Bob") assert re.match(r"Connected to radio", out) assert re.search(r"^Setting device owner to Bob", out, re.MULTILINE) assert return_value == 0 # pause for the radio waitFor(PAUSE_AFTER_COMMAND) - return_value, out = subprocess.getstatusoutput("meshtastic --info") + return_value, out = communicate("meshtastic --info") assert not re.search(r"Owner: Joe", out, re.MULTILINE) assert return_value == 0 # pause for the radio waitFor(PAUSE_AFTER_COMMAND) - return_value, out = subprocess.getstatusoutput("meshtastic --set-owner Joe") + return_value, out = communicate("meshtastic --set-owner Joe") assert re.match(r"Connected to radio", out) assert re.search(r"^Setting device owner to Joe", out, re.MULTILINE) assert return_value == 0 # pause for the radio waitFor(PAUSE_AFTER_COMMAND) - return_value, out = subprocess.getstatusoutput("meshtastic --info") + return_value, out = communicate("meshtastic --info") assert re.search(r"Owner: Joe", out, re.MULTILINE) assert return_value == 0 waitFor(PAUSE_AFTER_COMMAND) @@ -227,12 +304,12 @@ def test_smoke1_ch_modem_presets(): for key, val in exp.items(): print(key, val) - return_value, out = subprocess.getstatusoutput(f"meshtastic {key}") + return_value, out = communicate(f"meshtastic {key}") assert re.match(r"Connected to radio", out) assert return_value == 0 # pause for the radio (might reboot) waitFor(WAIT_FOR_REBOOT) # Radio tends to stall with many LoRa changes - return_value, out = subprocess.getstatusoutput("meshtastic --info") + return_value, out = communicate("meshtastic --info") assert re.search(f'"modemPreset":\\s*"{val}"', out, re.MULTILINE) assert return_value == 0 # pause for the radio @@ -242,18 +319,18 @@ def test_smoke1_ch_modem_presets(): @pytest.mark.smoke1 def test_smoke1_ch_set_name(): """Test --ch-set name""" - return_value, out = subprocess.getstatusoutput("meshtastic --info") + return_value, out = communicate("meshtastic --info") assert not re.search(r"MyChannel", out, re.MULTILINE) assert return_value == 0 # pause for the radio waitFor(PAUSE_AFTER_COMMAND) - return_value, out = subprocess.getstatusoutput("meshtastic --ch-set name MyChannel") + return_value, out = communicate("meshtastic --ch-set name MyChannel", repeatTimes=1) + assert return_value == 1 assert re.match(r"Connected to radio", out) assert re.search(r"Warning: Need to specify", out, re.MULTILINE) - assert return_value == 1 # pause for the radio waitFor(PAUSE_AFTER_COMMAND) - return_value, out = subprocess.getstatusoutput( + return_value, out = communicate( "meshtastic --ch-set name MyChannel --ch-index 0" ) assert re.match(r"Connected to radio", out) @@ -261,7 +338,7 @@ def test_smoke1_ch_set_name(): assert return_value == 0 # pause for the radio waitFor(PAUSE_AFTER_COMMAND) - return_value, out = subprocess.getstatusoutput("meshtastic --info") + return_value, out = communicate("meshtastic --info") assert re.search(r"MyChannel", out, re.MULTILINE) assert return_value == 0 waitFor(PAUSE_AFTER_COMMAND) @@ -270,28 +347,29 @@ def test_smoke1_ch_set_name(): @pytest.mark.smoke1 def test_smoke1_ch_set_downlink_and_uplink(): """Test -ch-set downlink_enabled X and --ch-set uplink_enabled X""" - return_value, out = subprocess.getstatusoutput( - "meshtastic --ch-set downlink_enabled false --ch-set uplink_enabled false" + return_value, out = communicate( + "meshtastic --ch-set downlink_enabled false --ch-set uplink_enabled false", + repeatTimes=1 ) + assert return_value == 1 assert re.match(r"Connected to radio", out) assert re.search(r"Warning: Need to specify", out, re.MULTILINE) - assert return_value == 1 # pause for the radio waitFor(PAUSE_AFTER_COMMAND) - return_value, out = subprocess.getstatusoutput( + return_value, out = communicate( "meshtastic --ch-set downlink_enabled false --ch-set uplink_enabled false --ch-index 0" ) assert re.match(r"Connected to radio", out) assert return_value == 0 # pause for the radio waitFor(PAUSE_AFTER_COMMAND) - return_value, out = subprocess.getstatusoutput("meshtastic --info") + return_value, out = communicate("meshtastic --info") assert re.search(r'("uplinkEnabled")\s*:\s*(false)', out, re.MULTILINE) assert re.search(r'("downlinkEnabled")\s*:\s*(false)', out, re.MULTILINE) assert return_value == 0 # pause for the radio waitFor(PAUSE_AFTER_COMMAND) - return_value, out = subprocess.getstatusoutput( + return_value, out = communicate( "meshtastic --ch-set downlink_enabled true --ch-set uplink_enabled true --ch-index 0" ) assert re.match(r"Connected to radio", out) @@ -300,7 +378,7 @@ def test_smoke1_ch_set_downlink_and_uplink(): assert return_value == 0 # pause for the radio waitFor(PAUSE_AFTER_COMMAND) - return_value, out = subprocess.getstatusoutput("meshtastic --info") + return_value, out = communicate("meshtastic --info") assert re.search(r'("uplinkEnabled")\s*:\s*(true)', out, re.MULTILINE) assert re.search(r'("downlinkEnabled")\s*:\s*(true)', out, re.MULTILINE) assert return_value == 0 @@ -311,28 +389,28 @@ def test_smoke1_ch_set_downlink_and_uplink(): def test_smoke1_ch_add_and_ch_del(): """Test --ch-add""" setAndTestUrl("CgI6ABIPCAE4A0ADSAFQG2gBwAYB") # ensure we have only primary channel configured. - return_value, out = subprocess.getstatusoutput("meshtastic --ch-add testing") + return_value, out = communicate("meshtastic --ch-add testing1") assert re.search(r"Writing modified channels to device", out, re.MULTILINE) assert return_value == 0 # pause for the radio waitFor(PAUSE_AFTER_COMMAND) - return_value, out = subprocess.getstatusoutput("meshtastic --info") + return_value, out = communicate("meshtastic --info") assert re.match(r"Connected to radio", out) assert re.search(r"SECONDARY", out, re.MULTILINE) - assert re.search(r"testing", out, re.MULTILINE) + assert re.search(r"testing1", out, re.MULTILINE) assert return_value == 0 # pause for the radio waitFor(PAUSE_AFTER_COMMAND) - return_value, out = subprocess.getstatusoutput("meshtastic --ch-index 1 --ch-del") + return_value, out = communicate("meshtastic --ch-index 1 --ch-del") assert re.search(r"Deleting channel 1", out, re.MULTILINE) assert return_value == 0 # pause for the radio waitFor(WAIT_FOR_REBOOT) # make sure the secondary channel is not there - return_value, out = subprocess.getstatusoutput("meshtastic --info") + return_value, out = communicate("meshtastic --info") assert re.match(r"Connected to radio", out) assert not re.search(r"SECONDARY", out, re.MULTILINE) - assert not re.search(r"testing", out, re.MULTILINE) + assert not re.search(r"testing1", out, re.MULTILINE) assert return_value == 0 waitFor(PAUSE_AFTER_COMMAND) @@ -340,47 +418,44 @@ def test_smoke1_ch_add_and_ch_del(): @pytest.mark.smoke1 def test_smoke1_ch_enable_and_disable(): """Test --ch-enable and --ch-disable""" - return_value, out = subprocess.getstatusoutput("meshtastic --ch-add testing") + setAndTestUrl("CgI6ABIPCAE4A0ADSAFQG2gBwAYB") # ensure we have only primary channel configured. + return_value, out = communicate("meshtastic --ch-add testing1") assert re.search(r"Writing modified channels to device", out, re.MULTILINE) assert return_value == 0 # pause for the radio waitFor(PAUSE_AFTER_COMMAND) - return_value, out = subprocess.getstatusoutput("meshtastic --info") + return_value, out = communicate("meshtastic --info") assert re.match(r"Connected to radio", out) - assert re.search(r'(Index\s*1:\s* SECONDARY).*("name"\s*:\s*"testing")', out, re.MULTILINE) + assert re.search(r'(Index\s*1:\s* SECONDARY).*("name"\s*:\s*"testing1")', out, re.MULTILINE) assert return_value == 0 # pause for the radio waitFor(PAUSE_AFTER_COMMAND) # ensure they need to specify a --ch-index - return_value, out = subprocess.getstatusoutput("meshtastic --ch-disable") + return_value, out = communicate("meshtastic --ch-disable", repeatTimes=1) assert return_value == 1 # pause for the radio waitFor(PAUSE_AFTER_COMMAND) - return_value, out = subprocess.getstatusoutput( - "meshtastic --ch-disable --ch-index 1" - ) + return_value, out = communicate("meshtastic --ch-disable --ch-index 1") assert return_value == 0 # pause for the radio waitFor(PAUSE_AFTER_COMMAND) - return_value, out = subprocess.getstatusoutput("meshtastic --info") + return_value, out = communicate("meshtastic --info") assert re.match(r"Connected to radio", out) assert not re.search(r'SECONDARY', out, re.MULTILINE) assert return_value == 0 # pause for the radio waitFor(PAUSE_AFTER_COMMAND) - return_value, out = subprocess.getstatusoutput( - "meshtastic --ch-enable --ch-index 1" - ) + return_value, out = communicate("meshtastic --ch-enable --ch-index 1") assert return_value == 0 # pause for the radio waitFor(PAUSE_AFTER_COMMAND) - return_value, out = subprocess.getstatusoutput("meshtastic --info") + return_value, out = communicate("meshtastic --info") assert re.match(r"Connected to radio", out) - assert re.search(r'(Index\s*1:\s* SECONDARY).*("name"\s*:\s*"testing")', out, re.MULTILINE) + assert re.search(r'(Index\s*1:\s* SECONDARY).*("name"\s*:\s*"testing1")', out, re.MULTILINE) assert return_value == 0 # pause for the radio waitFor(PAUSE_AFTER_COMMAND) - return_value, out = subprocess.getstatusoutput("meshtastic --ch-del --ch-index 1") + return_value, out = communicate("meshtastic --ch-del --ch-index 1") assert re.match(r"Connected to radio", out) assert re.search(r"Deleting\schannel\s1", out) assert return_value == 0 @@ -391,50 +466,50 @@ def test_smoke1_ch_enable_and_disable(): @pytest.mark.smoke1 def test_smoke1_ch_del_a_disabled_non_primary_channel(): """Test --ch-del will work on a disabled non-primary channel.""" - return_value, out = subprocess.getstatusoutput("meshtastic --ch-add testing") + setAndTestUrl("CgI6ABIPCAE4A0ADSAFQG2gBwAYB") # ensure we have only primary channel configured. + return_value, out = communicate("meshtastic --ch-add testing1") assert re.search(r"Writing modified channels to device", out, re.MULTILINE) assert return_value == 0 - # pause for the radio waitFor(PAUSE_AFTER_COMMAND) - return_value, out = subprocess.getstatusoutput("meshtastic --info") + + return_value, out = communicate("meshtastic --info") assert re.match(r"Connected to radio", out) - assert re.search(r'(Index\s*1:\s* SECONDARY).*("name"\s*:\s*"testing")', out, re.MULTILINE) + assert re.search(r'(Index\s*1:\s* SECONDARY).*("name"\s*:\s*"testing1")', out, re.MULTILINE) assert return_value == 0 - # pause for the radio waitFor(PAUSE_AFTER_COMMAND) + # ensure they need to specify a --ch-index - return_value, out = subprocess.getstatusoutput("meshtastic --ch-disable") + return_value, out = communicate("meshtastic --ch-disable", repeatTimes=1) assert return_value == 1 - # pause for the radio waitFor(PAUSE_AFTER_COMMAND) - return_value, out = subprocess.getstatusoutput( - "meshtastic --ch-disable --ch-index 1" - ) + + return_value, out = communicate("meshtastic --ch-disable --ch-index 1") assert return_value == 0 - return_value, out = subprocess.getstatusoutput("meshtastic --info") + waitFor(PAUSE_AFTER_COMMAND) + + return_value, out = communicate("meshtastic --info") assert re.match(r"Connected to radio", out) assert not re.search(r"SECONDARY", out, re.MULTILINE) - # pause for the radio waitFor(PAUSE_AFTER_COMMAND) - return_value, out = subprocess.getstatusoutput("meshtastic --ch-del --ch-index 1") + + return_value, out = communicate("meshtastic --ch-del --ch-index 1") assert re.search(r"Deleting\schannel\s1", out) assert return_value == 0 - # pause for the radio waitFor(PAUSE_AFTER_COMMAND) - return_value, out = subprocess.getstatusoutput("meshtastic --info") + + return_value, out = communicate("meshtastic --info") assert re.match(r"Connected to radio", out) assert not re.search(r"DISABLED", out, re.MULTILINE) assert not re.search(r"SECONDARY", out, re.MULTILINE) - assert not re.search(r"testing", out, re.MULTILINE) + assert not re.search(r"testing1", out, re.MULTILINE) assert return_value == 0 - # pause for the radio waitFor(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 def test_smoke1_attempt_to_delete_primary_channel(): """Test that we cannot delete the PRIMARY channel.""" - return_value, out = subprocess.getstatusoutput("meshtastic --ch-del --ch-index 0") + return_value, out = communicate("meshtastic --ch-del --ch-index 0", repeatTimes=1) assert re.search(r"Warning: Cannot delete primary channel", out, re.MULTILINE) assert return_value == 1 # pause for the radio @@ -444,8 +519,9 @@ def test_smoke1_attempt_to_delete_primary_channel(): @pytest.mark.smoke1 def test_smoke1_attempt_to_disable_primary_channel(): """Test that we cannot disable the PRIMARY channel.""" - return_value, out = subprocess.getstatusoutput( - "meshtastic --ch-disable --ch-index 0" + return_value, out = communicate( + "meshtastic --ch-disable --ch-index 0", + repeatTimes=1 ) assert re.search(r"Warning: Cannot enable", out, re.MULTILINE) assert return_value == 1 @@ -456,8 +532,9 @@ def test_smoke1_attempt_to_disable_primary_channel(): @pytest.mark.smoke1 def test_smoke1_attempt_to_enable_primary_channel(): """Test that we cannot enable the PRIMARY channel.""" - return_value, out = subprocess.getstatusoutput( - "meshtastic --ch-enable --ch-index 0" + return_value, out = communicate( + "meshtastic --ch-enable --ch-index 0", + repeatTimes=1 ) assert re.search(r"Warning: Cannot enable", out, re.MULTILINE) assert return_value == 1 @@ -468,112 +545,101 @@ def test_smoke1_attempt_to_enable_primary_channel(): @pytest.mark.smoke1 def test_smoke1_ensure_ch_del_second_of_three_channels(): """Test that when we delete the 2nd of 3 channels, that it deletes the correct channel.""" - return_value, out = subprocess.getstatusoutput("meshtastic --ch-add testing1") + + # prepare test setting: setup 2 channels and validate they are created + return_value, out = communicate("meshtastic --ch-info") + print(f"ret: {return_value} out: {out}") + + return_value, out = communicate("meshtastic --configure tests/ch_reset_config.yaml") + assert return_value == 0 + waitFor(WAIT_FOR_REBOOT) + + return_value, out = communicate("meshtastic --info") + print(f"ret: {return_value} out: {out}") + + return_value, out = communicate("meshtastic --ch-add testing1") + assert return_value == 0 assert re.match(r"Connected to radio", out) + waitFor(PAUSE_AFTER_COMMAND) + + return_value, out = communicate("meshtastic --ch-add testing2") assert return_value == 0 - # pause for the radio + assert re.match(r"Connected to radio", out) waitFor(PAUSE_AFTER_COMMAND) - return_value, out = subprocess.getstatusoutput("meshtastic --info") + + waitFor(WAIT_FOR_REBOOT) + return_value, out = communicate("meshtastic --info") + assert return_value == 0 assert re.match(r"Connected to radio", out) assert re.search(r"SECONDARY", out, re.MULTILINE) assert re.search(r"testing1", out, re.MULTILINE) - assert return_value == 0 - # pause for the radio + assert re.search(r"testing2", out, re.MULTILINE) waitFor(PAUSE_AFTER_COMMAND) - return_value, out = subprocess.getstatusoutput("meshtastic --ch-add testing2") - assert re.match(r"Connected to radio", out) + + # validate the first channel is deleted correctly. + # Second channel must move up to index 1 and index 2 must become disabled + return_value, out = communicate("meshtastic --ch-del --ch-index 1") assert return_value == 0 - # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) - return_value, out = subprocess.getstatusoutput("meshtastic --info") assert re.match(r"Connected to radio", out) - assert re.search(r"testing2", out, re.MULTILINE) - assert return_value == 0 - # pause for the radio + print(f"ret: {return_value} out: {out}") waitFor(PAUSE_AFTER_COMMAND) - return_value, out = subprocess.getstatusoutput("meshtastic --ch-del --ch-index 1") - assert re.match(r"Connected to radio", out) + + return_value, out = communicate("meshtastic --info") assert return_value == 0 - # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) - return_value, out = subprocess.getstatusoutput("meshtastic --info") assert re.match(r"Connected to radio", out) assert re.search(r"testing2", out, re.MULTILINE) - assert return_value == 0 - # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) - return_value, out = subprocess.getstatusoutput("meshtastic --ch-del --ch-index 1") - assert re.match(r"Connected to radio", out) - assert return_value == 0 - # pause for the radio + assert re.search(r"Index 1: SECONDARY", out, re.MULTILINE) + assert not re.search(r"Index 2", out, re.MULTILINE) + assert not re.search(r"Index 3", out, re.MULTILINE) waitFor(PAUSE_AFTER_COMMAND) @pytest.mark.smoke1 def test_smoke1_ensure_ch_del_third_of_three_channels(): """Test that when we delete the 3rd of 3 channels, that it deletes the correct channel.""" - return_value, out = subprocess.getstatusoutput("meshtastic --ch-add testing1") - assert re.match(r"Connected to radio", out) + + # prepare test setting: setup 2 channels and validate they are created + return_value, out = communicate("meshtastic --ch-info") + print(f"ret: {return_value} out: {out}") + return_value, out = communicate("meshtastic --configure tests/ch_reset_config.yaml") assert return_value == 0 - # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) - return_value, out = subprocess.getstatusoutput("meshtastic --info") - assert re.match(r"Connected to radio", out) - assert re.search(r"SECONDARY", out, re.MULTILINE) - assert re.search(r"testing1", out, re.MULTILINE) + waitFor(WAIT_FOR_REBOOT) + + return_value, out = communicate("meshtastic --ch-info") + print(f"ret: {return_value} out: {out}") + + return_value, out = communicate("meshtastic --ch-add testing1") assert return_value == 0 - # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) - return_value, out = subprocess.getstatusoutput("meshtastic --ch-add testing2") assert re.match(r"Connected to radio", out) - assert return_value == 0 - # pause for the radio waitFor(PAUSE_AFTER_COMMAND) - return_value, out = subprocess.getstatusoutput("meshtastic --info") - assert re.match(r"Connected to radio", out) - assert re.search(r"testing2", out, re.MULTILINE) + + return_value, out = communicate("meshtastic --ch-add testing2") assert return_value == 0 - # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) - return_value, out = subprocess.getstatusoutput("meshtastic --ch-del --ch-index 2") assert re.match(r"Connected to radio", out) - assert return_value == 0 - # pause for the radio waitFor(PAUSE_AFTER_COMMAND) - return_value, out = subprocess.getstatusoutput("meshtastic --info") - assert re.match(r"Connected to radio", out) - assert re.search(r"testing1", out, re.MULTILINE) + + waitFor(WAIT_FOR_REBOOT) + return_value, out = communicate("meshtastic --info", repeatTimes=2) assert return_value == 0 - # pause for the radio - waitFor(PAUSE_AFTER_COMMAND) - return_value, out = subprocess.getstatusoutput("meshtastic --ch-del --ch-index 1") assert re.match(r"Connected to radio", out) - assert return_value == 0 - # pause for the radio + assert re.search(r"SECONDARY", out, re.MULTILINE) + assert re.search(r"testing1", out, re.MULTILINE) + assert re.search(r"testing2", out, re.MULTILINE) waitFor(PAUSE_AFTER_COMMAND) - -def checkQr(f, qrCompare: str) -> None: - """checks binary file containing url""" - f.seek(0) - qrData = f.read() - assert len(qrData) > 20000 # file containing qr does not contain enough data - qrSplit = qrData.split(b'\r\n', 3) - assert len(qrSplit) == 4 - assert re.search(qrCompare, qrSplit[1].decode('utf-8'), re.MULTILINE) - - -def setAndTestUrl(pat: str) -> None: - """transmits set-url command with pattern "pat" and then checks if has been set correctly""" - url = f"https://meshtastic.org/e/#{pat}" - return_value, out = subprocess.getstatusoutput(f"meshtastic --seturl {url}") - assert re.match(r"Connected to radio", out) + # validate the second channel is deleted correctly + return_value, out = communicate("meshtastic --ch-del --ch-index 2") assert return_value == 0 - # pause for the radio + assert re.match(r"Connected to radio", out) waitFor(PAUSE_AFTER_COMMAND) - return_value, out = subprocess.getstatusoutput("meshtastic --info") - assert re.search(pat, out, re.MULTILINE) + + return_value, out = communicate("meshtastic --info", repeatTimes=2) assert return_value == 0 + assert re.match(r"Connected to radio", out) + assert re.search(r"testing1", out, re.MULTILINE) + assert re.search(r"Index 1: SECONDARY", out, re.MULTILINE) + assert not re.search(r"Index 2", out, re.MULTILINE) + assert not re.search(r"Index 3", out, re.MULTILINE) waitFor(PAUSE_AFTER_COMMAND) @@ -581,14 +647,14 @@ def setAndTestUrl(pat: str) -> None: def test_smoke1_seturl_default(): """Test --seturl with default value""" # set some channel value so we no longer have a default channel - return_value, out = subprocess.getstatusoutput( + return_value, out = communicate( "meshtastic --ch-set name foo --ch-index 0" ) assert return_value == 0 # pause for the radio waitFor(PAUSE_AFTER_COMMAND) # ensure we no longer have a default primary channel - return_value, out = subprocess.getstatusoutput("meshtastic --info") + return_value, out = communicate("meshtastic --info") assert not re.search("CgI6ABIPCAE4A0ADSAFQG2gBwAYB", out, re.MULTILINE) assert return_value == 0 setAndTestUrl("CgI6ABIPCAE4A0ADSAFQG2gBwAYB") @@ -599,7 +665,7 @@ def test_smoke1_seturl_invalid_url(): """Test --seturl with invalid url""" # Note: This url is no longer a valid url. url = "https://www.meshtastic.org/c/#GAMiENTxuzogKQdZ8Lz_q89Oab8qB0RlZmF1bHQ=" - return_value, out = subprocess.getstatusoutput(f"meshtastic --seturl {url}") + return_value, out = communicate(f"meshtastic --seturl {url}", repeatTimes=1) assert re.match(r"Connected to radio", out) assert re.search("Warning: There were no settings", out, re.MULTILINE) assert return_value == 1 @@ -614,31 +680,21 @@ def test_smoke1_seturl_2chan(): setAndTestUrl(pat) # check qr output with tempfile.NamedTemporaryFile('w+b', delete=True, delete_on_close=False) as f: - return_value, _ = subprocess.getstatusoutput(f"meshtastic --qr-all >{f.name}") + return_value, _ = communicate(f"meshtastic --qr-all >{f.name}") assert return_value == 0 checkQr(f, pat) # reset to standard url setAndTestUrl("CgI6ABIPCAE4A0ADSAFQG2gBwAYB") -@pytest.mark.smoke1 -def test_smoke1_send_hello(): - """Test --sendtext hello, use channel 1 to not bother other participants with testing messages""" - return_value, out = subprocess.getstatusoutput('meshtastic --sendtext "hello from smoke test" --ch-index 1') - assert re.match(r"Connected to radio", out) - assert re.search(r"^Sending text message hello from smoke test to \^all on channelIndex:1", out, re.MULTILINE) - assert return_value == 0 - waitFor(PAUSE_AFTER_COMMAND) - - @pytest.mark.smoke1 def test_smoke1_seturl_3_to_2_chan(): - """Test --seturl with 3 channels, then reconfigure back to 1 channel""" + """Test --seturl with 3 channels, then reconfigure back to 2 channels""" pat = "CgcSAQE6AggNCjASIOKjX3f5UXnz8zkcXi6MxfIsnNof5sUAW4FQQi_IXsLdGgRUZXN0KAEwAToCCBESDwgBOANAA0gBUBtoAcAGAQ" setAndTestUrl(pat) # check qr output with tempfile.NamedTemporaryFile('w+b', delete=True, delete_on_close=False) as f: - return_value, _ = subprocess.getstatusoutput(f"meshtastic --qr-all >{f.name}") + return_value, _ = communicate(f"meshtastic --qr-all >{f.name}") assert return_value == 0 checkQr(f, pat) # now configure 2 channels only @@ -646,11 +702,20 @@ def test_smoke1_seturl_3_to_2_chan(): setAndTestUrl(pat) # check qr output with tempfile.NamedTemporaryFile('w+b', delete=True, delete_on_close=False) as f: - return_value, _ = subprocess.getstatusoutput(f"meshtastic --qr-all >{f.name}") + return_value, _ = communicate(f"meshtastic --qr-all >{f.name}") assert return_value == 0 checkQr(f, pat) - # reset to standard url - setAndTestUrl("CgI6ABIPCAE4A0ADSAFQG2gBwAYB") + # Note: keep one secondary channel in order to send the hello to it + +@pytest.mark.smoke1 +def test_smoke1_send_hello(): + """Test --sendtext hello, use channel 1 to not bother other participants with testing messages""" + return_value, out = communicate('meshtastic --sendtext "hello from smoke test" --ch-index 1') + assert re.match(r"Connected to radio", out) + assert re.search(r"^Sending text message hello from smoke test to \^all on channelIndex:1", out, re.MULTILINE) + assert return_value == 0 + waitFor(PAUSE_AFTER_COMMAND) + @pytest.mark.smoke1 def test_smoke1_qr(): @@ -659,7 +724,7 @@ def test_smoke1_qr(): pat = "CgI6ABIPCAE4A0ADSAFQG2gBwAYB" setAndTestUrl(pat) with tempfile.NamedTemporaryFile('w+b', delete=True, delete_on_close=False) as f: - return_value, _ = subprocess.getstatusoutput(f"meshtastic --qr >{f.name}") + return_value, _ = communicate(f"meshtastic --qr >{f.name}") assert return_value == 0 checkQr(f, pat) waitFor(PAUSE_AFTER_COMMAND) @@ -668,29 +733,33 @@ def test_smoke1_qr(): @pytest.mark.smoke1 def test_smoke1_configure(): """Test --configure""" - if not Path('example_config.yaml').exists(): - pytest.fail(f"Cannot access config: actual path: {Path.cwd()}. Execute tests from base folder.") - - _, out = subprocess.getstatusoutput(f"meshtastic --configure example_config.yaml") + cfgPth = Path('example_config.yaml') + if not cfgPth.exists(): + cfgPth = Path.cwd().parent / cfgPth + if not cfgPth.exists(): + pytest.fail(f"Cannot access config: actual path: {Path.cwd()}. Execute tests from base folder.") + + _, out = communicate(f"meshtastic --configure {str(cfgPth)}") + print(f"out: {out}") assert re.search("Connected to radio", out) assert re.search("^Setting owner properties: Bob TBeam - BOB - True", out, re.MULTILINE) - assert re.search("^Setting channel url to https://www.meshtastic.org/e/#CgI6ABIPCAE4AUADSAFQHmgBwAYB", out, re.MULTILINE) + assert re.search("^Setting channel url to https://www.meshtastic.org/e/#CgQ6AggNEg8IATgBQANIAVAeaAHABgE", out, re.MULTILINE) assert re.search("^Setting fixed device position to lat 35.88888 lon -93.88888 alt 304", out, re.MULTILINE) assert re.search("^Set lora.region to US", out, re.MULTILINE) assert re.search("^Set display.screen_on_secs to 781", out, re.MULTILINE) assert re.search("^Set power.wait_bluetooth_secs to 344", out, re.MULTILINE) assert re.search("^Committing modified configuration to device", out, re.MULTILINE) # pause for the radio - time.sleep(2*PAUSE_AFTER_REBOOT) - return_value, out = subprocess.getstatusoutput("meshtastic --info") + waitFor(WAIT_FOR_REBOOT) + return_value, out = communicate("meshtastic --info", repeatTimes=2) assert re.search('Bob TBeam', out, re.MULTILINE) assert re.search('"latitude": 35.8', out, re.MULTILINE) assert re.search('"longitude": -93.8', out, re.MULTILINE) - assert re.search('"fixedPosition": true', out, re.MULTILINE) + assert re.search('"gpsMode": "ENABLED"', out, re.MULTILINE) assert re.search('"region": "US"', out, re.MULTILINE) assert re.search('"screenOnSecs": 781', out, re.MULTILINE) assert re.search('"waitBluetoothSecs": 344', out, re.MULTILINE) - assert re.search("CgI6ABIPCAE4AUADSAFQHmgBwAYB", out, re.MULTILINE) + assert re.search("CgQ6AggNEg8IATgBQANIAVAeaAHABgE", out, re.MULTILINE) assert return_value == 0 waitFor(PAUSE_AFTER_COMMAND) @@ -700,12 +769,12 @@ def test_smoke1_set_ham(): """Test --set-ham Note: Do a factory reset after this setting so it is very short-lived. """ - return_value, out = subprocess.getstatusoutput("meshtastic --set-ham KI1234") + return_value, out = communicate("meshtastic --set-ham KI1234") assert re.search(r"Setting Ham ID", out, re.MULTILINE) assert return_value == 0 # pause for the radio waitFor(WAIT_FOR_REBOOT) - return_value, out = subprocess.getstatusoutput("meshtastic --info") + return_value, out = communicate("meshtastic --info") assert re.search(r"Owner: KI1234", out, re.MULTILINE) assert return_value == 0 waitFor(PAUSE_AFTER_COMMAND) @@ -714,7 +783,7 @@ def test_smoke1_set_ham(): @pytest.mark.smoke1 def test_smoke1_set_wifi_settings(): """Test --set wifi_ssid and --set wifi_password""" - return_value, out = subprocess.getstatusoutput( + return_value, out = communicate( 'meshtastic --set network.wifi_ssid "some_ssid" --set network.wifi_psk "temp1234"' ) assert re.match(r"Connected to radio", out) @@ -723,7 +792,7 @@ def test_smoke1_set_wifi_settings(): assert return_value == 0 # pause for the radio waitFor(WAIT_FOR_REBOOT) - return_value, out = subprocess.getstatusoutput( + return_value, out = communicate( "meshtastic --get network.wifi_ssid --get network.wifi_psk" ) assert re.search(r"^network.wifi_ssid:\s*some_ssid", out, re.MULTILINE) @@ -735,12 +804,23 @@ def test_smoke1_set_wifi_settings(): @pytest.mark.smoke1 def test_smoke1_factory_reset(): """Test factory reset""" - return_value, out = subprocess.getstatusoutput( - "meshtastic --factory-reset" - ) + return_value, out = communicate("meshtastic --factory-reset") assert re.search("Connected to radio", out) assert re.search(r"(factory reset).+config\sreset", out, re.MULTILINE) assert return_value == 0 # NOTE: The radio may not be responsive after this, may need to do a manual reboot # by pressing the button waitFor(WAIT_FOR_REBOOT) + +@pytest.mark.smoke1 +def test_smoke1_config_reset(temporaryCfgFile): + """Restore original settings""" + print(f"Got temp file: {temporaryCfgFile.name}") + return_value, out = communicate(f"meshtastic --config {temporaryCfgFile.name}") + print(f"ret: {return_value} out: {out}") + assert re.search(r"Connected to radio", out) + assert re.search(r"Configuration finished", out) + + +if TEMPFILE is not None: + Path(TEMPFILE).unlink(missing_ok=True) \ No newline at end of file From 5a7817dbe43c349479ca1eaee19757bd2dc85585 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?j=C3=BCrgenRe?= Date: Mon, 17 Nov 2025 15:24:21 +0100 Subject: [PATCH 09/12] Do not delete secondary channels when setting via URL. Deleting now only happens when reading a config. Adding additional protobuf files as exclusion to linter Smoke test 1: finalizing DEBUG/VERBOSE switches. Validating URL and QR functions in the right order. Systematic setup of test conditions. --- .pylintrc | 2 +- meshtastic/node.py | 9 +- meshtastic/tests/test_smoke1.py | 183 ++++++++++++++++++++------------ 3 files changed, 125 insertions(+), 69 deletions(-) diff --git a/.pylintrc b/.pylintrc index c94cddf2f..28934b6e8 100644 --- a/.pylintrc +++ b/.pylintrc @@ -7,7 +7,7 @@ # Add files or directories matching the regex patterns to the blacklist. The # regex matches against base names, not paths. -ignore-patterns=mqtt_pb2.py,channel_pb2.py,telemetry_pb2.py,admin_pb2.py,config_pb2.py,deviceonly_pb2.py,apponly_pb2.py,remote_hardware_pb2.py,portnums_pb2.py,mesh_pb2.py,storeforward_pb2.py,cannedmessages_pb2.py,module_config_pb2.py,localonly_pb2.py,node.py,device_metadata_pb2.py,nanopb_pb2.py +ignore-patterns=xmodem_pb2.py,rtttl_pb2.py,powermon_pb2.py,paxcount_pb2.py,interdevice_pb2.py,atak_pb2,clientonly_pb2.py,connection_status_pb2.py,device_ui_pb2.py,mqtt_pb2.py,channel_pb2.py,telemetry_pb2.py,admin_pb2.py,config_pb2.py,deviceonly_pb2.py,apponly_pb2.py,remote_hardware_pb2.py,portnums_pb2.py,mesh_pb2.py,storeforward_pb2.py,cannedmessages_pb2.py,module_config_pb2.py,localonly_pb2.py,node.py,device_metadata_pb2.py,nanopb_pb2.py diff --git a/meshtastic/node.py b/meshtastic/node.py index c561febc3..6b62d6a7d 100644 --- a/meshtastic/node.py +++ b/meshtastic/node.py @@ -436,8 +436,13 @@ def setURL(self, url: str, addOnly: bool = False): ch.role = channel_pb2.Channel.Role.SECONDARY print(f"Adding new channel '{chs.name}' to device") self.writeChannel(ch.index) - else: - self.deleteAllSecondaryChannels() # first clean up all channels before setting the new ones. + else: # set new channel settings, starting from index 0. Keep previous settings otherwise + # The behavior is somewhat strange: if you load a URL, you might expect that your channel set is + # exactly as defined by the URL. So, when the URL has 2 channels and your radio has defined 5, after loading + # there should be 2 channels available but not 5 (2 newly set and 3 untouched) + # Publishing one URL for the primary channel and one for all together opens an ambiguity: the system + # cannot decide if you want to configure just one primary channel without any secondary channels or if you + # want to overwrite only the primary channel and keep the secondary channels as they are. for i, chs in enumerate(channelSet.settings): ch = channel_pb2.Channel() ch.role = ( diff --git a/meshtastic/tests/test_smoke1.py b/meshtastic/tests/test_smoke1.py index f8f21b055..2cc7b3d95 100644 --- a/meshtastic/tests/test_smoke1.py +++ b/meshtastic/tests/test_smoke1.py @@ -1,29 +1,48 @@ """Meshtastic smoke tests with a single device via USB""" -import io import re import subprocess import time from pathlib import Path import tempfile -# Do not like using hard coded sleeps, but it probably makes -# sense to pause for the radio at appropriate times import pytest from ..util import findPorts +# Do not like using hard coded sleeps, but it makes +# sense to pause for the radio at appropriate times to +# avoid overload of the radio. # seconds to pause after running a meshtastic command PAUSE_AFTER_COMMAND = 2 PAUSE_AFTER_REBOOT = 10 WAIT_FOR_REBOOT = -1 -DEBUG = '--debug --logTo smoke1.log' +""" Following 2 switches allow control creation of additional or debug messages during testing + DEBUG contains a string passed to the meshtastic call to log internal behavior + VERBOSE toggles additional print output during the command execution of smoketest functions + TEMPFILE contains the extracted settings of the radio till the tests have finished, so the + radio will not stay with wrong region settings for a longer time +""" +# DEBUG: str = '' +# VERBOSE: bool = False +DEBUG: str = '--debug --logTo smoke1.log' +VERBOSE: bool = True +TEMPFILE = None + + +def noPrint(*args): + """Dummy print function""" + pass + + +vprint = print if VERBOSE else noPrint + # Helper functions used in executing tests def communicate(cmd: str, repeatTimes: int = 2) -> tuple[int, str]: """Communicate to the radio. Repeat request in case serial line is not operational""" k = 0 - print(f'---COM: "{cmd}", r: {repeatTimes}') + vprint(f'---COM: "{cmd}", r: {repeatTimes}') while k < repeatTimes: return_value, out = subprocess.getstatusoutput(f"{cmd} {DEBUG}") k += 1 @@ -32,13 +51,13 @@ def communicate(cmd: str, repeatTimes: int = 2) -> tuple[int, str]: and not re.search("Input/output error", out, re.MULTILINE) \ and not re.search("MeshInterfaceError: Timed out", out, re.MULTILINE): break - print(f"k: {k} ret: {return_value} out: {out}") + vprint(f"k: {k} ret: {return_value} out: {out}") return return_value, out def waitFor(eventOrTime: int, repeatTimes: int = 5) -> None: """Wait for a dedicated time (positive integer input) or for a reboot. The latter will ensure that the serial line is back operational so we can safely send the next command.""" - print(f"---WAI {eventOrTime}") + vprint(f"---WAI {eventOrTime}") if eventOrTime > 0: time.sleep(eventOrTime) elif eventOrTime == WAIT_FOR_REBOOT: @@ -46,25 +65,25 @@ def waitFor(eventOrTime: int, repeatTimes: int = 5) -> None: while True: time.sleep(2*PAUSE_AFTER_REBOOT) return_value, out = communicate("meshtastic --device-metadata") - print(f"ret: {return_value} out: {out}") + vprint(f"ret: {return_value} out: {out}") k += 1 if return_value == 0 and re.search("firmware_version", out, re.MULTILINE) is not None: - break + break if k > repeatTimes: - print("Reboot failed") + vprint("Reboot failed") break - + def checkQr(f, qrCompare: str) -> None: """checks binary file containing url""" f.seek(0) qrData = f.read() assert len(qrData) > 20000 # file containing qr does not contain enough data qrSplit = qrData.splitlines(keepends=True) - print(f"checkQr: found lines: {len(qrSplit)}") + vprint(f"checkQr: found lines: {len(qrSplit)}") assert len(qrSplit) >= 4 assert re.search(qrCompare, qrSplit[1].decode('utf-8'), re.MULTILINE) -def setAndTestUrl(pat: str) -> None: +def setAndTestUrl(pat: str, skipTest: bool = False) -> None: """transmits set-url command with pattern "pat" and then checks if has been set correctly""" url = f"https://meshtastic.org/e/#{pat}" return_value, out = communicate(f"meshtastic --seturl {url}") @@ -72,13 +91,11 @@ def setAndTestUrl(pat: str) -> None: assert return_value == 0 # pause for the radio waitFor(PAUSE_AFTER_COMMAND) - return_value, out = communicate("meshtastic --info") - assert re.search(pat, out, re.MULTILINE) - assert return_value == 0 - waitFor(PAUSE_AFTER_COMMAND) - - -TEMPFILE = None + if not skipTest: + return_value, out = communicate("meshtastic --info") + assert re.search(pat, out, re.MULTILINE) + assert return_value == 0 + waitFor(PAUSE_AFTER_COMMAND) # Fixtures @@ -124,10 +141,10 @@ def test_smoke1_info(): def test_smoke1_export_config(temporaryCfgFile): """Test exporting current config, then later reimport and check if things are back as before Store this config in a temporary file to be used later""" - print(f"Got temp file: {temporaryCfgFile.name}") + vprint(f"\nGot temp file: {temporaryCfgFile.name}") return_value, out = communicate(f'meshtastic --export-config {temporaryCfgFile.name}') temporaryCfgFile.seek(0) - print(f"ret: {return_value} out: {out}") + vprint(f"ret: {return_value} out: {out}") pat = f"Exported configuration to {temporaryCfgFile.name}".replace('\\', '\\\\') assert re.match(pat, out) @@ -301,7 +318,7 @@ def test_smoke1_ch_modem_presets(): "--ch-shortslow": "SHORT_SLOW", "--ch-shortfast": "SHORT_FAST", } - + print("\n") for key, val in exp.items(): print(key, val) return_value, out = communicate(f"meshtastic {key}") @@ -547,15 +564,12 @@ def test_smoke1_ensure_ch_del_second_of_three_channels(): """Test that when we delete the 2nd of 3 channels, that it deletes the correct channel.""" # prepare test setting: setup 2 channels and validate they are created - return_value, out = communicate("meshtastic --ch-info") - print(f"ret: {return_value} out: {out}") - return_value, out = communicate("meshtastic --configure tests/ch_reset_config.yaml") assert return_value == 0 waitFor(WAIT_FOR_REBOOT) - return_value, out = communicate("meshtastic --info") - print(f"ret: {return_value} out: {out}") + return_value, out = communicate("meshtastic --ch-info") + vprint(f"ret: {return_value} out: {out}") return_value, out = communicate("meshtastic --ch-add testing1") assert return_value == 0 @@ -581,7 +595,7 @@ def test_smoke1_ensure_ch_del_second_of_three_channels(): return_value, out = communicate("meshtastic --ch-del --ch-index 1") assert return_value == 0 assert re.match(r"Connected to radio", out) - print(f"ret: {return_value} out: {out}") + vprint(f"ret: {return_value} out: {out}") waitFor(PAUSE_AFTER_COMMAND) return_value, out = communicate("meshtastic --info") @@ -599,14 +613,12 @@ def test_smoke1_ensure_ch_del_third_of_three_channels(): """Test that when we delete the 3rd of 3 channels, that it deletes the correct channel.""" # prepare test setting: setup 2 channels and validate they are created - return_value, out = communicate("meshtastic --ch-info") - print(f"ret: {return_value} out: {out}") return_value, out = communicate("meshtastic --configure tests/ch_reset_config.yaml") assert return_value == 0 waitFor(WAIT_FOR_REBOOT) return_value, out = communicate("meshtastic --ch-info") - print(f"ret: {return_value} out: {out}") + vprint(f"ret: {return_value} out: {out}") return_value, out = communicate("meshtastic --ch-add testing1") assert return_value == 0 @@ -643,9 +655,45 @@ def test_smoke1_ensure_ch_del_third_of_three_channels(): waitFor(PAUSE_AFTER_COMMAND) +@pytest.mark.smoke1 +def test_smoke1_set_primary_channel(): + """Test --seturl with primary channel""" + # prepare test setting: setup 2 channels and validate they are created + return_value, _ = communicate("meshtastic --configure tests/ch_reset_config.yaml") + assert return_value == 0 + waitFor(WAIT_FOR_REBOOT) + + # set to different url + pat = "CgcSAQE6AggNEg8IATgDQANIAVAbaAHABgE" + setAndTestUrl(pat) + + +@pytest.mark.smoke1 +def test_smoke1_qr(): + """Test --qr, based on setting via URL""" + # prepare test setting: setup 2 channels and validate they are created + return_value, _ = communicate("meshtastic --configure tests/ch_reset_config.yaml") + assert return_value == 0 + waitFor(WAIT_FOR_REBOOT) + + # set to different url + pat = "CgcSAQE6AggNEg8IATgDQANIAVAbaAHABgE" + setAndTestUrl(pat) + with tempfile.NamedTemporaryFile('w+b', delete=True, delete_on_close=False) as f: + return_value, _ = communicate(f"meshtastic --qr >{f.name}") + assert return_value == 0 + checkQr(f, pat) + waitFor(PAUSE_AFTER_COMMAND) + + @pytest.mark.smoke1 def test_smoke1_seturl_default(): """Test --seturl with default value""" + # prepare test setting: setup std channel + return_value, out = communicate("meshtastic --configure tests/ch_reset_config.yaml") + assert return_value == 0 + waitFor(WAIT_FOR_REBOOT) + # set some channel value so we no longer have a default channel return_value, out = communicate( "meshtastic --ch-set name foo --ch-index 0" @@ -657,7 +705,6 @@ def test_smoke1_seturl_default(): return_value, out = communicate("meshtastic --info") assert not re.search("CgI6ABIPCAE4A0ADSAFQG2gBwAYB", out, re.MULTILINE) assert return_value == 0 - setAndTestUrl("CgI6ABIPCAE4A0ADSAFQG2gBwAYB") @pytest.mark.smoke1 @@ -676,6 +723,11 @@ def test_smoke1_seturl_invalid_url(): @pytest.mark.smoke1 def test_smoke1_seturl_2chan(): """Test --seturl with 2 channels""" + # prepare test setting: setup 2 channels and validate they are created + return_value, _ = communicate("meshtastic --configure tests/ch_reset_config.yaml") + assert return_value == 0 + waitFor(WAIT_FOR_REBOOT) + pat = "CgcSAQE6AggNCjASIOKjX3f5UXnz8zkcXi6MxfIsnNof5sUAW4FQQi_IXsLdGgRUZXN0KAEwAToCCBESDwgBOANAA0gBUBtoAcAGAQ" setAndTestUrl(pat) # check qr output @@ -683,28 +735,40 @@ def test_smoke1_seturl_2chan(): return_value, _ = communicate(f"meshtastic --qr-all >{f.name}") assert return_value == 0 checkQr(f, pat) - # reset to standard url - setAndTestUrl("CgI6ABIPCAE4A0ADSAFQG2gBwAYB") @pytest.mark.smoke1 def test_smoke1_seturl_3_to_2_chan(): - """Test --seturl with 3 channels, then reconfigure back to 2 channels""" - pat = "CgcSAQE6AggNCjASIOKjX3f5UXnz8zkcXi6MxfIsnNof5sUAW4FQQi_IXsLdGgRUZXN0KAEwAToCCBESDwgBOANAA0gBUBtoAcAGAQ" + """Test --seturl with 3 channels, then reconfigure 2 channels""" + # prepare test setting: setup 2 channels and validate they are created + return_value, out = communicate("meshtastic --configure tests/ch_reset_config.yaml") + assert return_value == 0 + waitFor(WAIT_FOR_REBOOT) + + pat = "CgcSAQE6AggNCjESIOKjX3f5UXnz8zkcXi6MxfIsnNof5sUAW4FQQi_IXsLdGgV0ZXN0MSgBMAE6AggRCi0SIGyPI2Gbw3v6rl9H\ + Q8SL3LvRx7ScovIdU6pahs_l59CoGgV0ZXN0MigBMAESDwgBOANAA0gBUBtoAcAGAQ" setAndTestUrl(pat) - # check qr output - with tempfile.NamedTemporaryFile('w+b', delete=True, delete_on_close=False) as f: - return_value, _ = communicate(f"meshtastic --qr-all >{f.name}") - assert return_value == 0 - checkQr(f, pat) + # check that we have 3 channels + return_value, out = communicate("meshtastic --info", repeatTimes=2) + assert return_value == 0 + assert re.match(r"Connected to radio", out) + assert re.search(r"SECONDARY", out, re.MULTILINE) + assert re.search(r"test1", out, re.MULTILINE) + assert re.search(r"test2", out, re.MULTILINE) + waitFor(PAUSE_AFTER_COMMAND) + # now configure 2 channels only - pat = "CgcSAQE6AggNCjASIOKjX3f5UXnz8zkcXi6MxfIsnNof5sUAW4FQQi_IXsLdGgRUZXN0KAEwAToCCBESDwgBOANAA0gBUBtoAcAGAQ" - setAndTestUrl(pat) - # check qr output - with tempfile.NamedTemporaryFile('w+b', delete=True, delete_on_close=False) as f: - return_value, _ = communicate(f"meshtastic --qr-all >{f.name}") - assert return_value == 0 - checkQr(f, pat) + patSet = "CgcSAQE6AggNCjASIOKjX3f5UXnz8zkcXi6MxfIsnNof5sUAW4FQQi_IXsLdGgRUZXN0KAEwAToCCBESDwgBOANAA0gBUBtoAcAGAQ" + setAndTestUrl(patSet, skipTest=True) + + # now test for patComp (url will be diefferent because of not deleted channel 2) + return_value, out = communicate("meshtastic --info", repeatTimes=2) + assert return_value == 0 + assert re.match(r"Connected to radio", out) + assert re.search(r"SECONDARY", out, re.MULTILINE) + assert re.search(r"Test", out, re.MULTILINE) # Test for changed channel + assert re.search(r"test2", out, re.MULTILINE) # this one should remain as before + waitFor(PAUSE_AFTER_COMMAND) # Note: keep one secondary channel in order to send the hello to it @pytest.mark.smoke1 @@ -717,19 +781,6 @@ def test_smoke1_send_hello(): waitFor(PAUSE_AFTER_COMMAND) -@pytest.mark.smoke1 -def test_smoke1_qr(): - """Test --qr""" - # reset to standard url - pat = "CgI6ABIPCAE4A0ADSAFQG2gBwAYB" - setAndTestUrl(pat) - with tempfile.NamedTemporaryFile('w+b', delete=True, delete_on_close=False) as f: - return_value, _ = communicate(f"meshtastic --qr >{f.name}") - assert return_value == 0 - checkQr(f, pat) - waitFor(PAUSE_AFTER_COMMAND) - - @pytest.mark.smoke1 def test_smoke1_configure(): """Test --configure""" @@ -740,7 +791,7 @@ def test_smoke1_configure(): pytest.fail(f"Cannot access config: actual path: {Path.cwd()}. Execute tests from base folder.") _, out = communicate(f"meshtastic --configure {str(cfgPth)}") - print(f"out: {out}") + vprint(f"out: {out}") assert re.search("Connected to radio", out) assert re.search("^Setting owner properties: Bob TBeam - BOB - True", out, re.MULTILINE) assert re.search("^Setting channel url to https://www.meshtastic.org/e/#CgQ6AggNEg8IATgBQANIAVAeaAHABgE", out, re.MULTILINE) @@ -815,12 +866,12 @@ def test_smoke1_factory_reset(): @pytest.mark.smoke1 def test_smoke1_config_reset(temporaryCfgFile): """Restore original settings""" - print(f"Got temp file: {temporaryCfgFile.name}") + vprint(f"Got temp file: {temporaryCfgFile.name}") return_value, out = communicate(f"meshtastic --config {temporaryCfgFile.name}") - print(f"ret: {return_value} out: {out}") + vprint(f"ret: {return_value} out: {out}") assert re.search(r"Connected to radio", out) assert re.search(r"Configuration finished", out) if TEMPFILE is not None: - Path(TEMPFILE).unlink(missing_ok=True) \ No newline at end of file + Path(TEMPFILE).unlink(missing_ok=True) From 88e22bc619a41d5609d2872e87a1369c09829e7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?j=C3=BCrgenRe?= Date: Tue, 18 Nov 2025 08:11:05 +0100 Subject: [PATCH 10/12] update exampleConfig with CamelCase to have the same entries as the one with snake_case first try to solve unit test issues - not finished --- exampleConfig.yaml | 44 +++++++++++++++++++++++++++++------ meshtastic/tests/test_main.py | 28 ++++++++++------------ 2 files changed, 49 insertions(+), 23 deletions(-) diff --git a/exampleConfig.yaml b/exampleConfig.yaml index a546e5a39..d68b6f464 100644 --- a/exampleConfig.yaml +++ b/exampleConfig.yaml @@ -1,16 +1,46 @@ # example config using camelCase keys owner: Bob TBeam ownerShort: BOB +isUnmessagable: true -channelUrl: https://www.meshtastic.org/d/#CgUYAyIBAQ +channelUrl: https://www.meshtastic.org/e/#CgQ6AggNEg8IATgBQANIAVAeaAHABgE + +cannedMessages: 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 alt: 304 - -userPrefs: - region: 1 - isAlwaysPowered: "true" - screenOnSecs: 31536000 - waitBluetoothSecs: 31536000 +config: + bluetooth: + enabled: true + fixedPin: 123456 + device: + serialEnabled: true + display: + screenOnSecs: 781 + lora: + region: US + hopLimit: 3 + txEnabled: true + txPower: 30 + network: + ntpServer: 0.pool.ntp.org + position: + gpsAttemptTime: 900 + gpsEnabled: true + gpsUpdateInterval: 120 + positionBroadcastSecs: 900 + positionBroadcastSmartEnabled: true + positionFlags: 3 + power: + lsSecs: 300 + meshSdsTimeoutSecs: 7200 + minWakeSecs: 10 + sdsSecs: 4294967295 + waitBluetoothSecs: 344 +moduleConfig: + telemetry: + deviceUpdateInterval: 900 + environmentUpdateInterval: 900 diff --git a/meshtastic/tests/test_main.py b/meshtastic/tests/test_main.py index 0f10d9735..ff836b640 100644 --- a/meshtastic/tests/test_main.py +++ b/meshtastic/tests/test_main.py @@ -6,6 +6,7 @@ import platform import re import sys +from pathlib import Path from unittest.mock import mock_open, MagicMock, patch import pytest @@ -408,8 +409,8 @@ def test_main_nodes(capsys): iface = MagicMock(autospec=SerialInterface) - def mock_showNodes(includeSelf, showFields): - print(f"inside mocked showNodes: {includeSelf} {showFields}") + def mock_showNodes(includeSelf, showFields, printFmt): + print(f"inside mocked showNodes: {includeSelf} {showFields} {printFmt}") iface.showNodes.side_effect = mock_showNodes with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: @@ -1088,7 +1089,8 @@ def test_main_configure_with_snake_case(mocked_findports, mocked_serial, mocked_ """Test --configure with valid file""" sys.argv = ["", "--configure", "example_config.yaml"] mt_config.args = sys.argv - + outStr = f"Path: {Path.cwd()}, cfg: {mt_config.args}" + Path('debugTest.txt').write_text(outStr) serialInterface = SerialInterface(noProto=True) anode = Node(serialInterface, 1234567890, noProto=True) serialInterface.localNode = anode @@ -1784,23 +1786,17 @@ def test_main_export_config(capsys): with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: mo.getLongName.return_value = "foo" mo.getShortName.return_value = "oof" + mo.getIsUnmessagable.return_value = True 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, - "altitude": 100, - "batteryLevel": 34, - "latitude": 110.0, - "longitude": 120.0, + mo.getMyNodeInfo.return_value = { + "user": {"hwModel": "HELTEC_V3", "longName": "foo", "shortName": "oof"}, + "position": {"altitude": 100, "latitude": 110.0, "longitude": 120.0}, + "deviceMetrics": {"airUtilTx": 0.06, "batteryLevel": 101}, + "localStats": {"heapFreeBytes": 132796}, } - mo.localNode.radioConfig.preferences = """phone_timeout_secs: 900 -ls_secs: 300 -position_broadcast_smart: true -fixed_position: true -position_flags: 35""" - export_config(mo) + # export_config(mo) out = export_config(mo) err = "" From 335bebf83b0aa43914699a01a3f118681bdfa624 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?j=C3=BCrgenRe?= Date: Tue, 18 Nov 2025 23:18:28 +0100 Subject: [PATCH 11/12] -fix a bug, where yaml config is not read when running within pytest - mock node completely to be able to run configuration unit test - reset smoketest1 DEBUG settings --- meshtastic/__main__.py | 8 ++--- meshtastic/tests/test_main.py | 53 +++++++++++++++++---------------- meshtastic/tests/test_node.py | 30 +++++-------------- meshtastic/tests/test_smoke1.py | 10 +++---- 4 files changed, 44 insertions(+), 57 deletions(-) diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index 859ccdf1a..1bbaa7f67 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -1,6 +1,6 @@ """ Main Meshtastic """ - +from pathlib import Path # We just hit the 1600 line limit for main.py, but I currently have a huge set of powermon/structured logging changes # later we can have a separate changelist to refactor main.py into smaller files # pylint: disable=R0917,C0302 @@ -698,9 +698,8 @@ def writeSectionConfig(newConfig: dict, actConfig: dict, actNode: meshtastic.nod actNode.writeConfig(meshtastic.util.camel_to_snake(sectionName)) time.sleep(0.5) - with open(args.configure[0], encoding="utf8") as file: - configuration = yaml.safe_load(file) - closeNow = True + configuration = yaml.safe_load(Path(args.configure[0]).read_text(encoding="utf8")) + closeNow = True # fix configuration structure: older version had entries "ownerShort" and "channelUrl"? fixEntry(configuration, "ownerShort", "owner_short") @@ -1761,7 +1760,6 @@ def addChannelConfigArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentPa default=False, ) - # ToDo: just for easier debug, to be removed later group.add_argument( "--ch-info", help="List all channels", diff --git a/meshtastic/tests/test_main.py b/meshtastic/tests/test_main.py index ff836b640..772072b27 100644 --- a/meshtastic/tests/test_main.py +++ b/meshtastic/tests/test_main.py @@ -6,7 +6,6 @@ import platform import re import sys -from pathlib import Path from unittest.mock import mock_open, MagicMock, patch import pytest @@ -22,7 +21,8 @@ setMissingFlagsFalse, ) from meshtastic import mt_config - +from meshtastic.tests.test_node import initChannels +from ..protobuf import localonly_pb2, config_pb2 from ..protobuf.channel_pb2 import Channel # pylint: disable=E0611 # from ..ble_interface import BLEInterface @@ -1077,6 +1077,14 @@ def test_main_set_with_invalid(mocked_findports, mocked_serial, mocked_open, moc assert err == "" mo.assert_called() +def mockNode(ifce: SerialInterface) -> Node: + n = Node(ifce, 1234567890, noProto=True) + lc = localonly_pb2.LocalConfig() + n.localConfig = lc + lc.lora.CopyFrom(config_pb2.Config.LoRaConfig()) + n.moduleConfig = localonly_pb2.LocalModuleConfig() + n.channels = initChannels() + return n # TODO: write some negative --configure tests @pytest.mark.unit @@ -1089,25 +1097,21 @@ def test_main_configure_with_snake_case(mocked_findports, mocked_serial, mocked_ """Test --configure with valid file""" sys.argv = ["", "--configure", "example_config.yaml"] mt_config.args = sys.argv - outStr = f"Path: {Path.cwd()}, cfg: {mt_config.args}" - Path('debugTest.txt').write_text(outStr) serialInterface = SerialInterface(noProto=True) - anode = Node(serialInterface, 1234567890, noProto=True) - serialInterface.localNode = anode + serialInterface.localNode = mockNode(serialInterface) with patch("meshtastic.serial_interface.SerialInterface", return_value=serialInterface) as mo: main() out, err = capsys.readouterr() assert re.search(r"Connected to radio", out, re.MULTILINE) - # should these come back? maybe a flag? - #assert re.search(r"Setting device owner", out, re.MULTILINE) - #assert re.search(r"Setting device owner short", out, re.MULTILINE) - #assert re.search(r"Setting channel url", out, re.MULTILINE) - #assert re.search(r"Fixing altitude", out, re.MULTILINE) - #assert re.search(r"Fixing latitude", out, re.MULTILINE) - #assert re.search(r"Fixing longitude", out, re.MULTILINE) - #assert re.search(r"Set location_share to LocEnabled", out, re.MULTILINE) - assert re.search(r"Writing modified configuration to device", out, re.MULTILINE) + assert re.search(r"Telling open a transaction to edit settings", out, re.MULTILINE) + assert re.search(r"Setting owner properties: Bob TBeam - BOB - True", out, re.MULTILINE) + assert re.search(r"Setting channel url to https://www.meshtastic.org/e/#CgQ6AggNEg8IATgBQANIAVAeaAHABgE", out, re.MULTILINE) + assert re.search(r"Setting fixed device position to lat 35.88888 lon -93.88888 alt 304", out, re.MULTILINE) + assert re.search(r"Set lora.region to US", out, re.MULTILINE) + assert re.search(r"Set position.position_flags to 3", out, re.MULTILINE) + assert re.search(r"Set telemetry.environment_update_interval to 900", out, re.MULTILINE) + assert re.search(r"Committing modified configuration to device", out, re.MULTILINE) assert err == "" mo.assert_called() @@ -1124,21 +1128,20 @@ def test_main_configure_with_camel_case_keys(mocked_findports, mocked_serial, mo mt_config.args = sys.argv serialInterface = SerialInterface(noProto=True) - anode = Node(serialInterface, 1234567890, noProto=True) - serialInterface.localNode = anode + serialInterface.localNode = mockNode(serialInterface) with patch("meshtastic.serial_interface.SerialInterface", return_value=serialInterface) as mo: main() out, err = capsys.readouterr() assert re.search(r"Connected to radio", out, re.MULTILINE) - # should these come back? maybe a flag? - #assert re.search(r"Setting device owner", out, re.MULTILINE) - #assert re.search(r"Setting device owner short", out, re.MULTILINE) - #assert re.search(r"Setting channel url", out, re.MULTILINE) - #assert re.search(r"Fixing altitude", out, re.MULTILINE) - #assert re.search(r"Fixing latitude", out, re.MULTILINE) - #assert re.search(r"Fixing longitude", out, re.MULTILINE) - assert re.search(r"Writing modified configuration to device", out, re.MULTILINE) + assert re.search(r"Connected to radio", out, re.MULTILINE) + assert re.search(r"Telling open a transaction to edit settings", out, re.MULTILINE) + assert re.search(r"Setting owner properties: Bob TBeam - BOB - None", out, re.MULTILINE) + assert re.search(r"Setting channel url to https://www.meshtastic.org/e/#CgQ6AggNEg8IATgBQANIAVAeaAHABgE", out, re.MULTILINE) + assert re.search(r"Setting fixed device position to lat 35.88888 lon -93.88888 alt 304", out, re.MULTILINE) + assert re.search(r"Set lora.region to US", out, re.MULTILINE) + assert re.search(r"Set position.position_flags to 3", out, re.MULTILINE) + assert re.search(r"Committing modified configuration to device", out, re.MULTILINE) assert err == "" mo.assert_called() diff --git a/meshtastic/tests/test_node.py b/meshtastic/tests/test_node.py index c5cb6b3fa..bf3c2a7f0 100644 --- a/meshtastic/tests/test_node.py +++ b/meshtastic/tests/test_node.py @@ -19,6 +19,13 @@ # from ..util import Timeout +def initChannels(maxIdx: int = 8) -> list[Channel]: + """Initialize a set of empty channels, set the first one to PRIMARY""" + channels = [Channel(index=idx, role=0) for idx in range(0, maxIdx)] + channels[0].role = 1 + return channels + + @pytest.mark.unit def test_node(capsys): """Test that we can instantiate a Node""" @@ -354,28 +361,7 @@ def test_setURL_valid_URL_but_no_settings(capsys): def test_getChannelByChannelIndex(): """Test getChannelByChannelIndex()""" anode = Node("foo", "bar") - - channel1 = Channel(index=1, role=1) # primary channel - channel2 = Channel(index=2, role=2) # secondary channel - channel3 = Channel(index=3, role=0) - channel4 = Channel(index=4, role=0) - channel5 = Channel(index=5, role=0) - channel6 = Channel(index=6, role=0) - channel7 = Channel(index=7, role=0) - channel8 = Channel(index=8, role=0) - - channels = [ - channel1, - channel2, - channel3, - channel4, - channel5, - channel6, - channel7, - channel8, - ] - - anode.channels = channels + anode.channels = initChannels() # test primary assert anode.getChannelByChannelIndex(0) is not None diff --git a/meshtastic/tests/test_smoke1.py b/meshtastic/tests/test_smoke1.py index 2cc7b3d95..38b9d8008 100644 --- a/meshtastic/tests/test_smoke1.py +++ b/meshtastic/tests/test_smoke1.py @@ -23,10 +23,10 @@ TEMPFILE contains the extracted settings of the radio till the tests have finished, so the radio will not stay with wrong region settings for a longer time """ -# DEBUG: str = '' -# VERBOSE: bool = False -DEBUG: str = '--debug --logTo smoke1.log' -VERBOSE: bool = True +DEBUG: str = '' +VERBOSE: bool = False +# DEBUG: str = '--debug --logTo smoke1.log' +# VERBOSE: bool = True TEMPFILE = None @@ -220,7 +220,7 @@ def test_smoke1_test_with_arg_but_no_hardware(): waitFor(PAUSE_AFTER_COMMAND) -@pytest.mark.smoke1D +@pytest.mark.smoke1 def test_smoke1_debug(): """Test --debug""" return_value, out = communicate("meshtastic --info --debug") From 7d0c5b6a3e491fdf887718622879f944eb4f34c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?j=C3=BCrgenRe?= Date: Fri, 21 Nov 2025 16:56:31 +0100 Subject: [PATCH 12/12] systematically use --configure to reset channels fix bug in url splitted over 2 lines minor changes in formatting --- meshtastic/tests/test_smoke1.py | 46 +++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/meshtastic/tests/test_smoke1.py b/meshtastic/tests/test_smoke1.py index 38b9d8008..084569b2f 100644 --- a/meshtastic/tests/test_smoke1.py +++ b/meshtastic/tests/test_smoke1.py @@ -27,8 +27,9 @@ VERBOSE: bool = False # DEBUG: str = '--debug --logTo smoke1.log' # VERBOSE: bool = True -TEMPFILE = None +TEMPFILE = None +RESET_CHANNEL_CONFIG = "meshtastic/tests/ch_reset_config.yaml" # Path to reset file def noPrint(*args): """Dummy print function""" @@ -52,8 +53,10 @@ def communicate(cmd: str, repeatTimes: int = 2) -> tuple[int, str]: and not re.search("MeshInterfaceError: Timed out", out, re.MULTILINE): break vprint(f"k: {k} ret: {return_value} out: {out}") + vprint("Repeating command...") return return_value, out + def waitFor(eventOrTime: int, repeatTimes: int = 5) -> None: """Wait for a dedicated time (positive integer input) or for a reboot. The latter will ensure that the serial line is back operational so we can safely send the next command.""" @@ -73,6 +76,7 @@ def waitFor(eventOrTime: int, repeatTimes: int = 5) -> None: vprint("Reboot failed") break + def checkQr(f, qrCompare: str) -> None: """checks binary file containing url""" f.seek(0) @@ -83,8 +87,9 @@ def checkQr(f, qrCompare: str) -> None: assert len(qrSplit) >= 4 assert re.search(qrCompare, qrSplit[1].decode('utf-8'), re.MULTILINE) + def setAndTestUrl(pat: str, skipTest: bool = False) -> None: - """transmits set-url command with pattern "pat" and then checks if has been set correctly""" + """transmits set-url command with pattern "pat" and then checks if it has been set correctly""" url = f"https://meshtastic.org/e/#{pat}" return_value, out = communicate(f"meshtastic --seturl {url}") assert re.match(r"Connected to radio", out) @@ -405,7 +410,10 @@ def test_smoke1_ch_set_downlink_and_uplink(): @pytest.mark.smoke1 def test_smoke1_ch_add_and_ch_del(): """Test --ch-add""" - setAndTestUrl("CgI6ABIPCAE4A0ADSAFQG2gBwAYB") # ensure we have only primary channel configured. + return_value, out = communicate(f"meshtastic --configure {RESET_CHANNEL_CONFIG}") + assert return_value == 0 + waitFor(WAIT_FOR_REBOOT) + return_value, out = communicate("meshtastic --ch-add testing1") assert re.search(r"Writing modified channels to device", out, re.MULTILINE) assert return_value == 0 @@ -435,7 +443,10 @@ def test_smoke1_ch_add_and_ch_del(): @pytest.mark.smoke1 def test_smoke1_ch_enable_and_disable(): """Test --ch-enable and --ch-disable""" - setAndTestUrl("CgI6ABIPCAE4A0ADSAFQG2gBwAYB") # ensure we have only primary channel configured. + return_value, out = communicate(f"meshtastic --configure {RESET_CHANNEL_CONFIG}") + assert return_value == 0 + waitFor(WAIT_FOR_REBOOT) + return_value, out = communicate("meshtastic --ch-add testing1") assert re.search(r"Writing modified channels to device", out, re.MULTILINE) assert return_value == 0 @@ -483,7 +494,10 @@ def test_smoke1_ch_enable_and_disable(): @pytest.mark.smoke1 def test_smoke1_ch_del_a_disabled_non_primary_channel(): """Test --ch-del will work on a disabled non-primary channel.""" - setAndTestUrl("CgI6ABIPCAE4A0ADSAFQG2gBwAYB") # ensure we have only primary channel configured. + return_value, out = communicate(f"meshtastic --configure {RESET_CHANNEL_CONFIG}") + assert return_value == 0 + waitFor(WAIT_FOR_REBOOT) + return_value, out = communicate("meshtastic --ch-add testing1") assert re.search(r"Writing modified channels to device", out, re.MULTILINE) assert return_value == 0 @@ -564,7 +578,7 @@ def test_smoke1_ensure_ch_del_second_of_three_channels(): """Test that when we delete the 2nd of 3 channels, that it deletes the correct channel.""" # prepare test setting: setup 2 channels and validate they are created - return_value, out = communicate("meshtastic --configure tests/ch_reset_config.yaml") + return_value, out = communicate(f"meshtastic --configure {RESET_CHANNEL_CONFIG}") assert return_value == 0 waitFor(WAIT_FOR_REBOOT) @@ -613,7 +627,7 @@ def test_smoke1_ensure_ch_del_third_of_three_channels(): """Test that when we delete the 3rd of 3 channels, that it deletes the correct channel.""" # prepare test setting: setup 2 channels and validate they are created - return_value, out = communicate("meshtastic --configure tests/ch_reset_config.yaml") + return_value, out = communicate(f"meshtastic --configure {RESET_CHANNEL_CONFIG}") assert return_value == 0 waitFor(WAIT_FOR_REBOOT) @@ -659,7 +673,7 @@ def test_smoke1_ensure_ch_del_third_of_three_channels(): def test_smoke1_set_primary_channel(): """Test --seturl with primary channel""" # prepare test setting: setup 2 channels and validate they are created - return_value, _ = communicate("meshtastic --configure tests/ch_reset_config.yaml") + return_value, _ = communicate(f"meshtastic --configure {RESET_CHANNEL_CONFIG}") assert return_value == 0 waitFor(WAIT_FOR_REBOOT) @@ -672,7 +686,7 @@ def test_smoke1_set_primary_channel(): def test_smoke1_qr(): """Test --qr, based on setting via URL""" # prepare test setting: setup 2 channels and validate they are created - return_value, _ = communicate("meshtastic --configure tests/ch_reset_config.yaml") + return_value, _ = communicate(f"meshtastic --configure {RESET_CHANNEL_CONFIG}") assert return_value == 0 waitFor(WAIT_FOR_REBOOT) @@ -690,7 +704,7 @@ def test_smoke1_qr(): def test_smoke1_seturl_default(): """Test --seturl with default value""" # prepare test setting: setup std channel - return_value, out = communicate("meshtastic --configure tests/ch_reset_config.yaml") + return_value, out = communicate(f"meshtastic --configure {RESET_CHANNEL_CONFIG}") assert return_value == 0 waitFor(WAIT_FOR_REBOOT) @@ -724,7 +738,7 @@ def test_smoke1_seturl_invalid_url(): def test_smoke1_seturl_2chan(): """Test --seturl with 2 channels""" # prepare test setting: setup 2 channels and validate they are created - return_value, _ = communicate("meshtastic --configure tests/ch_reset_config.yaml") + return_value, _ = communicate(f"meshtastic --configure {RESET_CHANNEL_CONFIG}") assert return_value == 0 waitFor(WAIT_FOR_REBOOT) @@ -741,15 +755,15 @@ def test_smoke1_seturl_2chan(): def test_smoke1_seturl_3_to_2_chan(): """Test --seturl with 3 channels, then reconfigure 2 channels""" # prepare test setting: setup 2 channels and validate they are created - return_value, out = communicate("meshtastic --configure tests/ch_reset_config.yaml") + return_value, out = communicate(f"meshtastic --configure {RESET_CHANNEL_CONFIG}") assert return_value == 0 waitFor(WAIT_FOR_REBOOT) - pat = "CgcSAQE6AggNCjESIOKjX3f5UXnz8zkcXi6MxfIsnNof5sUAW4FQQi_IXsLdGgV0ZXN0MSgBMAE6AggRCi0SIGyPI2Gbw3v6rl9H\ - Q8SL3LvRx7ScovIdU6pahs_l59CoGgV0ZXN0MigBMAESDwgBOANAA0gBUBtoAcAGAQ" + pat = "CgcSAQE6AggNCjESIOKjX3f5UXnz8zkcXi6MxfIsnNof5sUAW4FQQi_IXsLdGgV0ZXN0MSgBMAE6AggRCi0SIGyPI2Gbw3v6rl9H" + \ + "Q8SL3LvRx7ScovIdU6pahs_l59CoGgV0ZXN0MigBMAESDwgBOANAA0gBUBtoAcAGAQ" setAndTestUrl(pat) # check that we have 3 channels - return_value, out = communicate("meshtastic --info", repeatTimes=2) + return_value, out = communicate("meshtastic --info") assert return_value == 0 assert re.match(r"Connected to radio", out) assert re.search(r"SECONDARY", out, re.MULTILINE) @@ -762,7 +776,7 @@ def test_smoke1_seturl_3_to_2_chan(): setAndTestUrl(patSet, skipTest=True) # now test for patComp (url will be diefferent because of not deleted channel 2) - return_value, out = communicate("meshtastic --info", repeatTimes=2) + return_value, out = communicate("meshtastic --info") assert return_value == 0 assert re.match(r"Connected to radio", out) assert re.search(r"SECONDARY", out, re.MULTILINE)