diff --git a/examples/security_audit.py b/examples/security_audit.py new file mode 100644 index 000000000..9cc84ffd6 --- /dev/null +++ b/examples/security_audit.py @@ -0,0 +1,225 @@ +#!/usr/bin/env python3 +""" +Example script to audit Meshtastic node security settings. + +This standalone script connects to the local Meshtastic device and +generates a checklist of security recommendations based on the +device's current configuration. + +Usage: + python security_audit.py [--output FILE] +""" + +import argparse + +try: + from meshtastic.serial_interface import SerialInterface + from meshtastic.__main__ import export_security_recommendations # retained for backward compatibility +except ImportError as e: + raise SystemExit("Error importing Meshtastic libraries: %s" % e) + + +def main(): + parser = argparse.ArgumentParser( + description="Audit Meshtastic device security settings." + ) + parser.add_argument( + "--output", "-o", + help="Write security recommendations to FILE instead of stdout" + ) + args = parser.parse_args() + + print("Connecting to Meshtastic device...") + iface = SerialInterface() + + print("Gathering security settings...") + + # Perform structured security checks + issues = [] + sec = iface.localNode.localConfig.security + + # Serial port check + if sec.serial_enabled: + issues.append(( + 'INFO', '⚪', + 'Serial port is enabled', + f'serial_enabled={sec.serial_enabled}' + )) + else: + issues.append(( + 'GOOD', '🟢', + 'Serial port is disabled', + f'serial_enabled={sec.serial_enabled}' + )) + + # Debug log API check + if sec.debug_log_api_enabled: + issues.append(( + 'INFO', '⚪', + 'Debug log API is enabled', + f'debug_log_api_enabled={sec.debug_log_api_enabled}' + )) + else: + issues.append(( + 'GOOD', '🟢', + 'Debug log API is disabled', + f'debug_log_api_enabled={sec.debug_log_api_enabled}' + )) + + # Admin channel check + if not sec.admin_channel_enabled: + issues.append(( + 'MEDIUM', '🟠', + 'Admin channel is not enabled', + f'admin_channel_enabled={sec.admin_channel_enabled}' + )) + else: + issues.append(( + 'GOOD', '🟢', + 'Admin channel is enabled', + f'admin_channel_enabled={sec.admin_channel_enabled}' + )) + + # Managed mode check + if not sec.is_managed: + issues.append(( + 'INFO', '⚪', + 'Device is not in managed mode', + f'is_managed={sec.is_managed}' + )) + else: + issues.append(( + 'GOOD', '🟢', + 'Device is in managed mode', + f'is_managed={sec.is_managed}' + )) + + # Private key presence + if sec.private_key: + issues.append(( + 'INFO', '⚪', + 'Private key is present; consider rotation', + f'private_key_length={len(sec.private_key)}' + )) + else: + issues.append(( + 'GOOD', '🟢', + 'No private key present', + 'private_key_length=0' + )) + + # Admin keys count + if len(sec.admin_key) > 1: + issues.append(( + 'INFO', '⚪', + 'Multiple admin keys configured', + f'admin_key_count={len(sec.admin_key)}' + )) + else: + issues.append(( + 'GOOD', '🟢', + 'Acceptable admin key count', + f'admin_key_count={len(sec.admin_key)}' + )) + + # Channel PSK checks + import base64 + from meshtastic.protobuf.channel_pb2 import Channel + + for ch in getattr(iface.localNode, 'channels', []) or []: + psk = ch.settings.psk or b'' + name = ch.settings.name or f'[{ch.index}]' + b64 = base64.b64encode(psk).decode('ascii') + # PSK length check + if len(psk) not in (0, 32): + issues.append(( + 'HIGH', '🔴', + f"Channel '{name}' PSK insecure length ({len(psk)} bytes)", + f'psk={b64}' + )) + else: + if len(psk) == 32: + issues.append(( + 'GOOD', '🟢', + f"Channel '{name}' PSK length is correct (32 bytes)", + f'psk={b64}' + )) + elif len(psk) == 0: + issues.append(( + 'GOOD', '🟢', + f"Channel '{name}' channel disabled", + f'psk={b64}' + )) + # Default PSK check + if b64 == 'AQ==': + issues.append(( + 'HIGH', '🔴', + f"Channel '{name}' uses default PSK (AQ==)", + f'psk={b64}' + )) + + + # Bluetooth checks + from meshtastic.protobuf.config_pb2 import Config + bt = iface.localNode.localConfig.bluetooth + # Bluetooth enabled check + if bt.enabled: + issues.append(( + 'INFO', '⚪', + 'Bluetooth is enabled', + f'bluetooth_enabled={bt.enabled}' + )) + else: + issues.append(( + 'GOOD', '🟢', + 'Bluetooth is disabled', + f'bluetooth_enabled={bt.enabled}' + )) + + # Bluetooth PIN mode check + if bt.mode == Config.BluetoothConfig.FIXED_PIN: + defaults = (0, 123456) + if bt.fixed_pin in defaults: + pin = str(bt.fixed_pin) + masked = pin[0] + '*'*(len(pin)-2) + pin[-1] + issues.append(( + 'HIGH', '🔴', + 'Bluetooth uses fixed default PIN', + f'fixed_pin={masked}' + )) + else: + issues.append(( + 'INFO', '🟠', + 'Bluetooth uses fixed PIN (non-default)', + f'fixed_pin={bt.fixed_pin}' + )) + else: + issues.append(( + 'GOOD', '🟢', + 'Bluetooth does not use a fixed PIN', + f'mode={bt.mode}' + )) + + # Output report + if not issues: + report = '✅ No obvious security issues detected.' + else: + # sort by severity: HIGH (worst), MEDIUM, then GOOD (best) + sev_order = {'HIGH': 0, 'MEDIUM': 1, 'LOW': 2, 'INFO': 3, 'GOOD': 4} + issues.sort(key=lambda x: sev_order.get(x[0], 99)) + lines = ['# Security Audit Report'] + for level, emoji, title, detail in issues: + lines.append(f'{emoji} [{level}] {title}') + lines.append(f' → {detail}') + report = '\n'.join(lines) + + if args.output: + with open(args.output, 'w', encoding='utf-8') as f: + f.write(report + '\n') + print(f"Security audit written to {args.output}") + else: + print(report) + + +if __name__ == "__main__": + main() diff --git a/meshtastic/__init__.py b/meshtastic/__init__.py deleted file mode 100644 index 7573e8b02..000000000 --- a/meshtastic/__init__.py +++ /dev/null @@ -1,282 +0,0 @@ -""" -# A library for the Meshtastic Client API - -Primary interfaces: SerialInterface, TCPInterface, BLEInterface - -Install with pip: "[pip3 install meshtastic](https://pypi.org/project/meshtastic/)" - -Source code on [github](https://github.com/meshtastic/python) - -notable properties of interface classes: - -- `nodes` - The database of received nodes. Includes always up-to-date location and username information for each -node in the mesh. This is a read-only datastructure. -- `nodesByNum` - like "nodes" but keyed by nodeNum instead of nodeId. As such, includes "unknown" nodes which haven't seen a User packet yet -- `myInfo` & `metadata` - Contain read-only information about the local radio device (software version, hardware version, etc) -- `localNode` - Pointer to a node object for the local node - -notable properties of nodes: - -- `localConfig` - Current radio settings, can be written to the radio with the `writeConfig` method. -- `moduleConfig` - Current module settings, can be written to the radio with the `writeConfig` method. -- `channels` - The node's channels, keyed by index. - -# Published PubSub topics - -We use a [publish-subscribe](https://pypubsub.readthedocs.io/en/v4.0.3/) model to communicate asynchronous events. Available -topics: - -- `meshtastic.connection.established` - published once we've successfully connected to the radio and downloaded the node DB -- `meshtastic.connection.lost` - published once we've lost our link to the radio -- `meshtastic.receive.text(packet)` - delivers a received packet as a dictionary, if you only care about a particular -type of packet, you should subscribe to the full topic name. If you want to see all packets, simply subscribe to "meshtastic.receive". -- `meshtastic.receive.position(packet)` -- `meshtastic.receive.user(packet)` -- `meshtastic.receive.data.portnum(packet)` (where portnum is an integer or well known PortNum enum) -- `meshtastic.node.updated(node = NodeInfo)` - published when a node in the DB changes (appears, location changed, username changed, etc...) -- `meshtastic.log.line(line)` - a raw unparsed log line from the radio -- `meshtastic.clientNotification(notification, interface) - a ClientNotification sent from the radio - -We receive position, user, or data packets from the mesh. You probably only care about `meshtastic.receive.data`. The first argument for -that publish will be the packet. Text or binary data packets (from `sendData` or `sendText`) will both arrive this way. If you print packet -you'll see the fields in the dictionary. `decoded.data.payload` will contain the raw bytes that were sent. If the packet was sent with -`sendText`, `decoded.data.text` will **also** be populated with the decoded string. For ASCII these two strings will be the same, but for -unicode scripts they can be different. - -# Example Usage -``` -import meshtastic -import meshtastic.serial_interface -from pubsub import pub - -def onReceive(packet, interface): # called when a packet arrives - print(f"Received: {packet}") - -def onConnection(interface, topic=pub.AUTO_TOPIC): # called when we (re)connect to the radio - # defaults to broadcast, specify a destination ID if you wish - interface.sendText("hello mesh") - -pub.subscribe(onReceive, "meshtastic.receive") -pub.subscribe(onConnection, "meshtastic.connection.established") -# By default will try to find a meshtastic device, otherwise provide a device path like /dev/ttyUSB0 -interface = meshtastic.serial_interface.SerialInterface() - -``` - -""" - -import base64 -import logging -import os -import platform -import random -import socket -import stat -import sys -import threading -import time -import traceback -from datetime import datetime -from typing import * - -import google.protobuf.json_format -import serial # type: ignore[import-untyped] -from google.protobuf.json_format import MessageToJson -from pubsub import pub # type: ignore[import-untyped] -from tabulate import tabulate - -from meshtastic.node import Node -from meshtastic.util import DeferredExecution, Timeout, catchAndIgnore, fixme, stripnl - -from .protobuf import ( - admin_pb2, - apponly_pb2, - channel_pb2, - config_pb2, - mesh_pb2, - mqtt_pb2, - paxcount_pb2, - portnums_pb2, - remote_hardware_pb2, - storeforward_pb2, - telemetry_pb2, - powermon_pb2 -) -from . import ( - util, -) - -# Note: To follow PEP224, comments should be after the module variable. - -LOCAL_ADDR = "^local" -"""A special ID that means the local node""" - -BROADCAST_NUM: int = 0xFFFFFFFF -"""if using 8 bit nodenums this will be shortened on the target""" - -BROADCAST_ADDR = "^all" -"""A special ID that means broadcast""" - -OUR_APP_VERSION: int = 20300 -"""The numeric buildnumber (shared with android apps) specifying the - level of device code we are guaranteed to understand - - format is Mmmss (where M is 1+the numeric major number. i.e. 20120 means 1.1.20 -""" - -NODELESS_WANT_CONFIG_ID = 69420 -"""A special thing to pass for want_config_id that instructs nodes to skip sending nodeinfos other than its own.""" - -publishingThread = DeferredExecution("publishing") - - -class ResponseHandler(NamedTuple): - """A pending response callback, waiting for a response to one of our messages""" - - # requestId: int - used only as a key - #: a callable to call when a response is received - callback: Callable - #: Whether ACKs and NAKs should be passed to this handler - ackPermitted: bool = False - # FIXME, add timestamp and age out old requests - - -class KnownProtocol(NamedTuple): - """Used to automatically decode known protocol payloads""" - - #: A descriptive name (e.g. "text", "user", "admin") - name: str - #: If set, will be called to parse as a protocol buffer - protobufFactory: Optional[Callable] = None - #: If set, invoked as onReceive(interface, packet) - onReceive: Optional[Callable] = None - - -def _onTextReceive(iface, asDict): - """Special text auto parsing for received messages""" - # We don't throw if the utf8 is invalid in the text message. Instead we just don't populate - # the decoded.data.text and we log an error message. This at least allows some delivery to - # the app and the app can deal with the missing decoded representation. - # - # Usually btw this problem is caused by apps sending binary data but setting the payload type to - # text. - logging.debug(f"in _onTextReceive() asDict:{asDict}") - try: - asBytes = asDict["decoded"]["payload"] - asDict["decoded"]["text"] = asBytes.decode("utf-8") - except Exception as ex: - logging.error(f"Malformatted utf8 in text message: {ex}") - _receiveInfoUpdate(iface, asDict) - - -def _onPositionReceive(iface, asDict): - """Special auto parsing for received messages""" - logging.debug(f"in _onPositionReceive() asDict:{asDict}") - if "decoded" in asDict: - if "position" in asDict["decoded"] and "from" in asDict: - p = asDict["decoded"]["position"] - logging.debug(f"p:{p}") - p = iface._fixupPosition(p) - logging.debug(f"after fixup p:{p}") - # update node DB as needed - iface._getOrCreateByNum(asDict["from"])["position"] = p - - -def _onNodeInfoReceive(iface, asDict): - """Special auto parsing for received messages""" - logging.debug(f"in _onNodeInfoReceive() asDict:{asDict}") - if "decoded" in asDict: - if "user" in asDict["decoded"] and "from" in asDict: - p = asDict["decoded"]["user"] - # decode user protobufs and update nodedb, provide decoded version as "position" in the published msg - # update node DB as needed - n = iface._getOrCreateByNum(asDict["from"]) - n["user"] = p - # We now have a node ID, make sure it is up-to-date in that table - iface.nodes[p["id"]] = n - _receiveInfoUpdate(iface, asDict) - -def _onTelemetryReceive(iface, asDict): - """Automatically update device metrics on received packets""" - logging.debug(f"in _onTelemetryReceive() asDict:{asDict}") - if "from" not in asDict: - return - - toUpdate = None - - telemetry = asDict.get("decoded", {}).get("telemetry", {}) - node = iface._getOrCreateByNum(asDict["from"]) - if "deviceMetrics" in telemetry: - toUpdate = "deviceMetrics" - elif "environmentMetrics" in telemetry: - toUpdate = "environmentMetrics" - elif "airQualityMetrics" in telemetry: - toUpdate = "airQualityMetrics" - elif "powerMetrics" in telemetry: - toUpdate = "powerMetrics" - elif "localStats" in telemetry: - toUpdate = "localStats" - else: - return - - updateObj = telemetry.get(toUpdate) - newMetrics = node.get(toUpdate, {}) - newMetrics.update(updateObj) - logging.debug(f"updating {toUpdate} metrics for {asDict['from']} to {newMetrics}") - node[toUpdate] = newMetrics - -def _receiveInfoUpdate(iface, asDict): - if "from" in asDict: - iface._getOrCreateByNum(asDict["from"])["lastReceived"] = asDict - iface._getOrCreateByNum(asDict["from"])["lastHeard"] = asDict.get("rxTime") - iface._getOrCreateByNum(asDict["from"])["snr"] = asDict.get("rxSnr") - iface._getOrCreateByNum(asDict["from"])["hopLimit"] = asDict.get("hopLimit") - -def _onAdminReceive(iface, asDict): - """Special auto parsing for received messages""" - logging.debug(f"in _onAdminReceive() asDict:{asDict}") - if "decoded" in asDict and "from" in asDict and "admin" in asDict["decoded"]: - adminMessage = asDict["decoded"]["admin"]["raw"] - iface._getOrCreateByNum(asDict["from"])["adminSessionPassKey"] = adminMessage.session_passkey - -"""Well known message payloads can register decoders for automatic protobuf parsing""" -protocols = { - portnums_pb2.PortNum.TEXT_MESSAGE_APP: KnownProtocol( - "text", onReceive=_onTextReceive - ), - portnums_pb2.PortNum.RANGE_TEST_APP: KnownProtocol( - "rangetest", onReceive=_onTextReceive - ), - portnums_pb2.PortNum.DETECTION_SENSOR_APP: KnownProtocol( - "detectionsensor", onReceive=_onTextReceive - ), - - portnums_pb2.PortNum.POSITION_APP: KnownProtocol( - "position", mesh_pb2.Position, _onPositionReceive - ), - portnums_pb2.PortNum.NODEINFO_APP: KnownProtocol( - "user", mesh_pb2.User, _onNodeInfoReceive - ), - portnums_pb2.PortNum.ADMIN_APP: KnownProtocol( - "admin", admin_pb2.AdminMessage, _onAdminReceive - ), - portnums_pb2.PortNum.ROUTING_APP: KnownProtocol("routing", mesh_pb2.Routing), - portnums_pb2.PortNum.TELEMETRY_APP: KnownProtocol( - "telemetry", telemetry_pb2.Telemetry, _onTelemetryReceive - ), - portnums_pb2.PortNum.REMOTE_HARDWARE_APP: KnownProtocol( - "remotehw", remote_hardware_pb2.HardwareMessage - ), - portnums_pb2.PortNum.SIMULATOR_APP: KnownProtocol("simulator", mesh_pb2.Compressed), - portnums_pb2.PortNum.TRACEROUTE_APP: KnownProtocol( - "traceroute", mesh_pb2.RouteDiscovery - ), - portnums_pb2.PortNum.POWERSTRESS_APP: KnownProtocol( - "powerstress", powermon_pb2.PowerStressMessage - ), - portnums_pb2.PortNum.WAYPOINT_APP: KnownProtocol("waypoint", mesh_pb2.Waypoint), - portnums_pb2.PortNum.PAXCOUNTER_APP: KnownProtocol("paxcounter", paxcount_pb2.Paxcount), - portnums_pb2.PortNum.STORE_FORWARD_APP: KnownProtocol("storeforward", storeforward_pb2.StoreAndForward), - portnums_pb2.PortNum.NEIGHBORINFO_APP: KnownProtocol("neighborinfo", mesh_pb2.NeighborInfo), - portnums_pb2.PortNum.MAP_REPORT_APP: KnownProtocol("mapreport", mqtt_pb2.MapReport), -} diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py deleted file mode 100644 index b58cc66b5..000000000 --- a/meshtastic/__main__.py +++ /dev/null @@ -1,2199 +0,0 @@ -""" Main Meshtastic -""" - -# 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 - -from typing import List, Optional, Union -from types import ModuleType - -import argparse -argcomplete: Union[None, ModuleType] = None -try: - import argcomplete # type: ignore -except ImportError as e: - pass # already set to None by default above - -import logging -import os -import platform -import sys -import time - -try: - import pyqrcode # type: ignore[import-untyped] -except ImportError as e: - pyqrcode = None - -import yaml -from google.protobuf.json_format import MessageToDict -from pubsub import pub # type: ignore[import-untyped] - -try: - import meshtastic.test - have_test = True -except ImportError as e: - have_test = False - -import meshtastic.util -import meshtastic.serial_interface -import meshtastic.tcp_interface - -from meshtastic import BROADCAST_ADDR, mt_config, remote_hardware -from meshtastic.ble_interface import BLEInterface -from meshtastic.mesh_interface import MeshInterface -try: - from meshtastic.powermon import ( - PowerMeter, - PowerStress, - PPK2PowerSupply, - RidenPowerSupply, - SimPowerSupply, - ) - from meshtastic.slog import LogSet - have_powermon = True - powermon_exception = None - meter: Optional[PowerMeter] = None -except ImportError as e: - have_powermon = False - powermon_exception = e - meter = None -from meshtastic.protobuf import channel_pb2, config_pb2, portnums_pb2 -from meshtastic.version import get_active_version - -def onReceive(packet, interface) -> None: - """Callback invoked when a packet arrives""" - args = mt_config.args - try: - d = packet.get("decoded") - logging.debug(f"in onReceive() d:{d}") - - # Exit once we receive a reply - if ( - args - and args.sendtext - and packet["to"] == interface.myInfo.my_node_num - and d.get("portnum", portnums_pb2.PortNum.UNKNOWN_APP) == portnums_pb2.PortNum.TEXT_MESSAGE_APP - ): - interface.close() # after running command then exit - - # Reply to every received message with some stats - if d is not None and args and args.reply: - msg = d.get("text") - if msg: - rxSnr = packet["rxSnr"] - hopLimit = packet["hopLimit"] - print(f"message: {msg}") - reply = f"got msg '{msg}' with rxSnr: {rxSnr} and hopLimit: {hopLimit}" - print("Sending reply: ", reply) - interface.sendText(reply) - - except Exception as ex: - print(f"Warning: Error processing received packet: {ex}.") - - -def onConnection(interface, topic=pub.AUTO_TOPIC) -> None: # pylint: disable=W0613 - """Callback invoked when we connect/disconnect from a radio""" - print(f"Connection changed: {topic.getName()}") - - -def checkChannel(interface: MeshInterface, channelIndex: int) -> bool: - """Given an interface and channel index, return True if that channel is non-disabled on the local node""" - ch = interface.localNode.getChannelByChannelIndex(channelIndex) - logging.debug(f"ch:{ch}") - return ch and ch.role != channel_pb2.Channel.Role.DISABLED - - -def getPref(node, comp_name) -> bool: - """Get a channel or preferences value""" - def _printSetting(config_type, uni_name, pref_value, repeated): - """Pretty print the setting""" - if repeated: - pref_value = [meshtastic.util.toStr(v) for v in pref_value] - else: - pref_value = meshtastic.util.toStr(pref_value) - print(f"{str(config_type.name)}.{uni_name}: {str(pref_value)}") - logging.debug(f"{str(config_type.name)}.{uni_name}: {str(pref_value)}") - - name = splitCompoundName(comp_name) - wholeField = name[0] == name[1] # We want the whole field - - camel_name = meshtastic.util.snake_to_camel(name[1]) - # Note: protobufs has the keys in snake_case, so snake internally - snake_name = meshtastic.util.camel_to_snake(name[1]) - uni_name = camel_name if mt_config.camel_case else snake_name - logging.debug(f"snake_name:{snake_name} camel_name:{camel_name}") - logging.debug(f"use camel:{mt_config.camel_case}") - - # First validate the input - localConfig = node.localConfig - moduleConfig = node.moduleConfig - found: bool = False - for config in [localConfig, moduleConfig]: - objDesc = config.DESCRIPTOR - config_type = objDesc.fields_by_name.get(name[0]) - pref = "" #FIXME - is this correct to leave as an empty string if not found? - if config_type: - pref = config_type.message_type.fields_by_name.get(snake_name) - if pref or wholeField: - found = True - break - - if not found: - print( - f"{localConfig.__class__.__name__} and {moduleConfig.__class__.__name__} do not have attribute {uni_name}." - ) - print("Choices are...") - printConfig(localConfig) - printConfig(moduleConfig) - return False - - # Check if we need to request the config - if len(config.ListFields()) != 0 and not isinstance(pref, str): # if str, it's still the empty string, I think - # read the value - config_values = getattr(config, config_type.name) - if not wholeField: - pref_value = getattr(config_values, pref.name) - repeated = pref.label == pref.LABEL_REPEATED - _printSetting(config_type, uni_name, pref_value, repeated) - else: - for field in config_values.ListFields(): - repeated = field[0].label == field[0].LABEL_REPEATED - _printSetting(config_type, field[0].name, field[1], repeated) - else: - # Always show whole field for remote node - node.requestConfig(config_type) - - return True - - -def splitCompoundName(comp_name: str) -> List[str]: - """Split compound (dot separated) preference name into parts""" - name: List[str] = comp_name.split(".") - if len(name) < 2: - name[0] = comp_name - name.append(comp_name) - return name - - -def traverseConfig(config_root, config, interface_config) -> bool: - """Iterate through current config level preferences and either traverse deeper if preference is a dict or set preference""" - snake_name = meshtastic.util.camel_to_snake(config_root) - for pref in config: - pref_name = f"{snake_name}.{pref}" - if isinstance(config[pref], dict): - traverseConfig(pref_name, config[pref], interface_config) - else: - setPref(interface_config, pref_name, config[pref]) - - return True - - -def setPref(config, comp_name, raw_val) -> bool: - """Set a channel or preferences value""" - - name = splitCompoundName(comp_name) - - snake_name = meshtastic.util.camel_to_snake(name[-1]) - camel_name = meshtastic.util.snake_to_camel(name[-1]) - uni_name = camel_name if mt_config.camel_case else snake_name - logging.debug(f"snake_name:{snake_name}") - logging.debug(f"camel_name:{camel_name}") - - objDesc = config.DESCRIPTOR - config_part = config - config_type = objDesc.fields_by_name.get(name[0]) - if config_type and config_type.message_type is not None: - for name_part in name[1:-1]: - part_snake_name = meshtastic.util.camel_to_snake((name_part)) - config_part = getattr(config, config_type.name) - config_type = config_type.message_type.fields_by_name.get(part_snake_name) - pref = None - if config_type and config_type.message_type is not None: - pref = config_type.message_type.fields_by_name.get(snake_name) - # Others like ChannelSettings are standalone - elif config_type: - pref = config_type - - if (not pref) or (not config_type): - return False - - if isinstance(raw_val, str): - val = meshtastic.util.fromStr(raw_val) - else: - val = raw_val - logging.debug(f"valStr:{raw_val} val:{val}") - - if snake_name == "wifi_psk" and len(str(raw_val)) < 8: - print("Warning: network.wifi_psk must be 8 or more characters.") - return False - - enumType = pref.enum_type - # pylint: disable=C0123 - if enumType and type(val) == str: - # We've failed so far to convert this string into an enum, try to find it by reflection - e = enumType.values_by_name.get(val) - if e: - val = e.number - else: - print( - f"{name[0]}.{uni_name} does not have an enum called {val}, so you can not set it." - ) - print(f"Choices in sorted order are:") - names = [] - for f in enumType.values: - # Note: We must use the value of the enum (regardless if camel or snake case) - names.append(f"{f.name}") - for temp_name in sorted(names): - print(f" {temp_name}") - return False - - # repeating fields need to be handled with append, not setattr - if pref.label != pref.LABEL_REPEATED: - try: - if config_type.message_type is not None: - config_values = getattr(config_part, config_type.name) - setattr(config_values, pref.name, val) - else: - setattr(config_part, snake_name, val) - except TypeError: - # The setter didn't like our arg type guess try again as a string - config_values = getattr(config_part, config_type.name) - setattr(config_values, pref.name, str(val)) - elif type(val) == list: - new_vals = [meshtastic.util.fromStr(x) for x in val] - config_values = getattr(config, config_type.name) - getattr(config_values, pref.name)[:] = new_vals - else: - config_values = getattr(config, config_type.name) - if val == 0: - # clear values - print(f"Clearing {pref.name} list") - del getattr(config_values, pref.name)[:] - else: - print(f"Adding '{raw_val}' to the {pref.name} list") - cur_vals = [x for x in getattr(config_values, pref.name) if x not in [0, "", b""]] - cur_vals.append(val) - getattr(config_values, pref.name)[:] = cur_vals - return True - - prefix = f"{'.'.join(name[0:-1])}." if config_type.message_type is not None else "" - print(f"Set {prefix}{uni_name} to {raw_val}") - - return True - - -def onConnected(interface): - """Callback invoked when we connect to a radio""" - 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? - ) - try: - args = mt_config.args - - # convenient place to store any keyword args we pass to getNode - getNode_kwargs = { - "requestChannelAttempts": args.channel_fetch_attempts, - "timeout": args.timeout - } - - # do not print this line if we are exporting the config - if not args.export_config: - print("Connected to radio") - - if args.set_time is not None: - interface.getNode(args.dest, False, **getNode_kwargs).setTime(args.set_time) - - if args.remove_position: - closeNow = True - waitForAckNak = True - - print("Removing fixed position and disabling fixed position setting") - interface.getNode(args.dest, False, **getNode_kwargs).removeFixedPosition() - elif args.setlat or args.setlon or args.setalt: - closeNow = True - waitForAckNak = True - - alt = 0 - lat = 0 - lon = 0 - if args.setalt: - alt = int(args.setalt) - print(f"Fixing altitude at {alt} meters") - if args.setlat: - try: - lat = int(args.setlat) - except ValueError: - lat = float(args.setlat) - print(f"Fixing latitude at {lat} degrees") - if args.setlon: - try: - lon = int(args.setlon) - except ValueError: - lon = float(args.setlon) - print(f"Fixing longitude at {lon} degrees") - - print("Setting device position and enabling fixed position setting") - # can include lat/long/alt etc: latitude = 37.5, longitude = -122.1 - interface.getNode(args.dest, False, **getNode_kwargs).setFixedPosition(lat, lon, alt) - - if args.set_owner or args.set_owner_short or args.set_is_unmessageable: - closeNow = True - waitForAckNak = True - - long_name = args.set_owner.strip() if args.set_owner else None - short_name = args.set_owner_short.strip() if args.set_owner_short else None - - if long_name is not None and not long_name: - meshtastic.util.our_exit("ERROR: Long Name cannot be empty or contain only whitespace characters") - - if short_name is not None and not short_name: - meshtastic.util.our_exit("ERROR: Short Name cannot be empty or contain only whitespace characters") - - if long_name and short_name: - print(f"Setting device owner to {long_name} and short name to {short_name}") - elif long_name: - print(f"Setting device owner to {long_name}") - elif short_name: - print(f"Setting device owner short to {short_name}") - - unmessagable = None - if args.set_is_unmessageable is not None: - unmessagable = ( - meshtastic.util.fromStr(args.set_is_unmessageable) - if isinstance(args.set_is_unmessageable, str) - else args.set_is_unmessageable - ) - print(f"Setting device owner is_unmessageable to {unmessagable}") - - interface.getNode(args.dest, False, **getNode_kwargs).setOwner( - long_name=long_name, - short_name=short_name, - is_unmessagable=unmessagable - ) - - if args.set_canned_message: - closeNow = True - waitForAckNak = True - print(f"Setting canned plugin message to {args.set_canned_message}") - interface.getNode(args.dest, False, **getNode_kwargs).set_canned_message( - args.set_canned_message - ) - - if args.set_ringtone: - closeNow = True - waitForAckNak = True - print(f"Setting ringtone to {args.set_ringtone}") - interface.getNode(args.dest, False, **getNode_kwargs).set_ringtone(args.set_ringtone) - - if args.pos_fields: - # If --pos-fields invoked with args, set position fields - closeNow = True - positionConfig = interface.getNode(args.dest, **getNode_kwargs).localConfig.position - allFields = 0 - - try: - for field in args.pos_fields: - v_field = positionConfig.PositionFlags.Value(field) - allFields |= v_field - - except ValueError: - print("ERROR: supported position fields are:") - print(positionConfig.PositionFlags.keys()) - print( - "If no fields are specified, will read and display current value." - ) - - else: - print(f"Setting position fields to {allFields}") - setPref(positionConfig, "position_flags", f"{allFields:d}") - print("Writing modified preferences to device") - interface.getNode(args.dest, **getNode_kwargs).writeConfig("position") - - elif args.pos_fields is not None: - # If --pos-fields invoked without args, read and display current value - closeNow = True - positionConfig = interface.getNode(args.dest, **getNode_kwargs).localConfig.position - - fieldNames = [] - for bit in positionConfig.PositionFlags.values(): - if positionConfig.position_flags & bit: - fieldNames.append(positionConfig.PositionFlags.Name(bit)) - print(" ".join(fieldNames)) - - if args.set_ham: - if not args.set_ham.strip(): - meshtastic.util.our_exit("ERROR: Ham radio callsign cannot be empty or contain only whitespace characters") - closeNow = True - print(f"Setting Ham ID to {args.set_ham} and turning off encryption") - interface.getNode(args.dest, **getNode_kwargs).setOwner(args.set_ham, is_licensed=True) - # Must turn off encryption on primary channel - interface.getNode(args.dest, **getNode_kwargs).turnOffEncryptionOnPrimaryChannel() - - if args.reboot: - closeNow = True - waitForAckNak = True - interface.getNode(args.dest, False, **getNode_kwargs).reboot() - - if args.reboot_ota: - closeNow = True - waitForAckNak = True - interface.getNode(args.dest, False, **getNode_kwargs).rebootOTA() - - if args.enter_dfu: - closeNow = True - waitForAckNak = True - interface.getNode(args.dest, False, **getNode_kwargs).enterDFUMode() - - if args.shutdown: - closeNow = True - waitForAckNak = True - interface.getNode(args.dest, False, **getNode_kwargs).shutdown() - - if args.device_metadata: - closeNow = True - interface.getNode(args.dest, False, **getNode_kwargs).getMetadata() - - if args.begin_edit: - closeNow = True - interface.getNode(args.dest, False, **getNode_kwargs).beginSettingsTransaction() - - if args.commit_edit: - closeNow = True - interface.getNode(args.dest, False, **getNode_kwargs).commitSettingsTransaction() - - if args.factory_reset or args.factory_reset_device: - closeNow = True - waitForAckNak = True - - full = bool(args.factory_reset_device) - interface.getNode(args.dest, False, **getNode_kwargs).factoryReset(full=full) - - if args.remove_node: - closeNow = True - waitForAckNak = True - interface.getNode(args.dest, False, **getNode_kwargs).removeNode(args.remove_node) - - if args.set_favorite_node: - closeNow = True - waitForAckNak = True - interface.getNode(args.dest, False, **getNode_kwargs).setFavorite(args.set_favorite_node) - - if args.remove_favorite_node: - closeNow = True - waitForAckNak = True - interface.getNode(args.dest, False, **getNode_kwargs).removeFavorite(args.remove_favorite_node) - - if args.set_ignored_node: - closeNow = True - waitForAckNak = True - interface.getNode(args.dest, False, **getNode_kwargs).setIgnored(args.set_ignored_node) - - if args.remove_ignored_node: - closeNow = True - waitForAckNak = True - interface.getNode(args.dest, False, **getNode_kwargs).removeIgnored(args.remove_ignored_node) - - if args.reset_nodedb: - closeNow = True - waitForAckNak = True - interface.getNode(args.dest, False, **getNode_kwargs).resetNodeDb() - - if args.sendtext: - closeNow = True - channelIndex = mt_config.channel_index or 0 - if checkChannel(interface, channelIndex): - print( - f"Sending text message {args.sendtext} to {args.dest} on channelIndex:{channelIndex}" - f" {'using PRIVATE_APP port' if args.private else ''}" - ) - interface.sendText( - args.sendtext, - args.dest, - wantAck=True, - channelIndex=channelIndex, - onResponse=interface.getNode(args.dest, False, **getNode_kwargs).onAckNak, - portNum=portnums_pb2.PortNum.PRIVATE_APP if args.private else portnums_pb2.PortNum.TEXT_MESSAGE_APP - ) - else: - meshtastic.util.our_exit( - f"Warning: {channelIndex} is not a valid channel. Channel must not be DISABLED." - ) - - if args.traceroute: - loraConfig = getattr(interface.localNode.localConfig, "lora") - hopLimit = getattr(loraConfig, "hop_limit") - dest = str(args.traceroute) - channelIndex = mt_config.channel_index or 0 - if checkChannel(interface, channelIndex): - print( - f"Sending traceroute request to {dest} on channelIndex:{channelIndex} (this could take a while)" - ) - interface.sendTraceRoute(dest, hopLimit, channelIndex=channelIndex) - - if args.request_telemetry: - if args.dest == BROADCAST_ADDR: - meshtastic.util.our_exit("Warning: Must use a destination node ID.") - else: - channelIndex = mt_config.channel_index or 0 - if checkChannel(interface, channelIndex): - telemMap = { - "device": "device_metrics", - "environment": "environment_metrics", - "air_quality": "air_quality_metrics", - "airquality": "air_quality_metrics", - "power": "power_metrics", - "localstats": "local_stats", - "local_stats": "local_stats", - } - telemType = telemMap.get(args.request_telemetry, "device_metrics") - print( - f"Sending {telemType} telemetry request to {args.dest} on channelIndex:{channelIndex} (this could take a while)" - ) - interface.sendTelemetry( - destinationId=args.dest, - wantResponse=True, - channelIndex=channelIndex, - telemetryType=telemType, - ) - - if args.request_position: - if args.dest == BROADCAST_ADDR: - meshtastic.util.our_exit("Warning: Must use a destination node ID.") - else: - channelIndex = mt_config.channel_index or 0 - if checkChannel(interface, channelIndex): - print( - f"Sending position request to {args.dest} on channelIndex:{channelIndex} (this could take a while)" - ) - interface.sendPosition( - destinationId=args.dest, - wantResponse=True, - channelIndex=channelIndex, - ) - - if args.gpio_wrb or args.gpio_rd or args.gpio_watch: - if args.dest == BROADCAST_ADDR: - meshtastic.util.our_exit("Warning: Must use a destination node ID.") - else: - rhc = remote_hardware.RemoteHardwareClient(interface) - - if args.gpio_wrb: - bitmask = 0 - bitval = 0 - for wrpair in args.gpio_wrb or []: - bitmask |= 1 << int(wrpair[0]) - bitval |= int(wrpair[1]) << int(wrpair[0]) - print( - f"Writing GPIO mask 0x{bitmask:x} with value 0x{bitval:x} to {args.dest}" - ) - rhc.writeGPIOs(args.dest, bitmask, bitval) - closeNow = True - - if args.gpio_rd: - bitmask = int(args.gpio_rd, 16) - print(f"Reading GPIO mask 0x{bitmask:x} from {args.dest}") - interface.mask = bitmask - rhc.readGPIOs(args.dest, bitmask, None) - # wait up to X seconds for a response - for _ in range(10): - time.sleep(1) - if interface.gotResponse: - break - logging.debug(f"end of gpio_rd") - - if args.gpio_watch: - bitmask = int(args.gpio_watch, 16) - print( - f"Watching GPIO mask 0x{bitmask:x} from {args.dest}. Press ctrl-c to exit" - ) - while True: - rhc.watchGPIOs(args.dest, bitmask) - time.sleep(1) - - # handle settings - if args.set: - closeNow = True - waitForAckNak = True - node = interface.getNode(args.dest, False, **getNode_kwargs) - - # Handle the int/float/bool arguments - pref = None - fields = set() - for pref in args.set: - found = False - field = splitCompoundName(pref[0].lower())[0] - for config in [node.localConfig, node.moduleConfig]: - config_type = config.DESCRIPTOR.fields_by_name.get(field) - if config_type: - if len(config.ListFields()) == 0: - node.requestConfig( - config.DESCRIPTOR.fields_by_name.get(field) - ) - found = setPref(config, pref[0], pref[1]) - if found: - fields.add(field) - break - - if found: - print("Writing modified preferences to device") - if len(fields) > 1: - print("Using a configuration transaction") - node.beginSettingsTransaction() - for field in fields: - print(f"Writing {field} configuration to device") - node.writeConfig(field) - if len(fields) > 1: - node.commitSettingsTransaction() - else: - if mt_config.camel_case: - print( - f"{node.localConfig.__class__.__name__} and {node.moduleConfig.__class__.__name__} do not have an attribute {pref[0]}." - ) - else: - print( - f"{node.localConfig.__class__.__name__} and {node.moduleConfig.__class__.__name__} do not have attribute {pref[0]}." - ) - print("Choices are...") - printConfig(node.localConfig) - printConfig(node.moduleConfig) - - if args.configure: - with open(args.configure[0], encoding="utf8") as file: - configuration = yaml.safe_load(file) - closeNow = True - - interface.getNode(args.dest, False, **getNode_kwargs).beginSettingsTransaction() - - 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"]) - 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']}" - ) - waitForAckNak = True - interface.getNode(args.dest, False, **getNode_kwargs).setOwner( - long_name=None, short_name=configuration["owner_short"] - ) - 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']}" - ) - waitForAckNak = True - interface.getNode(args.dest, False, **getNode_kwargs).setOwner( - long_name=None, short_name=configuration["ownerShort"] - ) - 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") - - if args.export_config: - if args.dest != BROADCAST_ADDR: - print("Exporting configuration of remote nodes is not supported.") - return - - closeNow = True - config_txt = export_config(interface) - - if args.export_config == "-": - # Output to stdout (preserves legacy use of `> file.yaml`) - print(config_txt) - else: - try: - with open(args.export_config, "w", encoding="utf-8") as f: - f.write(config_txt) - print(f"Exported configuration to {args.export_config}") - except Exception as e: - meshtastic.util.our_exit(f"ERROR: Failed to write config file: {e}") - - if args.ch_set_url: - closeNow = True - interface.getNode(args.dest, **getNode_kwargs).setURL(args.ch_set_url, addOnly=False) - - # handle changing channels - - if args.ch_add_url: - closeNow = True - interface.getNode(args.dest, **getNode_kwargs).setURL(args.ch_add_url, addOnly=True) - - if args.ch_add: - channelIndex = mt_config.channel_index - if channelIndex is not None: - # Since we set the channel index after adding a channel, don't allow --ch-index - meshtastic.util.our_exit( - "Warning: '--ch-add' and '--ch-index' are incompatible. Channel not added." - ) - closeNow = True - if len(args.ch_add) > 10: - meshtastic.util.our_exit( - "Warning: Channel name must be shorter. Channel not added." - ) - n = interface.getNode(args.dest, **getNode_kwargs) - ch = n.getChannelByName(args.ch_add) - if ch: - meshtastic.util.our_exit( - f"Warning: This node already has a '{args.ch_add}' channel. No changes were made." - ) - else: - # get the first channel that is disabled (i.e., available) - ch = n.getDisabledChannel() - if not ch: - meshtastic.util.our_exit("Warning: No free channels were found") - chs = channel_pb2.ChannelSettings() - chs.psk = meshtastic.util.genPSK256() - chs.name = args.ch_add - ch.settings.CopyFrom(chs) - ch.role = channel_pb2.Channel.Role.SECONDARY - print(f"Writing modified channels to device") - n.writeChannel(ch.index) - if channelIndex is None: - print( - f"Setting newly-added channel's {ch.index} as '--ch-index' for further modifications" - ) - mt_config.channel_index = ch.index - - if args.ch_del: - closeNow = True - - channelIndex = mt_config.channel_index - if channelIndex is None: - meshtastic.util.our_exit( - "Warning: Need to specify '--ch-index' for '--ch-del'.", 1 - ) - else: - if channelIndex == 0: - meshtastic.util.our_exit( - "Warning: Cannot delete primary channel.", 1 - ) - else: - print(f"Deleting channel {channelIndex}") - ch = interface.getNode(args.dest, **getNode_kwargs).deleteChannel(channelIndex) - - def setSimpleConfig(modem_preset): - """Set one of the simple modem_config""" - channelIndex = mt_config.channel_index - if channelIndex is not None and channelIndex > 0: - meshtastic.util.our_exit( - "Warning: Cannot set modem preset for non-primary channel", 1 - ) - # Overwrite modem_preset - node = interface.getNode(args.dest, False, **getNode_kwargs) - if len(node.localConfig.ListFields()) == 0: - node.requestConfig(node.localConfig.DESCRIPTOR.fields_by_name.get("lora")) - node.localConfig.lora.modem_preset = modem_preset - node.writeConfig("lora") - - # handle the simple radio set commands - if args.ch_vlongslow: - setSimpleConfig(config_pb2.Config.LoRaConfig.ModemPreset.VERY_LONG_SLOW) - - if args.ch_longslow: - setSimpleConfig(config_pb2.Config.LoRaConfig.ModemPreset.LONG_SLOW) - - if args.ch_longfast: - setSimpleConfig(config_pb2.Config.LoRaConfig.ModemPreset.LONG_FAST) - - if args.ch_medslow: - setSimpleConfig(config_pb2.Config.LoRaConfig.ModemPreset.MEDIUM_SLOW) - - if args.ch_medfast: - setSimpleConfig(config_pb2.Config.LoRaConfig.ModemPreset.MEDIUM_FAST) - - if args.ch_shortslow: - setSimpleConfig(config_pb2.Config.LoRaConfig.ModemPreset.SHORT_SLOW) - - if args.ch_shortfast: - setSimpleConfig(config_pb2.Config.LoRaConfig.ModemPreset.SHORT_FAST) - - if args.ch_set or args.ch_enable or args.ch_disable: - closeNow = True - - channelIndex = mt_config.channel_index - if channelIndex is None: - meshtastic.util.our_exit("Warning: Need to specify '--ch-index'.", 1) - node = interface.getNode(args.dest, **getNode_kwargs) - ch = node.channels[channelIndex] - - if args.ch_enable or args.ch_disable: - print( - "Warning: --ch-enable and --ch-disable can produce noncontiguous channels, " - "which can cause errors in some clients. Whenever possible, use --ch-add and --ch-del instead." - ) - if channelIndex == 0: - meshtastic.util.our_exit( - "Warning: Cannot enable/disable PRIMARY channel." - ) - - enable = True # default to enable - if args.ch_enable: - enable = True - if args.ch_disable: - enable = False - - # Handle the channel settings - for pref in args.ch_set or []: - if pref[0] == "psk": - found = True - ch.settings.psk = meshtastic.util.fromPSK(pref[1]) - else: - found = setPref(ch.settings, pref[0], pref[1]) - if not found: - category_settings = ["module_settings"] - print( - f"{ch.settings.__class__.__name__} does not have an attribute {pref[0]}." - ) - print("Choices are...") - for field in ch.settings.DESCRIPTOR.fields: - if field.name not in category_settings: - print(f"{field.name}") - else: - print(f"{field.name}:") - config = ch.settings.DESCRIPTOR.fields_by_name.get( - field.name - ) - names = [] - for sub_field in config.message_type.fields: - tmp_name = f"{field.name}.{sub_field.name}" - names.append(tmp_name) - for temp_name in sorted(names): - print(f" {temp_name}") - - enable = True # If we set any pref, assume the user wants to enable the channel - - if enable: - ch.role = ( - channel_pb2.Channel.Role.PRIMARY - if (channelIndex == 0) - else channel_pb2.Channel.Role.SECONDARY - ) - else: - ch.role = channel_pb2.Channel.Role.DISABLED - - print(f"Writing modified channels to device") - node.writeChannel(channelIndex) - - if args.get_canned_message: - closeNow = True - print("") - messages = interface.getNode(args.dest, **getNode_kwargs).get_canned_message() - print(f"canned_plugin_message:{messages}") - - if args.get_ringtone: - closeNow = True - print("") - ringtone = interface.getNode(args.dest, **getNode_kwargs).get_ringtone() - print(f"ringtone:{ringtone}") - - if args.info: - print("") - # If we aren't trying to talk to our local node, don't show it - if args.dest == BROADCAST_ADDR: - interface.showInfo() - print("") - interface.getNode(args.dest, **getNode_kwargs).showInfo() - closeNow = True - print("") - pypi_version = meshtastic.util.check_if_newer_version() - if pypi_version: - print( - f"*** A newer version v{pypi_version} is available!" - ' Consider running "pip install --upgrade meshtastic" ***\n' - ) - else: - print("Showing info of remote node is not supported.") - print( - "Use the '--get' command for a specific configuration (e.g. 'lora') instead." - ) - - if args.get: - closeNow = True - node = interface.getNode(args.dest, False, **getNode_kwargs) - for pref in args.get: - found = getPref(node, pref[0]) - - if found: - print("Completed getting preferences") - - if args.nodes: - closeNow = True - if args.dest != BROADCAST_ADDR: - print("Showing node list of a remote node is not supported.") - return - interface.showNodes(True, args.show_fields) - - if args.show_fields and not args.nodes: - print("--show-fields 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) - if args.qr_all: - urldesc = "Complete URL (includes all channels)" - else: - urldesc = "Primary channel URL" - print(f"{urldesc}: {url}") - if pyqrcode is not None: - qr = pyqrcode.create(url) - print(qr.terminal()) - else: - print("Install pyqrcode to view a QR code printed to terminal.") - - log_set: Optional = None # type: ignore[annotation-unchecked] - # we need to keep a reference to the logset so it doesn't get GCed early - - if args.slog or args.power_stress: - if have_powermon: - # Setup loggers - global meter # pylint: disable=global-variable-not-assigned - log_set = LogSet( - interface, args.slog if args.slog != "default" else None, meter - ) - - if args.power_stress: - stress = PowerStress(interface) - stress.run() - closeNow = True # exit immediately after stress test - else: - meshtastic.util.our_exit("The powermon module could not be loaded. " - "You may need to run `poetry install --with powermon`. " - "Import Error was: " + powermon_exception) - - - if args.listen: - closeNow = False - - have_tunnel = platform.system() == "Linux" - if have_tunnel and args.tunnel: - if args.dest != BROADCAST_ADDR: - print("A tunnel can only be created using the local node.") - return - # pylint: disable=C0415 - from . import tunnel - - # Even if others said we could close, stay open if the user asked for a tunnel - closeNow = False - if interface.noProto: - logging.warning(f"Not starting Tunnel - disabled by noProto") - else: - if args.tunnel_net: - tunnel.Tunnel(interface, subnet=args.tunnel_net) - else: - tunnel.Tunnel(interface) - - if args.ack or (args.dest != BROADCAST_ADDR and waitForAckNak): - print( - f"Waiting for an acknowledgment from remote node (this could take a while)" - ) - interface.getNode(args.dest, False, **getNode_kwargs).iface.waitForAckNak() - - if args.wait_to_disconnect: - print(f"Waiting {args.wait_to_disconnect} seconds before disconnecting") - time.sleep(int(args.wait_to_disconnect)) - - # 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 - - # Close any structured logs after we've done all of our API operations - if log_set: - log_set.close() - - except Exception as ex: - print(f"Aborting due to: {ex}") - interface.close() # close the connection now, so that our app exits - sys.exit(1) - - -def printConfig(config) -> None: - """print configuration""" - objDesc = config.DESCRIPTOR - for config_section in objDesc.fields: - if config_section.name != "version": - config = objDesc.fields_by_name.get(config_section.name) - print(f"{config_section.name}:") - names = [] - for field in config.message_type.fields: - tmp_name = f"{config_section.name}.{field.name}" - if mt_config.camel_case: - tmp_name = meshtastic.util.snake_to_camel(tmp_name) - names.append(tmp_name) - for temp_name in sorted(names): - print(f" {temp_name}") - - -def onNode(node) -> None: - """Callback invoked when the node DB changes""" - print(f"Node changed: {node}") - - -def subscribe() -> None: - """Subscribe to the topics the user probably wants to see, prints output to stdout""" - pub.subscribe(onReceive, "meshtastic.receive") - # pub.subscribe(onConnection, "meshtastic.connection") - - # We now call onConnected from main - # pub.subscribe(onConnected, "meshtastic.connection.established") - - # pub.subscribe(onNode, "meshtastic.node") - -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 true_defaults: - d = config_dict - for key in path[:-1]: - if key not in d or not isinstance(d[key], dict): - d[key] = {} - d = d[key] - if path[-1] not in d: - d[path[-1]] = False - -def export_config(interface) -> str: - """used in --export-config""" - configObj = {} - - # A list of configuration keys that should be set to False if they are missing - true_defaults = { - ("bluetooth", "enabled"), - ("lora", "sx126xRxBoostedGain"), - ("lora", "txEnabled"), - ("lora", "usePreset"), - ("position", "positionBroadcastSmartEnabled"), - ("security", "serialEnabled"), - ("mqtt", "encryptionEnabled"), - } - - owner = interface.getLongName() - owner_short = interface.getShortName() - channel_url = interface.localNode.getURL() - myinfo = interface.getMyNodeInfo() - canned_messages = interface.getCannedMessage() - ringtone = interface.getRingtone() - pos = myinfo.get("position") - lat = None - lon = None - alt = None - if pos: - lat = pos.get("latitude") - lon = pos.get("longitude") - alt = pos.get("altitude") - - if owner: - configObj["owner"] = owner - if owner_short: - configObj["owner_short"] = owner_short - if channel_url: - if mt_config.camel_case: - configObj["channelUrl"] = channel_url - else: - configObj["channel_url"] = channel_url - if canned_messages: - configObj["canned_messages"] = canned_messages - 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 - if lat or lon: - configObj["location"] = {"lat": lat or float(0), "lon": lon or float(0)} - 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 - if config: - # Convert inner keys to correct snake/camelCase - prefs = {} - for pref in config: - if mt_config.camel_case: - prefs[meshtastic.util.snake_to_camel(pref)] = config[pref] - else: - prefs[pref] = config[pref] - # mark base64 encoded fields as such - if pref == "security": - if 'privateKey' in prefs[pref]: - prefs[pref]['privateKey'] = 'base64:' + prefs[pref]['privateKey'] - if 'publicKey' in prefs[pref]: - prefs[pref]['publicKey'] = 'base64:' + prefs[pref]['publicKey'] - if 'adminKey' in prefs[pref]: - for i in range(len(prefs[pref]['adminKey'])): - prefs[pref]['adminKey'][i] = 'base64:' + prefs[pref]['adminKey'][i] - if mt_config.camel_case: - configObj["config"] = config #Identical command here and 2 lines below? - else: - configObj["config"] = config - - set_missing_flags_false(configObj["config"], true_defaults) - - module_config = MessageToDict(interface.localNode.moduleConfig) - if module_config: - # Convert inner keys to correct snake/camelCase - prefs = {} - 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 - - 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 - - -def create_power_meter(): - """Setup the power meter.""" - - global meter # pylint: disable=global-statement - args = mt_config.args - - # If the user specified a voltage, make sure it is valid - v = 0.0 - if args.power_voltage: - v = float(args.power_voltage) - if v < 0.8 or v > 5.0: - meshtastic.util.our_exit("Voltage must be between 0.8 and 5.0") - - if args.power_riden: - meter = RidenPowerSupply(args.power_riden) - elif args.power_ppk2_supply or args.power_ppk2_meter: - meter = PPK2PowerSupply() - assert v > 0, "Voltage must be specified for PPK2" - meter.v = v # PPK2 requires setting voltage before selecting supply mode - meter.setIsSupply(args.power_ppk2_supply) - elif args.power_sim: - meter = SimPowerSupply() - - if meter and v: - logging.info(f"Setting power supply to {v} volts") - meter.v = v - meter.powerOn() - - if args.power_wait: - input("Powered on, press enter to continue...") - else: - logging.info("Powered-on, waiting for device to boot") - time.sleep(5) - - -def common(): - """Shared code for all of our command line wrappers.""" - 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", - ) - - if len(sys.argv) == 1: - parser.print_help(sys.stderr) - meshtastic.util.our_exit("", 1) - else: - if args.support: - meshtastic.util.support_info() - meshtastic.util.our_exit("", 0) - - # 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() - if not stripped_long_name: - meshtastic.util.our_exit("ERROR: Long Name cannot be empty or contain only whitespace characters") - - if hasattr(args, 'set_owner_short') and args.set_owner_short is not None: - stripped_short_name = args.set_owner_short.strip() - if not stripped_short_name: - meshtastic.util.our_exit("ERROR: Short Name cannot be empty or contain only whitespace characters") - - if hasattr(args, 'set_ham') and args.set_ham is not None: - stripped_ham_name = args.set_ham.strip() - if not stripped_ham_name: - meshtastic.util.our_exit("ERROR: Ham radio callsign cannot be empty or contain only whitespace characters") - - if have_powermon: - create_power_meter() - - if args.ch_index is not None: - channelIndex = int(args.ch_index) - mt_config.channel_index = channelIndex - - if not args.dest: - args.dest = BROADCAST_ADDR - - if not args.seriallog: - if args.noproto: - args.seriallog = "stdout" - else: - args.seriallog = "none" # assume no debug output in this case - - if args.deprecated is not None: - logging.error( - "This option has been deprecated, see help below for the correct replacement..." - ) - parser.print_help(sys.stderr) - meshtastic.util.our_exit("", 1) - elif args.test: - if not have_test: - meshtastic.util.our_exit("Test module could not be important. Ensure you have the 'dotmap' module installed.") - else: - result = meshtastic.test.testAll() - if not result: - meshtastic.util.our_exit("Warning: Test was not successful.") - else: - meshtastic.util.our_exit("Test was a success.", 0) - else: - if args.seriallog == "stdout": - logfile = sys.stdout - elif args.seriallog == "none": - args.seriallog = None - logging.debug("Not logging serial output") - logfile = None - else: - logging.info(f"Logging serial output to {args.seriallog}") - # Note: using "line buffering" - # pylint: disable=R1732 - logfile = open(args.seriallog, "w+", buffering=1, encoding="utf8") - mt_config.logfile = logfile - - subscribe() - if args.ble_scan: - logging.debug("BLE scan starting") - for x in BLEInterface.scan(): - print(f"Found: name='{x.name}' address='{x.address}'") - meshtastic.util.our_exit("BLE scan finished", 0) - elif args.ble: - client = BLEInterface( - args.ble if args.ble != "any" else None, - debugOut=logfile, - noProto=args.noproto, - noNodes=args.no_nodes, - ) - elif args.host: - try: - if ":" in args.host: - tcp_hostname, tcp_port = args.host.split(':') - else: - tcp_hostname = args.host - tcp_port = meshtastic.tcp_interface.DEFAULT_TCP_PORT - client = meshtastic.tcp_interface.TCPInterface( - tcp_hostname, - portNumber=tcp_port, - debugOut=logfile, - noProto=args.noproto, - noNodes=args.no_nodes, - ) - except Exception as ex: - meshtastic.util.our_exit(f"Error connecting to {args.host}:{ex}", 1) - else: - try: - client = meshtastic.serial_interface.SerialInterface( - args.port, - debugOut=logfile, - noProto=args.noproto, - noNodes=args.no_nodes, - ) - except FileNotFoundError: - # Handle the case where the serial device is not found - message = ( - f"File Not Found Error:\n" - ) - message += f" The serial device at '{args.port}' was not found.\n" - message += " Please check the following:\n" - message += " 1. Is the device connected properly?\n" - message += " 2. Is the correct serial port specified?\n" - message += " 3. Are the necessary drivers installed?\n" - message += " 4. Are you using a **power-only USB cable**? A power-only cable cannot transmit data.\n" - message += " Ensure you are using a **data-capable USB cable**.\n" - meshtastic.util.our_exit(message, 1) - except PermissionError as ex: - username = os.getlogin() - message = "Permission Error:\n" - message += ( - " Need to add yourself to the 'dialout' group by running:\n" - ) - message += f" sudo usermod -a -G dialout {username}\n" - message += " After running that command, log out and re-login for it to take effect.\n" - message += f"Error was:{ex}" - meshtastic.util.our_exit(message) - except OSError as ex: - message = f"OS Error:\n" - message += " The serial device couldn't be opened, it might be in use by another process.\n" - message += " Please close any applications or webpages that may be using the device and try again.\n" - message += f"\nOriginal error: {ex}" - meshtastic.util.our_exit(message) - if client.devPath is None: - try: - client = meshtastic.tcp_interface.TCPInterface( - "localhost", - debugOut=logfile, - noProto=args.noproto, - noNodes=args.no_nodes, - ) - except Exception as ex: - meshtastic.util.our_exit( - f"Error connecting to localhost:{ex}", 1 - ) - - # We assume client is fully connected now - onConnected(client) - - have_tunnel = platform.system() == "Linux" - if ( - args.noproto - or args.reply - or (have_tunnel and args.tunnel) - or args.listen - ): # loop until someone presses ctrlc - try: - while True: - time.sleep(1000) - except KeyboardInterrupt: - logging.info("Exiting due to keyboard interrupt") - - # don't call exit, background threads might be running still - # sys.exit(0) - - -def addConnectionArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentParser: - """Add connection specification arguments""" - - outer = parser.add_argument_group( - "Connection", - "Optional arguments that specify how to connect to a Meshtastic device.", - ) - group = outer.add_mutually_exclusive_group() - group.add_argument( - "--port", - "--serial", - "-s", - help="The port of the device to connect to using serial, e.g. /dev/ttyUSB0. (defaults to trying to detect a port)", - nargs="?", - const=None, - default=None, - ) - - group.add_argument( - "--host", - "--tcp", - "-t", - help="Connect to a device using TCP, optionally passing hostname or IP address to use. (defaults to '%(const)s')", - nargs="?", - default=None, - const="localhost", - ) - - group.add_argument( - "--ble", - "-b", - help="Connect to a BLE device, optionally specifying a device name (defaults to '%(const)s')", - nargs="?", - default=None, - const="any", - ) - - outer.add_argument( - "--ble-scan", - help="Scan for Meshtastic BLE devices that may be available to connect to", - action="store_true", - ) - - return parser - -def addSelectionArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentParser: - """Add node/channel specification arguments""" - group = parser.add_argument_group( - "Selection", - "Arguments that select channels to use, destination nodes, etc." - ) - - group.add_argument( - "--dest", - help="The destination node id for any sent commands. If not set '^all' or '^local' is assumed." - "Use the node ID with a '!' or '0x' prefix or the node number.", - default=None, - metavar="!xxxxxxxx", - ) - - group.add_argument( - "--ch-index", - help="Set the specified channel index for channel-specific commands. Channels start at 0 (0 is the PRIMARY channel).", - action="store", - metavar="INDEX", - ) - - return parser - -def addImportExportArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentParser: - """Add import/export config arguments""" - group = parser.add_argument_group( - "Import/Export", - "Arguments that concern importing and exporting configuration of Meshtastic devices", - ) - - group.add_argument( - "--configure", - help="Specify a path to a yaml(.yml) file containing the desired settings for the connected device.", - action="append", - ) - group.add_argument( - "--export-config", - nargs="?", - const="-", # default to "-" if no value provided - metavar="FILE", - help="Export device config as YAML (to stdout if no file given)" - ) - return parser - -def addConfigArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentParser: - """Add arguments to do with configuring a device""" - - group = parser.add_argument_group( - "Configuration", - "Arguments that concern general configuration of Meshtastic devices", - ) - - group.add_argument( - "--get", - help=( - "Get a preferences field. Use an invalid field such as '0' to get a list of all fields." - " Can use either snake_case or camelCase format. (ex: 'power.ls_secs' or 'power.lsSecs')" - ), - nargs=1, - action="append", - metavar="FIELD" - ) - - group.add_argument( - "--set", - help=( - "Set a preferences field. Can use either snake_case or camelCase format." - " (ex: 'power.ls_secs' or 'power.lsSecs'). May be less reliable when" - " setting properties from more than one configuration section." - ), - nargs=2, - action="append", - metavar=("FIELD", "VALUE"), - ) - - group.add_argument( - "--begin-edit", - help="Tell the node to open a transaction to edit settings", - action="store_true", - ) - - group.add_argument( - "--commit-edit", - help="Tell the node to commit open settings transaction", - action="store_true", - ) - - group.add_argument( - "--get-canned-message", - help="Show the canned message plugin message", - action="store_true", - ) - - group.add_argument( - "--set-canned-message", - help="Set the canned messages plugin message (up to 200 characters).", - action="store", - ) - - group.add_argument( - "--get-ringtone", help="Show the stored ringtone", action="store_true" - ) - - group.add_argument( - "--set-ringtone", - help="Set the Notification Ringtone (up to 230 characters).", - action="store", - metavar="RINGTONE", - ) - - group.add_argument( - "--ch-vlongslow", - help="Change to the very long-range and slow modem preset", - action="store_true", - ) - - group.add_argument( - "--ch-longslow", - help="Change to the long-range and slow modem preset", - action="store_true", - ) - - group.add_argument( - "--ch-longfast", - help="Change to the long-range and fast modem preset", - action="store_true", - ) - - group.add_argument( - "--ch-medslow", - help="Change to the med-range and slow modem preset", - action="store_true", - ) - - group.add_argument( - "--ch-medfast", - help="Change to the med-range and fast modem preset", - action="store_true", - ) - - group.add_argument( - "--ch-shortslow", - help="Change to the short-range and slow modem preset", - action="store_true", - ) - - group.add_argument( - "--ch-shortfast", - help="Change to the short-range and fast modem preset", - action="store_true", - ) - - group.add_argument("--set-owner", help="Set device owner name", action="store") - - group.add_argument( - "--set-owner-short", help="Set device owner short name", action="store" - ) - - group.add_argument( - "--set-ham", help="Set licensed Ham ID and turn off encryption", action="store" - ) - - group.add_argument( - "--set-is-unmessageable", "--set-is-unmessagable", - help="Set if a node is messageable or not", action="store" - ) - - group.add_argument( - "--ch-set-url", "--seturl", - help="Set all channels and set LoRa config from a supplied URL", - metavar="URL", - action="store" - ) - - group.add_argument( - "--ch-add-url", - help="Add secondary channels and set LoRa config from a supplied URL", - metavar="URL", - default=None, - ) - - - return parser - -def addChannelConfigArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentParser: - """Add arguments to do with configuring channels""" - - group = parser.add_argument_group( - "Channel Configuration", - "Arguments that concern configuration of channels", - ) - - group.add_argument( - "--ch-add", - help="Add a secondary channel, you must specify a channel name", - default=None, - ) - - group.add_argument( - "--ch-del", help="Delete the ch-index channel", action="store_true" - ) - - group.add_argument( - "--ch-set", - help=( - "Set a channel parameter. To see channel settings available:'--ch-set all all --ch-index 0'. " - "Can set the 'psk' using this command. To disable encryption on primary channel:'--ch-set psk none --ch-index 0'. " - "To set encryption with a new random key on second channel:'--ch-set psk random --ch-index 1'. " - "To set encryption back to the default:'--ch-set psk default --ch-index 0'. To set encryption with your " - "own key: '--ch-set psk 0x1a1a1a1a2b2b2b2b1a1a1a1a2b2b2b2b1a1a1a1a2b2b2b2b1a1a1a1a2b2b2b2b --ch-index 0'." - ), - nargs=2, - action="append", - metavar=("FIELD", "VALUE"), - ) - - group.add_argument( - "--channel-fetch-attempts", - help=("Attempt to retrieve channel settings for --ch-set this many times before giving up. Default %(default)s."), - default=3, - type=int, - metavar="ATTEMPTS", - ) - - group.add_argument( - "--qr", - help=( - "Display a QR code for the node's primary channel (or all channels with --qr-all). " - "Also shows the shareable channel URL." - ), - action="store_true", - ) - - group.add_argument( - "--qr-all", - help="Display a QR code and URL for all of the node's channels.", - action="store_true", - ) - - group.add_argument( - "--ch-enable", - help="Enable the specified channel. Use --ch-add instead whenever possible.", - action="store_true", - dest="ch_enable", - default=False, - ) - - # Note: We are doing a double negative here (Do we want to disable? If ch_disable==True, then disable.) - group.add_argument( - "--ch-disable", - help="Disable the specified channel Use --ch-del instead whenever possible.", - action="store_true", - dest="ch_disable", - default=False, - ) - - return parser - -def addPositionConfigArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentParser: - """Add arguments to do with fixed positions and position config""" - - group = parser.add_argument_group( - "Position Configuration", - "Arguments that modify fixed position and other position-related configuration.", - ) - group.add_argument( - "--setalt", - help="Set device altitude in meters (allows use without GPS), and enable fixed position. " - "When providing positions with `--setlat`, `--setlon`, and `--setalt`, missing values will be set to 0.", - ) - - group.add_argument( - "--setlat", - help="Set device latitude (allows use without GPS), and enable fixed position. Accepts a decimal value or an integer premultiplied by 1e7. " - "When providing positions with `--setlat`, `--setlon`, and `--setalt`, missing values will be set to 0.", - ) - - group.add_argument( - "--setlon", - help="Set device longitude (allows use without GPS), and enable fixed position. Accepts a decimal value or an integer premultiplied by 1e7. " - "When providing positions with `--setlat`, `--setlon`, and `--setalt`, missing values will be set to 0.", - ) - - group.add_argument( - "--remove-position", - help="Clear any existing fixed position and disable fixed position.", - action="store_true", - ) - - group.add_argument( - "--pos-fields", - help="Specify fields to send when sending a position. Use no argument for a list of valid values. " - "Can pass multiple values as a space separated list like " - "this: '--pos-fields ALTITUDE HEADING SPEED'", - nargs="*", - action="store", - ) - return parser - -def addLocalActionArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentParser: - """Add arguments concerning local-only information & actions""" - group = parser.add_argument_group( - "Local Actions", - "Arguments that take actions or request information from the local node only.", - ) - - group.add_argument( - "--info", - help="Read and display the radio config information", - action="store_true", - ) - - group.add_argument( - "--nodes", - help="Print Node List in a pretty formatted table", - action="store_true", - ) - - group.add_argument( - "--show-fields", - help="Specify fields to show (comma-separated) when using --nodes", - type=lambda s: s.split(','), - default=None - ) - - return parser - -def addRemoteActionArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentParser: - """Add arguments concerning information & actions that may interact with the mesh""" - group = parser.add_argument_group( - "Remote Actions", - "Arguments that take actions or request information from either the local node or remote nodes via the mesh.", - ) - - group.add_argument( - "--sendtext", - help="Send a text message. Can specify a destination '--dest', use of PRIVATE_APP port '--private', and/or channel index '--ch-index'.", - metavar="TEXT", - ) - - group.add_argument( - "--private", - help="Optional argument for sending text messages to the PRIVATE_APP port. Use in combination with --sendtext.", - action="store_true" - ) - - group.add_argument( - "--traceroute", - help="Traceroute from connected node to a destination. " - "You need pass the destination ID as argument, like " - "this: '--traceroute !ba4bf9d0' | '--traceroute 0xba4bf9d0'" - "Only nodes with a shared channel can be traced.", - metavar="!xxxxxxxx", - ) - - group.add_argument( - "--request-telemetry", - help="Request telemetry from a node. With an argument, requests that specific type of telemetry. " - "You need to pass the destination ID as argument with '--dest'. " - "For repeaters, the nodeNum is required.", - action="store", - nargs="?", - default=None, - const="device", - metavar="TYPE", - ) - - group.add_argument( - "--request-position", - help="Request the position from a node. " - "You need to pass the destination ID as an argument with '--dest'. " - "For repeaters, the nodeNum is required.", - action="store_true", - ) - - group.add_argument( - "--reply", help="Reply to received messages", action="store_true" - ) - - return parser - -def addRemoteAdminArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentParser: - """Add arguments concerning admin actions that may interact with the mesh""" - - outer = parser.add_argument_group( - "Remote Admin Actions", - "Arguments that interact with local node or remote nodes via the mesh, requiring admin access.", - ) - - group = outer.add_mutually_exclusive_group() - - group.add_argument( - "--reboot", help="Tell the destination node to reboot", action="store_true" - ) - - group.add_argument( - "--reboot-ota", - help="Tell the destination node to reboot into factory firmware (ESP32)", - action="store_true", - ) - - group.add_argument( - "--enter-dfu", - help="Tell the destination node to enter DFU mode (NRF52)", - action="store_true", - ) - - group.add_argument( - "--shutdown", help="Tell the destination node to shutdown", action="store_true" - ) - - group.add_argument( - "--device-metadata", - help="Get the device metadata from the node", - action="store_true", - ) - - group.add_argument( - "--factory-reset", "--factory-reset-config", - help="Tell the destination node to install the default config, preserving BLE bonds & PKI keys", - action="store_true", - ) - - group.add_argument( - "--factory-reset-device", - help="Tell the destination node to install the default config and clear BLE bonds & PKI keys", - action="store_true", - ) - - group.add_argument( - "--remove-node", - help="Tell the destination node to remove a specific node from its NodeDB. " - "Use the node ID with a '!' or '0x' prefix or the node number.", - metavar="!xxxxxxxx" - ) - group.add_argument( - "--set-favorite-node", - help="Tell the destination node to set the specified node to be favorited on the NodeDB. " - "Use the node ID with a '!' or '0x' prefix or the node number.", - metavar="!xxxxxxxx" - ) - group.add_argument( - "--remove-favorite-node", - help="Tell the destination node to set the specified node to be un-favorited on the NodeDB. " - "Use the node ID with a '!' or '0x' prefix or the node number.", - metavar="!xxxxxxxx" - ) - group.add_argument( - "--set-ignored-node", - help="Tell the destination node to set the specified node to be ignored on the NodeDB. " - "Use the node ID with a '!' or '0x' prefix or the node number.", - metavar="!xxxxxxxx" - ) - group.add_argument( - "--remove-ignored-node", - help="Tell the destination node to set the specified node to be un-ignored on the NodeDB. " - "Use the node ID with a '!' or '0x' prefix or the node number.", - metavar="!xxxxxxxx" - ) - group.add_argument( - "--reset-nodedb", - help="Tell the destination node to clear its list of nodes", - action="store_true", - ) - - group.add_argument( - "--set-time", - help="Set the time to the provided unix epoch timestamp, or the system's current time if omitted or 0.", - action="store", - type=int, - nargs="?", - default=None, - const=0, - metavar="TIMESTAMP", - ) - - return parser - -def initParser(): - """Initialize the command line argument parsing.""" - parser = mt_config.parser - args = mt_config.args - - # The "Help" group includes the help option and other informational stuff about the CLI itself - outerHelpGroup = parser.add_argument_group("Help") - helpGroup = outerHelpGroup.add_mutually_exclusive_group() - helpGroup.add_argument( - "-h", "--help", action="help", help="show this help message and exit" - ) - - the_version = get_active_version() - helpGroup.add_argument("--version", action="version", version=f"{the_version}") - - helpGroup.add_argument( - "--support", - action="store_true", - help="Show support info (useful when troubleshooting an issue)", - ) - - # Connection arguments to indicate a device to connect to - parser = addConnectionArgs(parser) - - # Selection arguments to denote nodes and channels to use - parser = addSelectionArgs(parser) - - # Arguments concerning viewing and setting configuration - parser = addImportExportArgs(parser) - parser = addConfigArgs(parser) - parser = addPositionConfigArgs(parser) - parser = addChannelConfigArgs(parser) - - # Arguments for sending or requesting things from the local device - parser = addLocalActionArgs(parser) - - # Arguments for sending or requesting things from the mesh - parser = addRemoteActionArgs(parser) - parser = addRemoteAdminArgs(parser) - - # All the rest of the arguments - group = parser.add_argument_group("Miscellaneous arguments") - - group.add_argument( - "--seriallog", - help="Log device serial output to either 'none' or a filename to append to. Defaults to '%(const)s' if no filename specified.", - nargs="?", - const="stdout", - default=None, - metavar="LOG_DESTINATION", - ) - - group.add_argument( - "--ack", - help="Use in combination with compatible actions (e.g. --sendtext) to wait for an acknowledgment.", - action="store_true", - ) - - group.add_argument( - "--timeout", - help="How long to wait for replies. Default %(default)ss.", - default=300, - type=int, - metavar="SECONDS", - ) - - group.add_argument( - "--no-nodes", - help="Request that the node not send node info to the client. " - "Will break things that depend on the nodedb, but will speed up startup. Requires 2.3.11+ firmware.", - action="store_true", - ) - - group.add_argument( - "--debug", help="Show API library debug log messages", action="store_true" - ) - - group.add_argument( - "--test", - help="Run stress test against all connected Meshtastic devices", - action="store_true", - ) - - group.add_argument( - "--wait-to-disconnect", - help="How many seconds to wait before disconnecting from the device.", - const="5", - nargs="?", - action="store", - metavar="SECONDS", - ) - - group.add_argument( - "--noproto", - help="Don't start the API, just function as a dumb serial terminal.", - action="store_true", - ) - - group.add_argument( - "--listen", - help="Just stay open and listen to the protobuf stream. Enables debug logging.", - action="store_true", - ) - - group.add_argument( - "--no-time", - help="Deprecated. Retained for backwards compatibility in scripts, but is a no-op.", - action="store_true", - ) - - power_group = parser.add_argument_group( - "Power Testing", "Options for power testing/logging." - ) - - power_supply_group = power_group.add_mutually_exclusive_group() - - power_supply_group.add_argument( - "--power-riden", - help="Talk to a Riden power-supply. You must specify the device path, i.e. /dev/ttyUSBxxx", - ) - - power_supply_group.add_argument( - "--power-ppk2-meter", - help="Talk to a Nordic Power Profiler Kit 2 (in meter mode)", - action="store_true", - ) - - power_supply_group.add_argument( - "--power-ppk2-supply", - help="Talk to a Nordic Power Profiler Kit 2 (in supply mode)", - action="store_true", - ) - - power_supply_group.add_argument( - "--power-sim", - help="Use a simulated power meter (for development)", - action="store_true", - ) - - power_group.add_argument( - "--power-voltage", - help="Set the specified voltage on the power-supply. Be VERY careful, you can burn things up.", - ) - - power_group.add_argument( - "--power-stress", - help="Perform power monitor stress testing, to capture a power consumption profile for the device (also requires --power-mon)", - action="store_true", - ) - - power_group.add_argument( - "--power-wait", - help="Prompt the user to wait for device reset before looking for device serial ports (some boards kill power to USB serial port)", - action="store_true", - ) - - power_group.add_argument( - "--slog", - help="Store structured-logs (slogs) for this run, optionally you can specify a destination directory", - nargs="?", - default=None, - const="default", - ) - - - remoteHardwareArgs = parser.add_argument_group( - "Remote Hardware", "Arguments related to the Remote Hardware module" - ) - - remoteHardwareArgs.add_argument( - "--gpio-wrb", nargs=2, help="Set a particular GPIO # to 1 or 0", action="append" - ) - - remoteHardwareArgs.add_argument( - "--gpio-rd", help="Read from a GPIO mask (ex: '0x10')" - ) - - remoteHardwareArgs.add_argument( - "--gpio-watch", help="Start watching a GPIO mask for changes (ex: '0x10')" - ) - - have_tunnel = platform.system() == "Linux" - if have_tunnel: - tunnelArgs = parser.add_argument_group( - "Tunnel", "Arguments related to establishing a tunnel device over the mesh." - ) - tunnelArgs.add_argument( - "--tunnel", - action="store_true", - help="Create a TUN tunnel device for forwarding IP packets over the mesh", - ) - tunnelArgs.add_argument( - "--subnet", - dest="tunnel_net", - help="Sets the local-end subnet address for the TUN IP bridge. (ex: 10.115' which is the default)", - default=None, - ) - - parser.set_defaults(deprecated=None) - - if argcomplete is not None: - argcomplete.autocomplete(parser) - args = parser.parse_args() - mt_config.args = args - mt_config.parser = parser - - -def main(): - """Perform command line meshtastic operations""" - parser = argparse.ArgumentParser( - add_help=False, - epilog="If no connection arguments are specified, we search for a compatible serial device, " - "and if none is found, then attempt a TCP connection to localhost.", - ) - mt_config.parser = parser - initParser() - common() - logfile = mt_config.logfile - if logfile: - logfile.close() - - -def tunnelMain(): - """Run a meshtastic IP tunnel""" - parser = argparse.ArgumentParser(add_help=False) - mt_config.parser = parser - initParser() - args = mt_config.args - args.tunnel = True - mt_config.args = args - common() - - -if __name__ == "__main__": - main() diff --git a/meshtastic/tests/test_main.py b/meshtastic/tests/test_main.py deleted file mode 100644 index 1e24dc2dd..000000000 --- a/meshtastic/tests/test_main.py +++ /dev/null @@ -1,2911 +0,0 @@ -"""Meshtastic unit tests for __main__.py""" -# pylint: disable=C0302,W0613,R0917 - -import logging -import os -import platform -import re -import sys -from unittest.mock import mock_open, MagicMock, patch - -import pytest - -from meshtastic.__main__ import ( - export_config, - initParser, - main, - onConnection, - onNode, - onReceive, - tunnelMain, - set_missing_flags_false, -) -from meshtastic import mt_config - -from ..protobuf.channel_pb2 import Channel # pylint: disable=E0611 - -# from ..ble_interface import BLEInterface -from ..node import Node - -# from ..radioconfig_pb2 import UserPreferences -# import meshtastic.config_pb2 -from ..serial_interface import SerialInterface -from ..tcp_interface import TCPInterface - -# from ..remote_hardware import onGPIOreceive -# from ..config_pb2 import Config - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_init_parser_no_args(capsys): - """Test no arguments""" - sys.argv = [""] - mt_config.args = sys.argv - initParser() - out, err = capsys.readouterr() - assert out == "" - assert err == "" - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_init_parser_version(capsys): - """Test --version""" - sys.argv = ["", "--version"] - mt_config.args = sys.argv - - with pytest.raises(SystemExit) as pytest_wrapped_e: - initParser() - assert pytest_wrapped_e.type == SystemExit - assert pytest_wrapped_e.value.code == 0 - out, err = capsys.readouterr() - assert re.match(r"[0-9]+\.[0-9]+[\.a][0-9]", out) - assert err == "" - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_main_version(capsys): - """Test --version""" - sys.argv = ["", "--version"] - mt_config.args = sys.argv - - with pytest.raises(SystemExit) as pytest_wrapped_e: - main() - assert pytest_wrapped_e.type == SystemExit - assert pytest_wrapped_e.value.code == 0 - out, err = capsys.readouterr() - assert re.match(r"[0-9]+\.[0-9]+[\.a][0-9]", out) - assert err == "" - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_main_no_args(capsys): - """Test with no args""" - sys.argv = [""] - mt_config.args = sys.argv - - with pytest.raises(SystemExit) as pytest_wrapped_e: - main() - assert pytest_wrapped_e.type == SystemExit - assert pytest_wrapped_e.value.code == 1 - _, err = capsys.readouterr() - assert re.search(r"usage:", err, re.MULTILINE) - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_support(capsys): - """Test --support""" - sys.argv = ["", "--support"] - mt_config.args = sys.argv - - with pytest.raises(SystemExit) as pytest_wrapped_e: - main() - assert pytest_wrapped_e.type == SystemExit - assert pytest_wrapped_e.value.code == 0 - out, err = capsys.readouterr() - assert re.search(r"System", out, re.MULTILINE) - assert re.search(r"Platform", out, re.MULTILINE) - assert re.search(r"Machine", out, re.MULTILINE) - assert re.search(r"Executable", out, re.MULTILINE) - assert err == "" - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -@patch("meshtastic.util.findPorts", return_value=[]) -def test_main_ch_index_no_devices(patched_find_ports, capsys): - """Test --ch-index 1""" - sys.argv = ["", "--ch-index", "1"] - mt_config.args = sys.argv - - with pytest.raises(SystemExit) as pytest_wrapped_e: - main() - assert mt_config.channel_index == 1 - assert pytest_wrapped_e.type == SystemExit - assert pytest_wrapped_e.value.code == 1 - out, err = capsys.readouterr() - assert re.search(r"No.*Meshtastic.*device.*detected", out, re.MULTILINE) - assert err == "" - patched_find_ports.assert_called() - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -@patch("meshtastic.util.findPorts", return_value=[]) -def test_main_test_no_ports(patched_find_ports, capsys): - """Test --test with no hardware""" - sys.argv = ["", "--test"] - mt_config.args = sys.argv - - with pytest.raises(SystemExit) as pytest_wrapped_e: - main() - assert pytest_wrapped_e.type == SystemExit - assert pytest_wrapped_e.value.code == 1 - patched_find_ports.assert_called() - out, err = capsys.readouterr() - assert re.search( - r"Warning: Must have at least two devices connected to USB", out, re.MULTILINE - ) - assert err == "" - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -@patch("meshtastic.util.findPorts", return_value=["/dev/ttyFake1"]) -def test_main_test_one_port(patched_find_ports, capsys): - """Test --test with one fake port""" - sys.argv = ["", "--test"] - mt_config.args = sys.argv - - with pytest.raises(SystemExit) as pytest_wrapped_e: - main() - assert pytest_wrapped_e.type == SystemExit - assert pytest_wrapped_e.value.code == 1 - patched_find_ports.assert_called() - out, err = capsys.readouterr() - assert re.search( - r"Warning: Must have at least two devices connected to USB", out, re.MULTILINE - ) - assert err == "" - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -@patch("meshtastic.test.testAll", return_value=True) -def test_main_test_two_ports_success(patched_test_all, capsys): - """Test --test two fake ports and testAll() is a simulated success""" - sys.argv = ["", "--test"] - mt_config.args = sys.argv - - with pytest.raises(SystemExit) as pytest_wrapped_e: - main() - assert pytest_wrapped_e.type == SystemExit - assert pytest_wrapped_e.value.code == 0 - patched_test_all.assert_called() - out, err = capsys.readouterr() - assert re.search(r"Test was a success.", out, re.MULTILINE) - assert err == "" - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -@patch("meshtastic.test.testAll", return_value=False) -def test_main_test_two_ports_fails(patched_test_all, capsys): - """Test --test two fake ports and testAll() is a simulated failure""" - sys.argv = ["", "--test"] - mt_config.args = sys.argv - - with pytest.raises(SystemExit) as pytest_wrapped_e: - main() - assert pytest_wrapped_e.type == SystemExit - assert pytest_wrapped_e.value.code == 1 - patched_test_all.assert_called() - out, err = capsys.readouterr() - assert re.search(r"Test was not successful.", out, re.MULTILINE) - assert err == "" - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_info(capsys, caplog): - """Test --info""" - sys.argv = ["", "--info"] - mt_config.args = sys.argv - - iface = MagicMock(autospec=SerialInterface) - - def mock_showInfo(): - print("inside mocked showInfo") - - iface.showInfo.side_effect = mock_showInfo - with caplog.at_level(logging.DEBUG): - with patch( - "meshtastic.serial_interface.SerialInterface", return_value=iface - ) as mo: - main() - out, err = capsys.readouterr() - assert re.search(r"Connected to radio", out, re.MULTILINE) - assert re.search(r"inside mocked showInfo", out, re.MULTILINE) - assert err == "" - mo.assert_called() - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -@patch("os.getlogin") -def test_main_info_with_permission_error(patched_getlogin, capsys, caplog): - """Test --info""" - sys.argv = ["", "--info"] - mt_config.args = sys.argv - - patched_getlogin.return_value = "me" - - iface = MagicMock(autospec=SerialInterface) - with caplog.at_level(logging.DEBUG): - with pytest.raises(SystemExit) as pytest_wrapped_e: - with patch( - "meshtastic.serial_interface.SerialInterface", return_value=iface - ) as mo: - mo.side_effect = PermissionError("bla bla") - main() - assert pytest_wrapped_e.type == SystemExit - assert pytest_wrapped_e.value.code == 1 - out, err = capsys.readouterr() - patched_getlogin.assert_called() - assert re.search(r"Need to add yourself", out, re.MULTILINE) - assert err == "" - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_info_with_tcp_interface(capsys): - """Test --info""" - sys.argv = ["", "--info", "--host", "meshtastic.local"] - mt_config.args = sys.argv - - iface = MagicMock(autospec=TCPInterface) - - def mock_showInfo(): - print("inside mocked showInfo") - - iface.showInfo.side_effect = mock_showInfo - with patch("meshtastic.tcp_interface.TCPInterface", return_value=iface) as mo: - main() - out, err = capsys.readouterr() - assert re.search(r"Connected to radio", out, re.MULTILINE) - assert re.search(r"inside mocked showInfo", out, re.MULTILINE) - assert err == "" - mo.assert_called() - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_no_proto(capsys): - """Test --noproto (using --info for output)""" - sys.argv = ["", "--info", "--noproto"] - mt_config.args = sys.argv - - iface = MagicMock(autospec=SerialInterface) - - def mock_showInfo(): - print("inside mocked showInfo") - - iface.showInfo.side_effect = mock_showInfo - - # Override the time.sleep so there is no loop - def my_sleep(amount): - print(f"amount:{amount}") - sys.exit(0) - - with patch("meshtastic.serial_interface.SerialInterface", return_value=iface): - with patch("time.sleep", side_effect=my_sleep): - with pytest.raises(SystemExit) as pytest_wrapped_e: - main() - assert pytest_wrapped_e.type == SystemExit - assert pytest_wrapped_e.value.code == 0 - out, err = capsys.readouterr() - assert re.search(r"Connected to radio", out, re.MULTILINE) - assert re.search(r"inside mocked showInfo", out, re.MULTILINE) - assert err == "" - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_info_with_seriallog_stdout(capsys): - """Test --info""" - sys.argv = ["", "--info", "--seriallog", "stdout"] - mt_config.args = sys.argv - - iface = MagicMock(autospec=SerialInterface) - - def mock_showInfo(): - print("inside mocked showInfo") - - iface.showInfo.side_effect = mock_showInfo - with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: - main() - out, err = capsys.readouterr() - assert re.search(r"Connected to radio", out, re.MULTILINE) - assert re.search(r"inside mocked showInfo", out, re.MULTILINE) - assert err == "" - mo.assert_called() - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_info_with_seriallog_output_txt(capsys): - """Test --info""" - sys.argv = ["", "--info", "--seriallog", "output.txt"] - mt_config.args = sys.argv - - iface = MagicMock(autospec=SerialInterface) - - def mock_showInfo(): - print("inside mocked showInfo") - - iface.showInfo.side_effect = mock_showInfo - with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: - main() - out, err = capsys.readouterr() - assert re.search(r"Connected to radio", out, re.MULTILINE) - assert re.search(r"inside mocked showInfo", out, re.MULTILINE) - assert err == "" - mo.assert_called() - # do some cleanup - os.remove("output.txt") - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_qr(capsys): - """Test --qr""" - sys.argv = ["", "--qr"] - mt_config.args = sys.argv - - iface = MagicMock(autospec=SerialInterface) - # TODO: could mock/check url - with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: - main() - out, err = capsys.readouterr() - assert re.search(r"Connected to radio", out, re.MULTILINE) - assert re.search(r"Primary channel URL", out, re.MULTILINE) - # if a qr code is generated it will have lots of these - assert re.search(r"\[7m", out, re.MULTILINE) - assert err == "" - mo.assert_called() - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_onConnected_exception(capsys): - """Test the exception in onConnected""" - sys.argv = ["", "--qr"] - mt_config.args = sys.argv - - def throw_an_exception(junk): - raise Exception("Fake exception.") # pylint: disable=W0719 - - iface = MagicMock(autospec=SerialInterface) - with patch("meshtastic.serial_interface.SerialInterface", return_value=iface): - with patch("pyqrcode.create", side_effect=throw_an_exception): - with pytest.raises(SystemExit) as pytest_wrapped_e: - main() - out, err = capsys.readouterr() - assert re.search("Aborting due to: Fake exception", out, re.MULTILINE) - assert err == "" - assert pytest_wrapped_e.type == Exception - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_nodes(capsys): - """Test --nodes""" - sys.argv = ["", "--nodes"] - mt_config.args = sys.argv - - iface = MagicMock(autospec=SerialInterface) - - def mock_showNodes(includeSelf, showFields): - print(f"inside mocked showNodes: {includeSelf} {showFields}") - - iface.showNodes.side_effect = mock_showNodes - with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: - main() - out, err = capsys.readouterr() - assert re.search(r"Connected to radio", out, re.MULTILINE) - assert re.search(r"inside mocked showNodes", out, re.MULTILINE) - assert err == "" - mo.assert_called() - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_set_owner_to_bob(capsys): - """Test --set-owner bob""" - sys.argv = ["", "--set-owner", "bob"] - mt_config.args = sys.argv - - iface = MagicMock(autospec=SerialInterface) - with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: - main() - out, err = capsys.readouterr() - assert re.search(r"Connected to radio", out, re.MULTILINE) - assert re.search(r"Setting device owner to bob", out, re.MULTILINE) - assert err == "" - mo.assert_called() - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_set_owner_short_to_bob(capsys): - """Test --set-owner-short bob""" - sys.argv = ["", "--set-owner-short", "bob"] - mt_config.args = sys.argv - - iface = MagicMock(autospec=SerialInterface) - with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: - main() - out, err = capsys.readouterr() - assert re.search(r"Connected to radio", out, re.MULTILINE) - assert re.search(r"Setting device owner short to bob", out, re.MULTILINE) - assert err == "" - mo.assert_called() - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_set_is_unmessageable_to_true(capsys): - """Test --set-is-unmessageable true""" - sys.argv = ["", "--set-is-unmessageable", "true"] - mt_config.args = sys.argv - - iface = MagicMock(autospec=SerialInterface) - with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: - main() - out, err = capsys.readouterr() - assert re.search(r"Connected to radio", out, re.MULTILINE) - assert re.search(r"Setting device owner is_unmessageable to True", out, re.MULTILINE) - assert err == "" - mo.assert_called() - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_set_is_unmessagable_to_true(capsys): - """Test --set-is-unmessagable true""" - sys.argv = ["", "--set-is-unmessagable", "true"] - mt_config.args = sys.argv - - iface = MagicMock(autospec=SerialInterface) - with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: - main() - out, err = capsys.readouterr() - assert re.search(r"Connected to radio", out, re.MULTILINE) - assert re.search(r"Setting device owner is_unmessageable to True", out, re.MULTILINE) - assert err == "" - mo.assert_called() - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_set_canned_messages(capsys): - """Test --set-canned-message""" - sys.argv = ["", "--set-canned-message", "foo"] - mt_config.args = sys.argv - - iface = MagicMock(autospec=SerialInterface) - with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: - main() - out, err = capsys.readouterr() - assert re.search(r"Connected to radio", out, re.MULTILINE) - assert re.search(r"Setting canned plugin message to foo", out, re.MULTILINE) - assert err == "" - mo.assert_called() - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_get_canned_messages(capsys, caplog, iface_with_nodes): - """Test --get-canned-message""" - sys.argv = ["", "--get-canned-message"] - mt_config.args = sys.argv - - iface = iface_with_nodes - iface.localNode.cannedPluginMessage = "foo" - iface.devPath = "bar" - - with caplog.at_level(logging.DEBUG): - with patch( - "meshtastic.serial_interface.SerialInterface", return_value=iface - ) as mo: - main() - out, err = capsys.readouterr() - assert re.search(r"Connected to radio", out, re.MULTILINE) - assert re.search(r"canned_plugin_message:foo", out, re.MULTILINE) - assert err == "" - mo.assert_called() - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_set_ringtone(capsys): - """Test --set-ringtone""" - sys.argv = ["", "--set-ringtone", "foo,bar"] - mt_config.args = sys.argv - - iface = MagicMock(autospec=SerialInterface) - with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: - main() - out, err = capsys.readouterr() - assert re.search(r"Connected to radio", out, re.MULTILINE) - assert re.search(r"Setting ringtone to foo,bar", out, re.MULTILINE) - assert err == "" - mo.assert_called() - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_get_ringtone(capsys, caplog, iface_with_nodes): - """Test --get-ringtone""" - sys.argv = ["", "--get-ringtone"] - mt_config.args = sys.argv - - iface = iface_with_nodes - iface.devPath = "bar" - - mocked_node = MagicMock(autospec=Node) - mocked_node.get_ringtone.return_value = "foo,bar" - iface.localNode = mocked_node - - with caplog.at_level(logging.DEBUG): - with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: - main() - out, err = capsys.readouterr() - assert re.search(r"Connected to radio", out, re.MULTILINE) - assert re.search(r"ringtone:foo,bar", out, re.MULTILINE) - assert err == "" - mo.assert_called() - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_set_ham_to_KI123(capsys): - """Test --set-ham KI123""" - sys.argv = ["", "--set-ham", "KI123"] - mt_config.args = sys.argv - - mocked_node = MagicMock(autospec=Node) - - def mock_turnOffEncryptionOnPrimaryChannel(): - print("inside mocked turnOffEncryptionOnPrimaryChannel") - - def mock_setOwner(name, is_licensed): - print(f"inside mocked setOwner name:{name} is_licensed:{is_licensed}") - - mocked_node.turnOffEncryptionOnPrimaryChannel.side_effect = ( - mock_turnOffEncryptionOnPrimaryChannel - ) - mocked_node.setOwner.side_effect = mock_setOwner - - iface = MagicMock(autospec=SerialInterface) - iface.getNode.return_value = mocked_node - - with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: - main() - out, err = capsys.readouterr() - assert re.search(r"Connected to radio", out, re.MULTILINE) - assert re.search(r"Setting Ham ID to KI123", out, re.MULTILINE) - assert re.search(r"inside mocked setOwner", out, re.MULTILINE) - assert re.search( - r"inside mocked turnOffEncryptionOnPrimaryChannel", out, re.MULTILINE - ) - assert err == "" - mo.assert_called() - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_reboot(capsys): - """Test --reboot""" - sys.argv = ["", "--reboot"] - mt_config.args = sys.argv - - mocked_node = MagicMock(autospec=Node) - - def mock_reboot(): - print("inside mocked reboot") - - mocked_node.reboot.side_effect = mock_reboot - - iface = MagicMock(autospec=SerialInterface) - iface.getNode.return_value = mocked_node - - with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: - main() - out, err = capsys.readouterr() - assert re.search(r"Connected to radio", out, re.MULTILINE) - assert re.search(r"inside mocked reboot", out, re.MULTILINE) - assert err == "" - mo.assert_called() - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_shutdown(capsys): - """Test --shutdown""" - sys.argv = ["", "--shutdown"] - mt_config.args = sys.argv - - mocked_node = MagicMock(autospec=Node) - - def mock_shutdown(): - print("inside mocked shutdown") - - mocked_node.shutdown.side_effect = mock_shutdown - - iface = MagicMock(autospec=SerialInterface) - iface.getNode.return_value = mocked_node - - with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: - main() - out, err = capsys.readouterr() - assert re.search(r"Connected to radio", out, re.MULTILINE) - assert re.search(r"inside mocked shutdown", out, re.MULTILINE) - assert err == "" - mo.assert_called() - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_sendtext(capsys): - """Test --sendtext""" - sys.argv = ["", "--sendtext", "hello"] - mt_config.args = sys.argv - - iface = MagicMock(autospec=SerialInterface) - - def mock_sendText( - text, dest, wantAck=False, wantResponse=False, onResponse=None, channelIndex=0, portNum=0 - ): - print("inside mocked sendText") - print(f"{text} {dest} {wantAck} {wantResponse} {channelIndex} {portNum}") - - iface.sendText.side_effect = mock_sendText - - with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: - main() - out, err = capsys.readouterr() - assert re.search(r"Connected to radio", out, re.MULTILINE) - assert re.search(r"Sending text message", out, re.MULTILINE) - assert re.search(r"inside mocked sendText", out, re.MULTILINE) - assert err == "" - mo.assert_called() - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_sendtext_with_channel(capsys): - """Test --sendtext""" - sys.argv = ["", "--sendtext", "hello", "--ch-index", "1"] - mt_config.args = sys.argv - - iface = MagicMock(autospec=SerialInterface) - - def mock_sendText( - text, dest, wantAck=False, wantResponse=False, onResponse=None, channelIndex=0, portNum=0 - ): - print("inside mocked sendText") - print(f"{text} {dest} {wantAck} {wantResponse} {channelIndex} {portNum}") - - iface.sendText.side_effect = mock_sendText - - with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: - main() - out, err = capsys.readouterr() - assert re.search(r"Connected to radio", out, re.MULTILINE) - assert re.search(r"Sending text message", out, re.MULTILINE) - assert re.search(r"on channelIndex:1", out, re.MULTILINE) - assert re.search(r"inside mocked sendText", out, re.MULTILINE) - assert err == "" - mo.assert_called() - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_sendtext_with_invalid_channel(caplog, capsys): - """Test --sendtext""" - sys.argv = ["", "--sendtext", "hello", "--ch-index", "-1"] - mt_config.args = sys.argv - - iface = MagicMock(autospec=SerialInterface) - iface.localNode.getChannelByChannelIndex.return_value = None - - with caplog.at_level(logging.DEBUG): - with patch( - "meshtastic.serial_interface.SerialInterface", return_value=iface - ) as mo: - with pytest.raises(SystemExit) as pytest_wrapped_e: - main() - assert pytest_wrapped_e.type == SystemExit - assert pytest_wrapped_e.value.code == 1 - out, err = capsys.readouterr() - assert re.search(r"is not a valid channel", out, re.MULTILINE) - assert err == "" - mo.assert_called() - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_sendtext_with_invalid_channel_nine(caplog, capsys): - """Test --sendtext""" - sys.argv = ["", "--sendtext", "hello", "--ch-index", "9"] - mt_config.args = sys.argv - - iface = MagicMock(autospec=SerialInterface) - iface.localNode.getChannelByChannelIndex.return_value = None - - with caplog.at_level(logging.DEBUG): - with patch( - "meshtastic.serial_interface.SerialInterface", return_value=iface - ) as mo: - with pytest.raises(SystemExit) as pytest_wrapped_e: - main() - assert pytest_wrapped_e.type == SystemExit - assert pytest_wrapped_e.value.code == 1 - out, err = capsys.readouterr() - assert re.search(r"is not a valid channel", out, re.MULTILINE) - assert err == "" - mo.assert_called() - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -@patch("termios.tcsetattr") -@patch("termios.tcgetattr") -@patch("builtins.open", new_callable=mock_open, read_data="data") -@patch("serial.Serial") -@patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"]) -def test_main_sendtext_with_dest(mock_findPorts, mock_serial, mocked_open, mock_get, mock_set, capsys, caplog, iface_with_nodes): - """Test --sendtext with --dest""" - sys.argv = ["", "--sendtext", "hello", "--dest", "foo"] - mt_config.args = sys.argv - - #iface = iface_with_nodes - #iface.myInfo.my_node_num = 2475227164 - serialInterface = SerialInterface(noProto=True) - - mocked_channel = MagicMock(autospec=Channel) - serialInterface.localNode.getChannelByChannelIndex = mocked_channel - - with patch("meshtastic.serial_interface.SerialInterface", return_value=serialInterface): - with caplog.at_level(logging.DEBUG): - #with pytest.raises(SystemExit) as pytest_wrapped_e: - main() - #assert pytest_wrapped_e.type == SystemExit - #assert pytest_wrapped_e.value.code == 1 - out, err = capsys.readouterr() - assert re.search(r"Connected to radio", out, re.MULTILINE) - assert not re.search( - r"Warning: 0 is not a valid channel", out, re.MULTILINE - ) - assert not re.search( - r"There is a SECONDARY channel named 'admin'", out, re.MULTILINE - ) - print(out) - assert re.search(r"Not sending packet because", caplog.text, re.MULTILINE) - assert re.search(r"Warning: There were no self.nodes.", caplog.text, re.MULTILINE) - assert err == "" - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_removeposition_remote(capsys): - """Test --remove-position with a remote dest""" - sys.argv = ["", "--remove-position", "--dest", "!12345678"] - mt_config.args = sys.argv - iface = MagicMock(autospec=SerialInterface) - with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: - main() - out, err = capsys.readouterr() - assert re.search(r"Connected to radio", out, re.MULTILINE) - assert re.search(r"Removing fixed position and disabling fixed position setting", out, re.MULTILINE) - assert re.search(r"Waiting for an acknowledgment from remote node", out, re.MULTILINE) - assert err == "" - mo.assert_called() - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_setlat_remote(capsys): - """Test --setlat with a remote dest""" - sys.argv = ["", "--setlat", "37.5", "--dest", "!12345678"] - mt_config.args = sys.argv - iface = MagicMock(autospec=SerialInterface) - with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: - main() - out, err = capsys.readouterr() - assert re.search(r"Connected to radio", out, re.MULTILINE) - assert re.search(r"Setting device position and enabling fixed position setting", out, re.MULTILINE) - assert re.search(r"Waiting for an acknowledgment from remote node", out, re.MULTILINE) - assert err == "" - mo.assert_called() - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_removeposition(capsys): - """Test --remove-position""" - sys.argv = ["", "--remove-position"] - mt_config.args = sys.argv - - mocked_node = MagicMock(autospec=Node) - - def mock_removeFixedPosition(): - print("inside mocked removeFixedPosition") - - mocked_node.removeFixedPosition.side_effect = mock_removeFixedPosition - - iface = MagicMock(autospec=SerialInterface) - iface.getNode.return_value = mocked_node - - with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: - main() - out, err = capsys.readouterr() - assert re.search(r"Connected to radio", out, re.MULTILINE) - assert re.search(r"Removing fixed position", out, re.MULTILINE) - assert re.search(r"inside mocked removeFixedPosition", out, re.MULTILINE) - assert err == "" - mo.assert_called() - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_setlat(capsys): - """Test --setlat""" - sys.argv = ["", "--setlat", "37.5"] - mt_config.args = sys.argv - - mocked_node = MagicMock(autospec=Node) - - def mock_setFixedPosition(lat, lon, alt): - print("inside mocked setFixedPosition") - print(f"{lat} {lon} {alt}") - - mocked_node.setFixedPosition.side_effect = mock_setFixedPosition - - iface = MagicMock(autospec=SerialInterface) - iface.getNode.return_value = mocked_node - - with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: - main() - out, err = capsys.readouterr() - assert re.search(r"Connected to radio", out, re.MULTILINE) - assert re.search(r"Fixing latitude", out, re.MULTILINE) - assert re.search(r"Setting device position", out, re.MULTILINE) - assert re.search(r"inside mocked setFixedPosition", out, re.MULTILINE) - assert err == "" - mo.assert_called() - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_setlon(capsys): - """Test --setlon""" - sys.argv = ["", "--setlon", "-122.1"] - mt_config.args = sys.argv - - mocked_node = MagicMock(autospec=Node) - - def mock_setFixedPosition(lat, lon, alt): - print("inside mocked setFixedPosition") - print(f"{lat} {lon} {alt}") - - mocked_node.setFixedPosition.side_effect = mock_setFixedPosition - - iface = MagicMock(autospec=SerialInterface) - iface.getNode.return_value = mocked_node - - with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: - main() - out, err = capsys.readouterr() - assert re.search(r"Connected to radio", out, re.MULTILINE) - assert re.search(r"Fixing longitude", out, re.MULTILINE) - assert re.search(r"Setting device position", out, re.MULTILINE) - assert re.search(r"inside mocked setFixedPosition", out, re.MULTILINE) - assert err == "" - mo.assert_called() - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_setalt(capsys): - """Test --setalt""" - sys.argv = ["", "--setalt", "51"] - mt_config.args = sys.argv - - mocked_node = MagicMock(autospec=Node) - - def mock_setFixedPosition(lat, lon, alt): - print("inside mocked setFixedPosition") - print(f"{lat} {lon} {alt}") - - mocked_node.setFixedPosition.side_effect = mock_setFixedPosition - - iface = MagicMock(autospec=SerialInterface) - iface.getNode.return_value = mocked_node - - with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: - main() - out, err = capsys.readouterr() - assert re.search(r"Connected to radio", out, re.MULTILINE) - assert re.search(r"Fixing altitude", out, re.MULTILINE) - assert re.search(r"Setting device position", out, re.MULTILINE) - assert re.search(r"inside mocked setFixedPosition", out, re.MULTILINE) - assert err == "" - mo.assert_called() - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_seturl(capsys): - """Test --seturl (url used below is what is generated after a factory_reset)""" - sys.argv = ["", "--seturl", "https://www.meshtastic.org/d/#CgUYAyIBAQ"] - mt_config.args = sys.argv - - iface = MagicMock(autospec=SerialInterface) - with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: - main() - out, err = capsys.readouterr() - assert re.search(r"Connected to radio", out, re.MULTILINE) - assert err == "" - mo.assert_called() - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -@patch("termios.tcsetattr") -@patch("termios.tcgetattr") -@patch("builtins.open", new_callable=mock_open, read_data="data") -@patch("serial.Serial") -@patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"]) -def test_main_set_valid(mocked_findports, mocked_serial, mocked_open, mocked_get, mocked_set, capsys): - """Test --set with valid field""" - sys.argv = ["", "--set", "network.wifi_ssid", "foo"] - mt_config.args = sys.argv - - serialInterface = SerialInterface(noProto=True) - anode = Node(serialInterface, 1234567890, noProto=True) - serialInterface.localNode = anode - - 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) - assert re.search(r"Set network.wifi_ssid to foo", out, re.MULTILINE) - assert err == "" - mo.assert_called() - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -@patch("termios.tcsetattr") -@patch("termios.tcgetattr") -@patch("builtins.open", new_callable=mock_open, read_data="data") -@patch("serial.Serial") -@patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"]) -def test_main_set_valid_wifi_psk(mocked_findports, mocked_serial, mocked_open, mocked_get, mocked_set, capsys): - """Test --set with valid field""" - sys.argv = ["", "--set", "network.wifi_psk", "123456789"] - mt_config.args = sys.argv - - serialInterface = SerialInterface(noProto=True) - anode = Node(serialInterface, 1234567890, noProto=True) - serialInterface.localNode = anode - - 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) - assert re.search(r"Set network.wifi_psk to 123456789", out, re.MULTILINE) - assert err == "" - mo.assert_called() - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -@patch("termios.tcsetattr") -@patch("termios.tcgetattr") -@patch("builtins.open", new_callable=mock_open, read_data="data") -@patch("serial.Serial") -@patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"]) -def test_main_set_invalid_wifi_psk(mocked_findports, mocked_serial, mocked_open, mocked_get, mocked_set, capsys): - """Test --set with an invalid value (psk must be 8 or more characters)""" - sys.argv = ["", "--set", "network.wifi_psk", "1234567"] - mt_config.args = sys.argv - - serialInterface = SerialInterface(noProto=True) - anode = Node(serialInterface, 1234567890, noProto=True) - serialInterface.localNode = anode - - 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) - assert not re.search(r"Set network.wifi_psk to 1234567", out, re.MULTILINE) - assert re.search( - r"Warning: network.wifi_psk must be 8 or more characters.", out, re.MULTILINE - ) - assert err == "" - mo.assert_called() - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -@patch("termios.tcsetattr") -@patch("termios.tcgetattr") -@patch("builtins.open", new_callable=mock_open, read_data="data") -@patch("serial.Serial") -@patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"]) -def test_main_set_valid_camel_case(mocked_findports, mocked_serial, mocked_open, mocked_get, mocked_set, capsys): - """Test --set with valid field""" - sys.argv = ["", "--set", "network.wifi_ssid", "foo"] - mt_config.args = sys.argv - mt_config.camel_case = True - - serialInterface = SerialInterface(noProto=True) - anode = Node(serialInterface, 1234567890, noProto=True) - serialInterface.localNode = anode - - 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) - assert re.search(r"Set network.wifiSsid to foo", out, re.MULTILINE) - assert err == "" - mo.assert_called() - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -@patch("termios.tcsetattr") -@patch("termios.tcgetattr") -@patch("builtins.open", new_callable=mock_open, read_data="data") -@patch("serial.Serial") -@patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"]) -def test_main_set_with_invalid(mocked_findports, mocked_serial, mocked_open, mocked_get, mocked_set, capsys): - """Test --set with invalid field""" - sys.argv = ["", "--set", "foo", "foo"] - mt_config.args = sys.argv - - serialInterface = SerialInterface(noProto=True) - anode = Node(serialInterface, 1234567890, noProto=True) - serialInterface.localNode = anode - - 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) - assert re.search(r"do not have attribute foo", out, re.MULTILINE) - assert err == "" - mo.assert_called() - - -# TODO: write some negative --configure tests -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -@patch("termios.tcsetattr") -@patch("termios.tcgetattr") -@patch("builtins.open", new_callable=mock_open, read_data="data") -@patch("serial.Serial") -@patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"]) -def test_main_configure_with_snake_case(mocked_findports, mocked_serial, mocked_open, mocked_get, mocked_set, capsys): - """Test --configure with valid file""" - sys.argv = ["", "--configure", "example_config.yaml"] - mt_config.args = sys.argv - - serialInterface = SerialInterface(noProto=True) - anode = Node(serialInterface, 1234567890, noProto=True) - serialInterface.localNode = anode - - 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 err == "" - mo.assert_called() - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -@patch("termios.tcsetattr") -@patch("termios.tcgetattr") -@patch("builtins.open", new_callable=mock_open, read_data="data") -@patch("serial.Serial") -@patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"]) -def test_main_configure_with_camel_case_keys(mocked_findports, mocked_serial, mocked_open, mocked_get, mocked_set, capsys): - """Test --configure with valid file""" - sys.argv = ["", "--configure", "exampleConfig.yaml"] - mt_config.args = sys.argv - - serialInterface = SerialInterface(noProto=True) - anode = Node(serialInterface, 1234567890, noProto=True) - serialInterface.localNode = anode - - 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 err == "" - mo.assert_called() - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_ch_add_valid(capsys): - """Test --ch-add with valid channel name, and that channel name does not already exist""" - sys.argv = ["", "--ch-add", "testing"] - mt_config.args = sys.argv - - mocked_channel = MagicMock(autospec=Channel) - # TODO: figure out how to get it to print the channel name instead of MagicMock - - mocked_node = MagicMock(autospec=Node) - # set it up so we do not already have a channel named this - mocked_node.getChannelByName.return_value = False - # set it up so we have free channels - mocked_node.getDisabledChannel.return_value = mocked_channel - - iface = MagicMock(autospec=SerialInterface) - iface.getNode.return_value = mocked_node - - with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: - main() - out, err = capsys.readouterr() - assert re.search(r"Connected to radio", out, re.MULTILINE) - assert re.search(r"Writing modified channels to device", out, re.MULTILINE) - assert err == "" - mo.assert_called() - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_ch_add_invalid_name_too_long(capsys): - """Test --ch-add with invalid channel name, name too long""" - sys.argv = ["", "--ch-add", "testingtestingtesting"] - mt_config.args = sys.argv - - mocked_channel = MagicMock(autospec=Channel) - # TODO: figure out how to get it to print the channel name instead of MagicMock - - mocked_node = MagicMock(autospec=Node) - # set it up so we do not already have a channel named this - mocked_node.getChannelByName.return_value = False - # set it up so we have free channels - mocked_node.getDisabledChannel.return_value = mocked_channel - - iface = MagicMock(autospec=SerialInterface) - iface.getNode.return_value = mocked_node - - with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: - with pytest.raises(SystemExit) as pytest_wrapped_e: - main() - assert pytest_wrapped_e.type == SystemExit - assert pytest_wrapped_e.value.code == 1 - out, err = capsys.readouterr() - assert re.search(r"Connected to radio", out, re.MULTILINE) - assert re.search(r"Warning: Channel name must be shorter", out, re.MULTILINE) - assert err == "" - mo.assert_called() - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_ch_add_but_name_already_exists(capsys): - """Test --ch-add with a channel name that already exists""" - sys.argv = ["", "--ch-add", "testing"] - mt_config.args = sys.argv - - mocked_node = MagicMock(autospec=Node) - # set it up so we do not already have a channel named this - mocked_node.getChannelByName.return_value = True - - iface = MagicMock(autospec=SerialInterface) - iface.getNode.return_value = mocked_node - - with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: - with pytest.raises(SystemExit) as pytest_wrapped_e: - main() - assert pytest_wrapped_e.type == SystemExit - assert pytest_wrapped_e.value.code == 1 - out, err = capsys.readouterr() - assert re.search(r"Connected to radio", out, re.MULTILINE) - assert re.search(r"Warning: This node already has", out, re.MULTILINE) - assert err == "" - mo.assert_called() - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_ch_add_but_no_more_channels(capsys): - """Test --ch-add with but there are no more channels""" - sys.argv = ["", "--ch-add", "testing"] - mt_config.args = sys.argv - - mocked_node = MagicMock(autospec=Node) - # set it up so we do not already have a channel named this - mocked_node.getChannelByName.return_value = False - # set it up so we have free channels - mocked_node.getDisabledChannel.return_value = None - - iface = MagicMock(autospec=SerialInterface) - iface.getNode.return_value = mocked_node - - with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: - with pytest.raises(SystemExit) as pytest_wrapped_e: - main() - assert pytest_wrapped_e.type == SystemExit - assert pytest_wrapped_e.value.code == 1 - out, err = capsys.readouterr() - assert re.search(r"Connected to radio", out, re.MULTILINE) - assert re.search(r"Warning: No free channels were found", out, re.MULTILINE) - assert err == "" - mo.assert_called() - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_ch_del(capsys): - """Test --ch-del with valid secondary channel to be deleted""" - sys.argv = ["", "--ch-del", "--ch-index", "1"] - mt_config.args = sys.argv - - mocked_node = MagicMock(autospec=Node) - - iface = MagicMock(autospec=SerialInterface) - iface.getNode.return_value = mocked_node - - with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: - main() - out, err = capsys.readouterr() - assert re.search(r"Connected to radio", out, re.MULTILINE) - assert re.search(r"Deleting channel", out, re.MULTILINE) - assert err == "" - mo.assert_called() - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_ch_del_no_ch_index_specified(capsys): - """Test --ch-del without a valid ch-index""" - sys.argv = ["", "--ch-del"] - mt_config.args = sys.argv - - mocked_node = MagicMock(autospec=Node) - - iface = MagicMock(autospec=SerialInterface) - iface.getNode.return_value = mocked_node - - with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: - with pytest.raises(SystemExit) as pytest_wrapped_e: - main() - assert pytest_wrapped_e.type == SystemExit - assert pytest_wrapped_e.value.code == 1 - out, err = capsys.readouterr() - assert re.search(r"Connected to radio", out, re.MULTILINE) - assert re.search(r"Warning: Need to specify", out, re.MULTILINE) - assert err == "" - mo.assert_called() - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_ch_del_primary_channel(capsys): - """Test --ch-del on ch-index=0""" - sys.argv = ["", "--ch-del", "--ch-index", "0"] - mt_config.args = sys.argv - mt_config.channel_index = 1 - - mocked_node = MagicMock(autospec=Node) - - iface = MagicMock(autospec=SerialInterface) - iface.getNode.return_value = mocked_node - - with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: - with pytest.raises(SystemExit) as pytest_wrapped_e: - main() - assert pytest_wrapped_e.type == SystemExit - assert pytest_wrapped_e.value.code == 1 - out, err = capsys.readouterr() - assert re.search(r"Connected to radio", out, re.MULTILINE) - assert re.search(r"Warning: Cannot delete primary channel", out, re.MULTILINE) - assert err == "" - mo.assert_called() - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_ch_enable_valid_secondary_channel(capsys): - """Test --ch-enable with --ch-index""" - sys.argv = ["", "--ch-enable", "--ch-index", "1"] - mt_config.args = sys.argv - - mocked_node = MagicMock(autospec=Node) - - iface = MagicMock(autospec=SerialInterface) - iface.getNode.return_value = mocked_node - - with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: - main() - out, err = capsys.readouterr() - assert re.search(r"Connected to radio", out, re.MULTILINE) - assert re.search(r"Writing modified channels", out, re.MULTILINE) - assert err == "" - assert mt_config.channel_index == 1 - mo.assert_called() - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_ch_disable_valid_secondary_channel(capsys): - """Test --ch-disable with --ch-index""" - sys.argv = ["", "--ch-disable", "--ch-index", "1"] - mt_config.args = sys.argv - - mocked_node = MagicMock(autospec=Node) - - iface = MagicMock(autospec=SerialInterface) - iface.getNode.return_value = mocked_node - - with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: - main() - out, err = capsys.readouterr() - assert re.search(r"Connected to radio", out, re.MULTILINE) - assert re.search(r"Writing modified channels", out, re.MULTILINE) - assert err == "" - assert mt_config.channel_index == 1 - mo.assert_called() - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_ch_enable_without_a_ch_index(capsys): - """Test --ch-enable without --ch-index""" - sys.argv = ["", "--ch-enable"] - mt_config.args = sys.argv - - mocked_node = MagicMock(autospec=Node) - - iface = MagicMock(autospec=SerialInterface) - iface.getNode.return_value = mocked_node - - with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: - with pytest.raises(SystemExit) as pytest_wrapped_e: - main() - assert pytest_wrapped_e.type == SystemExit - assert pytest_wrapped_e.value.code == 1 - out, err = capsys.readouterr() - assert re.search(r"Connected to radio", out, re.MULTILINE) - assert re.search(r"Warning: Need to specify", out, re.MULTILINE) - assert err == "" - assert mt_config.channel_index is None - mo.assert_called() - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_ch_enable_primary_channel(capsys): - """Test --ch-enable with --ch-index = 0""" - sys.argv = ["", "--ch-enable", "--ch-index", "0"] - mt_config.args = sys.argv - - mocked_node = MagicMock(autospec=Node) - - iface = MagicMock(autospec=SerialInterface) - iface.getNode.return_value = mocked_node - - with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: - with pytest.raises(SystemExit) as pytest_wrapped_e: - main() - assert pytest_wrapped_e.type == SystemExit - assert pytest_wrapped_e.value.code == 1 - out, err = capsys.readouterr() - assert re.search(r"Connected to radio", out, re.MULTILINE) - assert re.search(r"Warning: Cannot enable/disable PRIMARY", out, re.MULTILINE) - assert err == "" - assert mt_config.channel_index == 0 - mo.assert_called() - - -# TODO -# @pytest.mark.unit -# @pytest.mark.usefixtures("reset_mt_config") -# def test_main_ch_range_options(capsys): -# """Test changing the various range options.""" -# range_options = ['--ch-vlongslow', '--ch-longslow', '--ch-longfast', '--ch-midslow', -# '--ch-midfast', '--ch-shortslow', '--ch-shortfast'] -# for range_option in range_options: -# sys.argv = ['', f"{range_option}" ] -# mt_config.args = sys.argv -# -# mocked_node = MagicMock(autospec=Node) -# -# iface = MagicMock(autospec=SerialInterface) -# iface.getNode.return_value = mocked_node -# -# with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo: -# main() -# out, err = capsys.readouterr() -# assert re.search(r'Connected to radio', out, re.MULTILINE) -# assert re.search(r'Writing modified channels', out, re.MULTILINE) -# assert err == '' -# mo.assert_called() - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_ch_longfast_on_non_primary_channel(capsys): - """Test --ch-longfast --ch-index 1""" - sys.argv = ["", "--ch-longfast", "--ch-index", "1"] - mt_config.args = sys.argv - - mocked_node = MagicMock(autospec=Node) - - iface = MagicMock(autospec=SerialInterface) - iface.getNode.return_value = mocked_node - - with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: - with pytest.raises(SystemExit) as pytest_wrapped_e: - main() - assert pytest_wrapped_e.type == SystemExit - assert pytest_wrapped_e.value.code == 1 - out, err = capsys.readouterr() - assert re.search(r"Connected to radio", out, re.MULTILINE) - assert re.search(r"Warning: Cannot set modem preset for non-primary channel", out, re.MULTILINE) - assert err == "" - mo.assert_called() - - -# PositionFlags: -# Misc info that might be helpful (this info will grow stale, just -# a snapshot of the values.) The radioconfig_pb2.PositionFlags.Name and bit values are: -# POS_UNDEFINED 0 -# POS_ALTITUDE 1 -# POS_ALT_MSL 2 -# POS_GEO_SEP 4 -# POS_DOP 8 -# POS_HVDOP 16 -# POS_BATTERY 32 -# POS_SATINVIEW 64 -# POS_SEQ_NOS 128 -# POS_TIMESTAMP 256 - -# TODO -# @pytest.mark.unit -# @pytest.mark.usefixtures("reset_mt_config") -# def test_main_pos_fields_no_args(capsys): -# """Test --pos-fields no args (which shows settings)""" -# sys.argv = ['', '--pos-fields'] -# mt_config.args = sys.argv -# -# pos_flags = MagicMock(autospec=meshtastic.radioconfig_pb2.PositionFlags) -# -# with patch('meshtastic.serial_interface.SerialInterface') as mo: -# mo().getNode().radioConfig.preferences.position_flags = 35 -# with patch('meshtastic.radioconfig_pb2.PositionFlags', return_value=pos_flags) as mrc: -# -# mrc.values.return_value = [0, 1, 2, 4, 8, 16, 32, 64, 128, 256] -# # Note: When you use side_effect and a list, each call will use a value from the front of the list then -# # remove that value from the list. If there are three values in the list, we expect it to be called -# # three times. -# mrc.Name.side_effect = ['POS_ALTITUDE', 'POS_ALT_MSL', 'POS_BATTERY'] -# -# main() -# -# mrc.Name.assert_called() -# mrc.values.assert_called() -# mo.assert_called() -# -# out, err = capsys.readouterr() -# assert re.search(r'Connected to radio', out, re.MULTILINE) -# assert re.search(r'POS_ALTITUDE POS_ALT_MSL POS_BATTERY', out, re.MULTILINE) -# assert err == '' - - -# TODO -# @pytest.mark.unit -# @pytest.mark.usefixtures("reset_mt_config") -# def test_main_pos_fields_arg_of_zero(capsys): -# """Test --pos-fields an arg of 0 (which shows list)""" -# sys.argv = ['', '--pos-fields', '0'] -# mt_config.args = sys.argv -# -# pos_flags = MagicMock(autospec=meshtastic.radioconfig_pb2.PositionFlags) -# -# with patch('meshtastic.serial_interface.SerialInterface') as mo: -# with patch('meshtastic.radioconfig_pb2.PositionFlags', return_value=pos_flags) as mrc: -# -# def throw_value_error_exception(exc): -# raise ValueError() -# mrc.Value.side_effect = throw_value_error_exception -# mrc.keys.return_value = [ 'POS_UNDEFINED', 'POS_ALTITUDE', 'POS_ALT_MSL', -# 'POS_GEO_SEP', 'POS_DOP', 'POS_HVDOP', 'POS_BATTERY', -# 'POS_SATINVIEW', 'POS_SEQ_NOS', 'POS_TIMESTAMP'] -# -# main() -# -# mrc.Value.assert_called() -# mrc.keys.assert_called() -# mo.assert_called() -# -# out, err = capsys.readouterr() -# assert re.search(r'Connected to radio', out, re.MULTILINE) -# assert re.search(r'ERROR: supported position fields are:', out, re.MULTILINE) -# assert re.search(r"['POS_UNDEFINED', 'POS_ALTITUDE', 'POS_ALT_MSL', 'POS_GEO_SEP',"\ -# "'POS_DOP', 'POS_HVDOP', 'POS_BATTERY', 'POS_SATINVIEW', 'POS_SEQ_NOS',"\ -# "'POS_TIMESTAMP']", out, re.MULTILINE) -# assert err == '' - - -# TODO -# @pytest.mark.unit -# @pytest.mark.usefixtures("reset_mt_config") -# def test_main_pos_fields_valid_values(capsys): -# """Test --pos-fields with valid values""" -# sys.argv = ['', '--pos-fields', 'POS_GEO_SEP', 'POS_ALT_MSL'] -# mt_config.args = sys.argv -# -# pos_flags = MagicMock(autospec=meshtastic.radioconfig_pb2.PositionFlags) -# -# with patch('meshtastic.serial_interface.SerialInterface') as mo: -# with patch('meshtastic.radioconfig_pb2.PositionFlags', return_value=pos_flags) as mrc: -# -# mrc.Value.side_effect = [ 4, 2 ] -# -# main() -# -# mrc.Value.assert_called() -# mo.assert_called() -# -# out, err = capsys.readouterr() -# assert re.search(r'Connected to radio', out, re.MULTILINE) -# assert re.search(r'Setting position fields to 6', out, re.MULTILINE) -# assert re.search(r'Set position_flags to 6', out, re.MULTILINE) -# assert re.search(r'Writing modified preferences to device', out, re.MULTILINE) -# assert err == '' - - -# TODO -# @pytest.mark.unit -# @pytest.mark.usefixtures("reset_mt_config") -# def test_main_get_with_valid_values(capsys): -# """Test --get with valid values (with string, number, boolean)""" -# sys.argv = ['', '--get', 'ls_secs', '--get', 'wifi_ssid', '--get', 'fixed_position'] -# mt_config.args = sys.argv -# -# with patch('meshtastic.serial_interface.SerialInterface') as mo: -# -# mo().getNode().radioConfig.preferences.wifi_ssid = 'foo' -# mo().getNode().radioConfig.preferences.ls_secs = 300 -# mo().getNode().radioConfig.preferences.fixed_position = False -# -# main() -# -# mo.assert_called() -# -# out, err = capsys.readouterr() -# assert re.search(r'Connected to radio', out, re.MULTILINE) -# assert re.search(r'ls_secs: 300', out, re.MULTILINE) -# assert re.search(r'wifi_ssid: foo', out, re.MULTILINE) -# assert re.search(r'fixed_position: False', out, re.MULTILINE) -# assert err == '' - - -# TODO -#@pytest.mark.unit -#@pytest.mark.usefixtures("reset_mt_config") -#def test_main_get_with_valid_values_camel(capsys, caplog): -# """Test --get with valid values (with string, number, boolean)""" -# sys.argv = ["", "--get", "lsSecs", "--get", "wifiSsid", "--get", "fixedPosition"] -# mt_config.args = sys.argv -# mt_config.camel_case = True -# -# with caplog.at_level(logging.DEBUG): -# with patch("meshtastic.serial_interface.SerialInterface") as mo: -# mo().getNode().radioConfig.preferences.wifi_ssid = "foo" -# mo().getNode().radioConfig.preferences.ls_secs = 300 -# mo().getNode().radioConfig.preferences.fixed_position = False -# -# main() -# -# mo.assert_called() -# -# out, err = capsys.readouterr() -# assert re.search(r"Connected to radio", out, re.MULTILINE) -# assert re.search(r"lsSecs: 300", out, re.MULTILINE) -# assert re.search(r"wifiSsid: foo", out, re.MULTILINE) -# assert re.search(r"fixedPosition: False", out, re.MULTILINE) -# assert err == "" - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_get_with_invalid(capsys): - """Test --get with invalid field""" - sys.argv = ["", "--get", "foo"] - mt_config.args = sys.argv - - mocked_user_prefs = MagicMock() - mocked_user_prefs.DESCRIPTOR.fields_by_name.get.return_value = None - - mocked_node = MagicMock(autospec=Node) - mocked_node.localConfig = mocked_user_prefs - mocked_node.moduleConfig = mocked_user_prefs - - iface = MagicMock(autospec=SerialInterface) - iface.getNode.return_value = mocked_node - - with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: - main() - out, err = capsys.readouterr() - assert re.search(r"Connected to radio", out, re.MULTILINE) - assert re.search(r"do not have attribute foo", out, re.MULTILINE) - assert re.search(r"Choices are...", out, re.MULTILINE) - assert err == "" - mo.assert_called() - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_onReceive_empty(caplog, capsys): - """Test onReceive""" - args = MagicMock() - mt_config.args = args - iface = MagicMock(autospec=SerialInterface) - packet = {} - with caplog.at_level(logging.DEBUG): - onReceive(packet, iface) - assert re.search(r"in onReceive", caplog.text, re.MULTILINE) - out, err = capsys.readouterr() - assert re.search( - r"Warning: Error processing received packet: 'to'.", out, re.MULTILINE - ) - assert err == "" - - -# TODO: use this captured position app message (might want/need in the future) -# packet = { -# 'to': 4294967295, -# 'decoded': { -# 'portnum': 'POSITION_APP', -# 'payload': "M69\306a" -# }, -# 'id': 334776976, -# 'hop_limit': 3 -# } - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_onReceive_with_sendtext(caplog, capsys): - """Test onReceive with sendtext - The entire point of this test is to make sure the interface.close() call - is made in onReceive(). - """ - sys.argv = ["", "--sendtext", "hello"] - mt_config.args = sys.argv - - # Note: 'TEXT_MESSAGE_APP' value is 1 - packet = { - "to": 4294967295, - "decoded": {"portnum": 1, "payload": "hello"}, - "id": 334776977, - "hop_limit": 3, - "want_ack": True, - } - - iface = MagicMock(autospec=SerialInterface) - iface.myInfo.my_node_num = 4294967295 - - with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: - with caplog.at_level(logging.DEBUG): - main() - onReceive(packet, iface) - assert re.search(r"in onReceive", caplog.text, re.MULTILINE) - mo.assert_called() - out, err = capsys.readouterr() - assert re.search(r"Sending text message hello to", out, re.MULTILINE) - assert err == "" - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_onReceive_with_text(caplog, capsys): - """Test onReceive with text""" - args = MagicMock() - args.sendtext.return_value = "foo" - mt_config.args = args - - # Note: 'TEXT_MESSAGE_APP' value is 1 - # Note: Some of this is faked below. - packet = { - "to": 4294967295, - "decoded": {"portnum": 1, "payload": "hello", "text": "faked"}, - "id": 334776977, - "hop_limit": 3, - "want_ack": True, - "rxSnr": 6.0, - "hopLimit": 3, - "raw": "faked", - "fromId": "!28b5465c", - "toId": "^all", - } - - iface = MagicMock(autospec=SerialInterface) - iface.myInfo.my_node_num = 4294967295 - - with patch("meshtastic.serial_interface.SerialInterface", return_value=iface): - with caplog.at_level(logging.DEBUG): - onReceive(packet, iface) - assert re.search(r"in onReceive", caplog.text, re.MULTILINE) - out, err = capsys.readouterr() - assert re.search(r"Sending reply", out, re.MULTILINE) - assert err == "" - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_onConnection(capsys): - """Test onConnection""" - sys.argv = [""] - mt_config.args = sys.argv - iface = MagicMock(autospec=SerialInterface) - - class TempTopic: - """temp class for topic""" - - def getName(self): - """return the fake name of a topic""" - return "foo" - - mytopic = TempTopic() - onConnection(iface, mytopic) - out, err = capsys.readouterr() - assert re.search(r"Connection changed: foo", out, re.MULTILINE) - assert err == "" - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_export_config(capsys): - """Test export_config() function directly""" - iface = MagicMock(autospec=SerialInterface) - with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: - mo.getLongName.return_value = "foo" - mo.getShortName.return_value = "oof" - 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.localNode.radioConfig.preferences = """phone_timeout_secs: 900 -ls_secs: 300 -position_broadcast_smart: true -fixed_position: true -position_flags: 35""" - export_config(mo) - out = export_config(mo) - err = "" - - # ensure we do not output this line - assert not re.search(r"Connected to radio", out, re.MULTILINE) - - assert re.search(r"owner: foo", out, re.MULTILINE) - assert re.search(r"owner_short: oof", out, re.MULTILINE) - assert re.search(r"channel_url: bar", out, re.MULTILINE) - assert re.search(r"location:", out, re.MULTILINE) - assert re.search(r"lat: 110.0", out, re.MULTILINE) - assert re.search(r"lon: 120.0", out, re.MULTILINE) - assert re.search(r"alt: 100", out, re.MULTILINE) - # TODO: rework above config to test the following - #assert re.search(r"user_prefs:", out, re.MULTILINE) - #assert re.search(r"phone_timeout_secs: 900", out, re.MULTILINE) - #assert re.search(r"ls_secs: 300", out, re.MULTILINE) - #assert re.search(r"position_broadcast_smart: 'true'", out, re.MULTILINE) - #assert re.search(r"fixed_position: 'true'", out, re.MULTILINE) - #assert re.search(r"position_flags: 35", out, re.MULTILINE) - assert err == "" - - -# TODO -# recursion depth exceeded error -#@pytest.mark.unit -#@pytest.mark.usefixtures("reset_mt_config") -#def test_main_export_config_use_camel(capsys): -# """Test export_config() function directly""" -# mt_config.camel_case = True -# iface = MagicMock(autospec=SerialInterface) -# with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: -# mo.getLongName.return_value = "foo" -# mo.localNode.getURL.return_value = "bar" -# mo.getMyNodeInfo().get.return_value = { -# "latitudeI": 1100000000, -# "longitudeI": 1200000000, -# "altitude": 100, -# "batteryLevel": 34, -# "latitude": 110.0, -# "longitude": 120.0, -# } -# mo.localNode.radioConfig.preferences = """phone_timeout_secs: 900 -#ls_secs: 300 -#position_broadcast_smart: true -#fixed_position: true -#position_flags: 35""" -# export_config(mo) -# out, err = capsys.readouterr() -# -# # ensure we do not output this line -# assert not re.search(r"Connected to radio", out, re.MULTILINE) -# -# assert re.search(r"owner: foo", out, re.MULTILINE) -# assert re.search(r"channelUrl: bar", out, re.MULTILINE) -# assert re.search(r"location:", out, re.MULTILINE) -# assert re.search(r"lat: 110.0", out, re.MULTILINE) -# assert re.search(r"lon: 120.0", out, re.MULTILINE) -# assert re.search(r"alt: 100", out, re.MULTILINE) -# assert re.search(r"userPrefs:", out, re.MULTILINE) -# assert re.search(r"phoneTimeoutSecs: 900", out, re.MULTILINE) -# assert re.search(r"lsSecs: 300", out, re.MULTILINE) -# # TODO: should True be capitalized here? -# assert re.search(r"positionBroadcastSmart: 'True'", out, re.MULTILINE) -# assert re.search(r"fixedPosition: 'True'", out, re.MULTILINE) -# assert re.search(r"positionFlags: 35", out, re.MULTILINE) -# assert err == "" - - -# TODO -# maximum recursion depth error -#@pytest.mark.unit -#@pytest.mark.usefixtures("reset_mt_config") -#def test_main_export_config_called_from_main(capsys): -# """Test --export-config""" -# sys.argv = ["", "--export-config"] -# mt_config.args = sys.argv -# -# iface = MagicMock(autospec=SerialInterface) -# with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: -# main() -# out, err = capsys.readouterr() -# assert not re.search(r"Connected to radio", out, re.MULTILINE) -# assert re.search(r"# start of Meshtastic configure yaml", out, re.MULTILINE) -# assert err == "" -# mo.assert_called() - - -@pytest.mark.unit -def test_set_missing_flags_false(): - """Test set_missing_flags_false() function""" - config = { - "bluetooth": { - "enabled": True - }, - "lora": { - "txEnabled": True - } - } - - false_defaults = { - ("bluetooth", "enabled"), - ("lora", "sx126xRxBoostedGain"), - ("lora", "txEnabled"), - ("lora", "usePreset"), - ("position", "positionBroadcastSmartEnabled"), - ("security", "serialEnabled"), - ("mqtt", "encryptionEnabled"), - } - - set_missing_flags_false(config, false_defaults) - - # Preserved - assert config["bluetooth"]["enabled"] is True - assert config["lora"]["txEnabled"] is True - - # Added - assert config["lora"]["usePreset"] is False - assert config["lora"]["sx126xRxBoostedGain"] is False - assert config["position"]["positionBroadcastSmartEnabled"] is False - assert config["security"]["serialEnabled"] is False - assert config["mqtt"]["encryptionEnabled"] is False - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_gpio_rd_no_gpio_channel(capsys): - """Test --gpio_rd with no named gpio channel""" - sys.argv = ["", "--gpio-rd", "0x10", "--dest", "!foo"] - mt_config.args = sys.argv - - iface = MagicMock(autospec=SerialInterface) - iface.localNode.getChannelByName.return_value = None - with patch("meshtastic.serial_interface.SerialInterface", return_value=iface): - with pytest.raises(SystemExit) as pytest_wrapped_e: - main() - assert pytest_wrapped_e.type == SystemExit - assert pytest_wrapped_e.value.code == 1 - out, err = capsys.readouterr() - assert re.search(r"Warning: No channel named", out) - assert err == "" - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_gpio_rd_no_dest(capsys): - """Test --gpio_rd with a named gpio channel but no dest was specified""" - sys.argv = ["", "--gpio-rd", "0x2000"] - mt_config.args = sys.argv - - channel = Channel(index=2, role=2) - channel.settings.psk = b"\x8a\x94y\x0e\xc6\xc9\x1e5\x91\x12@\xa60\xa8\xb43\x87\x00\xf2K\x0e\xe7\x7fAz\xcd\xf5\xb0\x900\xa84" - channel.settings.name = "gpio" - - iface = MagicMock(autospec=SerialInterface) - iface.localNode.getChannelByName.return_value = channel - with patch("meshtastic.serial_interface.SerialInterface", return_value=iface): - with pytest.raises(SystemExit) as pytest_wrapped_e: - main() - assert pytest_wrapped_e.type == SystemExit - assert pytest_wrapped_e.value.code == 1 - out, err = capsys.readouterr() - assert re.search(r"Warning: Must use a destination node ID", out) - assert err == "" - - -# TODO -# @pytest.mark.unit -# @pytest.mark.usefixtures("reset_mt_config") -# @patch('time.sleep') -# def test_main_gpio_rd(caplog, capsys): -# """Test --gpio_rd with a named gpio channel""" -# # Note: On the Heltec v2.1, there is a GPIO pin GPIO 13 that does not have a -# # red arrow (meaning ok to use for our purposes) -# # See https://resource.heltec.cn/download/WiFi_LoRa_32/WIFI_LoRa_32_V2.pdf -# # To find out the mask for GPIO 13, let us assign n as 13. -# # 1. Find the 2^n or 2^13 (8192) -# # 2. Convert 8192 decimal to hex (0x2000) -# # You can use python: -# # >>> print(hex(2**13)) -# # 0x2000 -# sys.argv = ['', '--gpio-rd', '0x1000', '--dest', '!1234'] -# mt_config.args = sys.argv -# -# channel = Channel(index=1, role=1) -# channel.settings.modem_config = 3 -# channel.settings.psk = b'\x01' -# -# packet = { -# -# 'from': 682968668, -# 'to': 682968612, -# 'channel': 1, -# 'decoded': { -# 'portnum': 'REMOTE_HARDWARE_APP', -# 'payload': b'\x08\x05\x18\x80 ', -# 'requestId': 1629980484, -# 'remotehw': { -# 'typ': 'READ_GPIOS_REPLY', -# 'gpioValue': '4096', -# 'raw': 'faked', -# 'id': 1693085229, -# 'rxTime': 1640294262, -# 'rxSnr': 4.75, -# 'hopLimit': 3, -# 'wantAck': True, -# } -# } -# } -# -# iface = MagicMock(autospec=SerialInterface) -# iface.localNode.getChannelByName.return_value = channel -# with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo: -# with caplog.at_level(logging.DEBUG): -# main() -# onGPIOreceive(packet, mo) -# out, err = capsys.readouterr() -# assert re.search(r'Connected to radio', out, re.MULTILINE) -# assert re.search(r'Reading GPIO mask 0x1000 ', out, re.MULTILINE) -# assert re.search(r'Received RemoteHardware typ=READ_GPIOS_REPLY, gpio_value=4096', out, re.MULTILINE) -# assert err == '' - - -# TODO -# @pytest.mark.unit -# @pytest.mark.usefixtures("reset_mt_config") -# @patch('time.sleep') -# def test_main_gpio_rd_with_no_gpioMask(caplog, capsys): -# """Test --gpio_rd with a named gpio channel""" -# sys.argv = ['', '--gpio-rd', '0x1000', '--dest', '!1234'] -# mt_config.args = sys.argv -# -# channel = Channel(index=1, role=1) -# channel.settings.modem_config = 3 -# channel.settings.psk = b'\x01' -# -# # Note: Intentionally do not have gpioValue in response as that is the -# # default value -# packet = { -# 'from': 682968668, -# 'to': 682968612, -# 'channel': 1, -# 'decoded': { -# 'portnum': 'REMOTE_HARDWARE_APP', -# 'payload': b'\x08\x05\x18\x80 ', -# 'requestId': 1629980484, -# 'remotehw': { -# 'typ': 'READ_GPIOS_REPLY', -# 'raw': 'faked', -# 'id': 1693085229, -# 'rxTime': 1640294262, -# 'rxSnr': 4.75, -# 'hopLimit': 3, -# 'wantAck': True, -# } -# } -# } -# -# iface = MagicMock(autospec=SerialInterface) -# iface.localNode.getChannelByName.return_value = channel -# with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo: -# with caplog.at_level(logging.DEBUG): -# main() -# onGPIOreceive(packet, mo) -# out, err = capsys.readouterr() -# assert re.search(r'Connected to radio', out, re.MULTILINE) -# assert re.search(r'Reading GPIO mask 0x1000 ', out, re.MULTILINE) -# assert re.search(r'Received RemoteHardware typ=READ_GPIOS_REPLY, gpio_value=0', out, re.MULTILINE) -# assert err == '' - - -# TODO -# @pytest.mark.unit -# @pytest.mark.usefixtures("reset_mt_config") -# def test_main_gpio_watch(caplog, capsys): -# """Test --gpio_watch with a named gpio channel""" -# sys.argv = ['', '--gpio-watch', '0x1000', '--dest', '!1234'] -# mt_config.args = sys.argv -# -# def my_sleep(amount): -# print(f'{amount}') -# sys.exit(3) -# -# channel = Channel(index=1, role=1) -# channel.settings.modem_config = 3 -# channel.settings.psk = b'\x01' -# -# packet = { -# -# 'from': 682968668, -# 'to': 682968612, -# 'channel': 1, -# 'decoded': { -# 'portnum': 'REMOTE_HARDWARE_APP', -# 'payload': b'\x08\x05\x18\x80 ', -# 'requestId': 1629980484, -# 'remotehw': { -# 'typ': 'READ_GPIOS_REPLY', -# 'gpioValue': '4096', -# 'raw': 'faked', -# 'id': 1693085229, -# 'rxTime': 1640294262, -# 'rxSnr': 4.75, -# 'hopLimit': 3, -# 'wantAck': True, -# } -# } -# } -# -# with patch('time.sleep', side_effect=my_sleep): -# with pytest.raises(SystemExit) as pytest_wrapped_e: -# iface = MagicMock(autospec=SerialInterface) -# iface.localNode.getChannelByName.return_value = channel -# with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo: -# with caplog.at_level(logging.DEBUG): -# main() -# onGPIOreceive(packet, mo) -# assert pytest_wrapped_e.type == SystemExit -# assert pytest_wrapped_e.value.code == 3 -# out, err = capsys.readouterr() -# assert re.search(r'Connected to radio', out, re.MULTILINE) -# assert re.search(r'Watching GPIO mask 0x1000 ', out, re.MULTILINE) -# assert err == '' - - -# TODO -# @pytest.mark.unit -# @pytest.mark.usefixtures("reset_mt_config") -# def test_main_gpio_wrb(caplog, capsys): -# """Test --gpio_wrb with a named gpio channel""" -# sys.argv = ['', '--gpio-wrb', '4', '1', '--dest', '!1234'] -# mt_config.args = sys.argv -# -# channel = Channel(index=1, role=1) -# channel.settings.modem_config = 3 -# channel.settings.psk = b'\x01' -# -# packet = { -# -# 'from': 682968668, -# 'to': 682968612, -# 'channel': 1, -# 'decoded': { -# 'portnum': 'REMOTE_HARDWARE_APP', -# 'payload': b'\x08\x05\x18\x80 ', -# 'requestId': 1629980484, -# 'remotehw': { -# 'typ': 'READ_GPIOS_REPLY', -# 'gpioValue': '16', -# 'raw': 'faked', -# 'id': 1693085229, -# 'rxTime': 1640294262, -# 'rxSnr': 4.75, -# 'hopLimit': 3, -# 'wantAck': True, -# } -# } -# } -# -# -# iface = MagicMock(autospec=SerialInterface) -# iface.localNode.getChannelByName.return_value = channel -# with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo: -# with caplog.at_level(logging.DEBUG): -# main() -# onGPIOreceive(packet, mo) -# out, err = capsys.readouterr() -# assert re.search(r'Connected to radio', out, re.MULTILINE) -# assert re.search(r'Writing GPIO mask 0x10 with value 0x10 to !1234', out, re.MULTILINE) -# assert re.search(r'Received RemoteHardware typ=READ_GPIOS_REPLY, gpio_value=16 value=0', out, re.MULTILINE) -# assert err == '' - - -# TODO -# need to restructure these for nested configs -#@pytest.mark.unit -#@pytest.mark.usefixtures("reset_mt_config") -#def test_main_getPref_valid_field(capsys): -# """Test getPref() with a valid field""" -# prefs = MagicMock() -# prefs.DESCRIPTOR.fields_by_name.get.return_value = "ls_secs" -# prefs.wifi_ssid = "foo" -# prefs.ls_secs = 300 -# prefs.fixed_position = False -# -# getPref(prefs, "ls_secs") -# out, err = capsys.readouterr() -# assert re.search(r"ls_secs: 300", out, re.MULTILINE) -# assert err == "" -# -# -#@pytest.mark.unit -#@pytest.mark.usefixtures("reset_mt_config") -#def test_main_getPref_valid_field_camel(capsys): -# """Test getPref() with a valid field""" -# mt_config.camel_case = True -# prefs = MagicMock() -# prefs.DESCRIPTOR.fields_by_name.get.return_value = "ls_secs" -# prefs.wifi_ssid = "foo" -# prefs.ls_secs = 300 -# prefs.fixed_position = False -# -# getPref(prefs, "ls_secs") -# out, err = capsys.readouterr() -# assert re.search(r"lsSecs: 300", out, re.MULTILINE) -# assert err == "" -# -# -#@pytest.mark.unit -#@pytest.mark.usefixtures("reset_mt_config") -#def test_main_getPref_valid_field_string(capsys): -# """Test getPref() with a valid field and value as a string""" -# prefs = MagicMock() -# prefs.DESCRIPTOR.fields_by_name.get.return_value = "wifi_ssid" -# prefs.wifi_ssid = "foo" -# prefs.ls_secs = 300 -# prefs.fixed_position = False -# -# getPref(prefs, "wifi_ssid") -# out, err = capsys.readouterr() -# assert re.search(r"wifi_ssid: foo", out, re.MULTILINE) -# assert err == "" -# -# -#@pytest.mark.unit -#@pytest.mark.usefixtures("reset_mt_config") -#def test_main_getPref_valid_field_string_camel(capsys): -# """Test getPref() with a valid field and value as a string""" -# mt_config.camel_case = True -# prefs = MagicMock() -# prefs.DESCRIPTOR.fields_by_name.get.return_value = "wifi_ssid" -# prefs.wifi_ssid = "foo" -# prefs.ls_secs = 300 -# prefs.fixed_position = False -# -# getPref(prefs, "wifi_ssid") -# out, err = capsys.readouterr() -# assert re.search(r"wifiSsid: foo", out, re.MULTILINE) -# assert err == "" -# -# -#@pytest.mark.unit -#@pytest.mark.usefixtures("reset_mt_config") -#def test_main_getPref_valid_field_bool(capsys): -# """Test getPref() with a valid field and value as a bool""" -# prefs = MagicMock() -# prefs.DESCRIPTOR.fields_by_name.get.return_value = "fixed_position" -# prefs.wifi_ssid = "foo" -# prefs.ls_secs = 300 -# prefs.fixed_position = False -# -# getPref(prefs, "fixed_position") -# out, err = capsys.readouterr() -# assert re.search(r"fixed_position: False", out, re.MULTILINE) -# assert err == "" -# -# -#@pytest.mark.unit -#@pytest.mark.usefixtures("reset_mt_config") -#def test_main_getPref_valid_field_bool_camel(capsys): -# """Test getPref() with a valid field and value as a bool""" -# mt_config.camel_case = True -# prefs = MagicMock() -# prefs.DESCRIPTOR.fields_by_name.get.return_value = "fixed_position" -# prefs.wifi_ssid = "foo" -# prefs.ls_secs = 300 -# prefs.fixed_position = False -# -# getPref(prefs, "fixed_position") -# out, err = capsys.readouterr() -# assert re.search(r"fixedPosition: False", out, re.MULTILINE) -# assert err == "" -# -# -#@pytest.mark.unit -#@pytest.mark.usefixtures("reset_mt_config") -#def test_main_getPref_invalid_field(capsys): -# """Test getPref() with an invalid field""" -# -# class Field: -# """Simple class for testing.""" -# -# def __init__(self, name): -# """constructor""" -# self.name = name -# -# prefs = MagicMock() -# prefs.DESCRIPTOR.fields_by_name.get.return_value = None -# -# # Note: This is a subset of the real fields -# ls_secs_field = Field("ls_secs") -# is_router = Field("is_router") -# fixed_position = Field("fixed_position") -# -# fields = [ls_secs_field, is_router, fixed_position] -# prefs.DESCRIPTOR.fields = fields -# -# getPref(prefs, "foo") -# -# out, err = capsys.readouterr() -# assert re.search(r"does not have an attribute called foo", out, re.MULTILINE) -# # ensure they are sorted -# assert re.search(r"fixed_position\s+is_router\s+ls_secs", out, re.MULTILINE) -# assert err == "" -# -# -#@pytest.mark.unit -#@pytest.mark.usefixtures("reset_mt_config") -#def test_main_getPref_invalid_field_camel(capsys): -# """Test getPref() with an invalid field""" -# mt_config.camel_case = True -# -# class Field: -# """Simple class for testing.""" -# -# def __init__(self, name): -# """constructor""" -# self.name = name -# -# prefs = MagicMock() -# prefs.DESCRIPTOR.fields_by_name.get.return_value = None -# -# # Note: This is a subset of the real fields -# ls_secs_field = Field("ls_secs") -# is_router = Field("is_router") -# fixed_position = Field("fixed_position") -# -# fields = [ls_secs_field, is_router, fixed_position] -# prefs.DESCRIPTOR.fields = fields -# -# getPref(prefs, "foo") -# -# out, err = capsys.readouterr() -# assert re.search(r"does not have an attribute called foo", out, re.MULTILINE) -# # ensure they are sorted -# assert re.search(r"fixedPosition\s+isRouter\s+lsSecs", out, re.MULTILINE) -# assert err == "" -# -# -#@pytest.mark.unit -#@pytest.mark.usefixtures("reset_mt_config") -#def test_main_setPref_valid_field_int_as_string(capsys): -# """Test setPref() with a valid field""" -# -# class Field: -# """Simple class for testing.""" -# -# def __init__(self, name, enum_type): -# """constructor""" -# self.name = name -# self.enum_type = enum_type -# -# ls_secs_field = Field("ls_secs", "int") -# prefs = MagicMock() -# prefs.DESCRIPTOR.fields_by_name.get.return_value = ls_secs_field -# -# setPref(prefs, "ls_secs", "300") -# out, err = capsys.readouterr() -# assert re.search(r"Set ls_secs to 300", out, re.MULTILINE) -# assert err == "" - - -# TODO -# @pytest.mark.unit -# @pytest.mark.usefixtures("reset_mt_config") -# def test_main_setPref_valid_field_invalid_enum(capsys, caplog): -# """Test setPref() with a valid field but invalid enum value""" -# -# radioConfig = RadioConfig() -# prefs = radioConfig.preferences -# -# with caplog.at_level(logging.DEBUG): -# setPref(prefs, 'charge_current', 'foo') -# out, err = capsys.readouterr() -# assert re.search(r'charge_current does not have an enum called foo', out, re.MULTILINE) -# assert re.search(r'Choices in sorted order are', out, re.MULTILINE) -# assert re.search(r'MA100', out, re.MULTILINE) -# assert re.search(r'MA280', out, re.MULTILINE) -# assert err == '' - - -# TODO -# @pytest.mark.unit -# @pytest.mark.usefixtures("reset_mt_config") -# def test_main_setPref_valid_field_invalid_enum_where_enums_are_camel_cased_values(capsys, caplog): -# """Test setPref() with a valid field but invalid enum value""" -# -# radioConfig = RadioConfig() -# prefs = radioConfig.preferences -# -# with caplog.at_level(logging.DEBUG): -# setPref(prefs, 'region', 'foo') -# out, err = capsys.readouterr() -# assert re.search(r'region does not have an enum called foo', out, re.MULTILINE) -# assert re.search(r'Choices in sorted order are', out, re.MULTILINE) -# assert re.search(r'ANZ', out, re.MULTILINE) -# assert re.search(r'CN', out, re.MULTILINE) -# assert err == '' - - -# TODO -# @pytest.mark.unit -# @pytest.mark.usefixtures("reset_mt_config") -# def test_main_setPref_valid_field_invalid_enum_camel(capsys, caplog): -# """Test setPref() with a valid field but invalid enum value""" -# mt_config.camel_case = True -# -# radioConfig = RadioConfig() -# prefs = radioConfig.preferences -# -# with caplog.at_level(logging.DEBUG): -# setPref(prefs, 'charge_current', 'foo') -# out, err = capsys.readouterr() -# assert re.search(r'chargeCurrent does not have an enum called foo', out, re.MULTILINE) -# assert err == '' - - -# TODO -# @pytest.mark.unit -# @pytest.mark.usefixtures("reset_mt_config") -# def test_main_setPref_valid_field_valid_enum(capsys, caplog): -# """Test setPref() with a valid field and valid enum value""" -# -# # charge_current -# # some valid values: MA100 MA1000 MA1080 -# -# radioConfig = RadioConfig() -# prefs = radioConfig.preferences -# -# with caplog.at_level(logging.DEBUG): -# setPref(prefs, 'charge_current', 'MA100') -# out, err = capsys.readouterr() -# assert re.search(r'Set charge_current to MA100', out, re.MULTILINE) -# assert err == '' - - -# TODO -# @pytest.mark.unit -# @pytest.mark.usefixtures("reset_mt_config") -# def test_main_setPref_valid_field_valid_enum_camel(capsys, caplog): -# """Test setPref() with a valid field and valid enum value""" -# mt_config.camel_case = True -# -# # charge_current -# # some valid values: MA100 MA1000 MA1080 -# -# radioConfig = RadioConfig() -# prefs = radioConfig.preferences -# -# with caplog.at_level(logging.DEBUG): -# setPref(prefs, 'charge_current', 'MA100') -# out, err = capsys.readouterr() -# assert re.search(r'Set chargeCurrent to MA100', out, re.MULTILINE) -# assert err == '' - -# TODO -# need to update for nested configs -#@pytest.mark.unit -#@pytest.mark.usefixtures("reset_mt_config") -#def test_main_setPref_invalid_field(capsys): -# """Test setPref() with a invalid field""" -# -# class Field: -# """Simple class for testing.""" -# -# def __init__(self, name): -# """constructor""" -# self.name = name -# -# prefs = MagicMock() -# prefs.DESCRIPTOR.fields_by_name.get.return_value = None -# -# # Note: This is a subset of the real fields -# ls_secs_field = Field("ls_secs") -# is_router = Field("is_router") -# fixed_position = Field("fixed_position") -# -# fields = [ls_secs_field, is_router, fixed_position] -# prefs.DESCRIPTOR.fields = fields -# -# setPref(prefs, "foo", "300") -# out, err = capsys.readouterr() -# assert re.search(r"does not have an attribute called foo", out, re.MULTILINE) -# # ensure they are sorted -# assert re.search(r"fixed_position\s+is_router\s+ls_secs", out, re.MULTILINE) -# assert err == "" -# -# -#@pytest.mark.unit -#@pytest.mark.usefixtures("reset_mt_config") -#def test_main_setPref_invalid_field_camel(capsys): -# """Test setPref() with a invalid field""" -# mt_config.camel_case = True -# -# class Field: -# """Simple class for testing.""" -# -# def __init__(self, name): -# """constructor""" -# self.name = name -# -# prefs = MagicMock() -# prefs.DESCRIPTOR.fields_by_name.get.return_value = None -# -# # Note: This is a subset of the real fields -# ls_secs_field = Field("ls_secs") -# is_router = Field("is_router") -# fixed_position = Field("fixed_position") -# -# fields = [ls_secs_field, is_router, fixed_position] -# prefs.DESCRIPTOR.fields = fields -# -# setPref(prefs, "foo", "300") -# out, err = capsys.readouterr() -# assert re.search(r"does not have an attribute called foo", out, re.MULTILINE) -# # ensure they are sorted -# assert re.search(r"fixedPosition\s+isRouter\s+lsSecs", out, re.MULTILINE) -# assert err == "" -# -# -#@pytest.mark.unit -#@pytest.mark.usefixtures("reset_mt_config") -#def test_main_setPref_ignore_incoming_123(capsys): -# """Test setPref() with ignore_incoming""" -# -# class Field: -# """Simple class for testing.""" -# -# def __init__(self, name, enum_type): -# """constructor""" -# self.name = name -# self.enum_type = enum_type -# -# ignore_incoming_field = Field("ignore_incoming", "list") -# prefs = MagicMock() -# prefs.DESCRIPTOR.fields_by_name.get.return_value = ignore_incoming_field -# -# setPref(prefs, "ignore_incoming", "123") -# out, err = capsys.readouterr() -# assert re.search(r"Adding '123' to the ignore_incoming list", out, re.MULTILINE) -# assert re.search(r"Set ignore_incoming to 123", out, re.MULTILINE) -# assert err == "" -# -# -#@pytest.mark.unit -#@pytest.mark.usefixtures("reset_mt_config") -#def test_main_setPref_ignore_incoming_0(capsys): -# """Test setPref() with ignore_incoming""" -# -# class Field: -# """Simple class for testing.""" -# -# def __init__(self, name, enum_type): -# """constructor""" -# self.name = name -# self.enum_type = enum_type -# -# ignore_incoming_field = Field("ignore_incoming", "list") -# prefs = MagicMock() -# prefs.DESCRIPTOR.fields_by_name.get.return_value = ignore_incoming_field -# -# setPref(prefs, "ignore_incoming", "0") -# out, err = capsys.readouterr() -# assert re.search(r"Clearing ignore_incoming list", out, re.MULTILINE) -# assert re.search(r"Set ignore_incoming to 0", out, re.MULTILINE) -# assert err == "" - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_ch_set_psk_no_ch_index(capsys): - """Test --ch-set psk""" - sys.argv = ["", "--ch-set", "psk", "foo", "--host", "meshtastic.local"] - mt_config.args = sys.argv - - iface = MagicMock(autospec=TCPInterface) - with patch("meshtastic.tcp_interface.TCPInterface", return_value=iface) as mo: - with pytest.raises(SystemExit) as pytest_wrapped_e: - main() - out, err = capsys.readouterr() - assert re.search(r"Connected to radio", out, re.MULTILINE) - assert re.search(r"Warning: Need to specify '--ch-index'", out, re.MULTILINE) - assert err == "" - assert pytest_wrapped_e.type == SystemExit - assert pytest_wrapped_e.value.code == 1 - mo.assert_called() - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_ch_set_psk_with_ch_index(capsys): - """Test --ch-set psk""" - sys.argv = [ - "", - "--ch-set", - "psk", - "foo", - "--host", - "meshtastic.local", - "--ch-index", - "0", - ] - mt_config.args = sys.argv - - iface = MagicMock(autospec=TCPInterface) - with patch("meshtastic.tcp_interface.TCPInterface", return_value=iface) as mo: - main() - out, err = capsys.readouterr() - assert re.search(r"Connected to radio", out, re.MULTILINE) - assert re.search(r"Writing modified channels to device", out, re.MULTILINE) - assert err == "" - mo.assert_called() - - -# TODO -# doesn't work properly with nested/module config stuff -#@pytest.mark.unit -#@pytest.mark.usefixtures("reset_mt_config") -#def test_main_ch_set_name_with_ch_index(capsys): -# """Test --ch-set setting other than psk""" -# sys.argv = [ -# "", -# "--ch-set", -# "name", -# "foo", -# "--host", -# "meshtastic.local", -# "--ch-index", -# "0", -# ] -# mt_config.args = sys.argv -# -# iface = MagicMock(autospec=TCPInterface) -# with patch("meshtastic.tcp_interface.TCPInterface", return_value=iface) as mo: -# main() -# out, err = capsys.readouterr() -# assert re.search(r"Connected to radio", out, re.MULTILINE) -# assert re.search(r"Set name to foo", out, re.MULTILINE) -# assert re.search(r"Writing modified channels to device", out, re.MULTILINE) -# assert err == "" -# mo.assert_called() - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_onNode(capsys): - """Test onNode""" - onNode("foo") - out, err = capsys.readouterr() - assert re.search(r"Node changed", out, re.MULTILINE) - assert err == "" - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_tunnel_no_args(capsys): - """Test tunnel no arguments""" - sys.argv = [""] - mt_config.args = sys.argv - with pytest.raises(SystemExit) as pytest_wrapped_e: - tunnelMain() - assert pytest_wrapped_e.type == SystemExit - assert pytest_wrapped_e.value.code == 1 - _, err = capsys.readouterr() - assert re.search(r"usage: ", err, re.MULTILINE) - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -@patch("meshtastic.util.findPorts", return_value=[]) -@patch("platform.system") -def test_tunnel_tunnel_arg_with_no_devices(mock_platform_system, caplog, capsys): - """Test tunnel with tunnel arg (act like we are on a linux system)""" - a_mock = MagicMock() - a_mock.return_value = "Linux" - mock_platform_system.side_effect = a_mock - sys.argv = ["", "--tunnel"] - mt_config.args = sys.argv - print(f"platform.system():{platform.system()}") - with caplog.at_level(logging.DEBUG): - with pytest.raises(SystemExit) as pytest_wrapped_e: - tunnelMain() - mock_platform_system.assert_called() - assert pytest_wrapped_e.type == SystemExit - assert pytest_wrapped_e.value.code == 1 - out, err = capsys.readouterr() - assert re.search(r"No.*Meshtastic.*device.*detected", out, re.MULTILINE) - assert err == "" - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -@patch("meshtastic.util.findPorts", return_value=[]) -@patch("platform.system") -def test_tunnel_subnet_arg_with_no_devices(mock_platform_system, caplog, capsys): - """Test tunnel with subnet arg (act like we are on a linux system)""" - a_mock = MagicMock() - a_mock.return_value = "Linux" - mock_platform_system.side_effect = a_mock - sys.argv = ["", "--subnet", "foo"] - mt_config.args = sys.argv - print(f"platform.system():{platform.system()}") - with caplog.at_level(logging.DEBUG): - with pytest.raises(SystemExit) as pytest_wrapped_e: - tunnelMain() - mock_platform_system.assert_called() - assert pytest_wrapped_e.type == SystemExit - assert pytest_wrapped_e.value.code == 1 - out, err = capsys.readouterr() - assert re.search(r"No.*Meshtastic.*device.*detected", out, re.MULTILINE) - assert err == "" - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -@patch("platform.system") -@patch("termios.tcsetattr") -@patch("termios.tcgetattr") -@patch("builtins.open", new_callable=mock_open, read_data="data") -@patch("serial.Serial") -@patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"]) -def test_tunnel_tunnel_arg( - mocked_findPorts, mocked_serial, mocked_open, mock_get, mock_set, mock_platform_system, caplog, iface_with_nodes, capsys -): - """Test tunnel with tunnel arg (act like we are on a linux system)""" - - # Override the time.sleep so there is no loop - def my_sleep(amount): - print(f"{amount}") - sys.exit(3) - - a_mock = MagicMock() - a_mock.return_value = "Linux" - mock_platform_system.side_effect = a_mock - sys.argv = ["", "--tunnel"] - mt_config.args = sys.argv - - serialInterface = SerialInterface(noProto=True) - - with caplog.at_level(logging.DEBUG): - with patch("meshtastic.serial_interface.SerialInterface", return_value=serialInterface): - with patch("time.sleep", side_effect=my_sleep): - with pytest.raises(SystemExit) as pytest_wrapped_e: - tunnelMain() - mock_platform_system.assert_called() - assert pytest_wrapped_e.type == SystemExit - assert pytest_wrapped_e.value.code == 3 - assert re.search(r"Not starting Tunnel", caplog.text, re.MULTILINE) - out, err = capsys.readouterr() - assert re.search(r"Connected to radio", out, re.MULTILINE) - assert err == "" - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_set_favorite_node(): - """Test --set-favorite-node node""" - sys.argv = ["", "--set-favorite-node", "!12345678"] - mt_config.args = sys.argv - mocked_node = MagicMock(autospec=Node) - iface = MagicMock(autospec=SerialInterface) - iface.getNode.return_value = mocked_node - with patch("meshtastic.serial_interface.SerialInterface", return_value=iface): - main() - - mocked_node.setFavorite.assert_called_once_with("!12345678") - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_remove_favorite_node(): - """Test --remove-favorite-node node""" - sys.argv = ["", "--remove-favorite-node", "!12345678"] - mt_config.args = sys.argv - mocked_node = MagicMock(autospec=Node) - iface = MagicMock(autospec=SerialInterface) - iface.getNode.return_value = mocked_node - mocked_node.iface = iface - with patch("meshtastic.serial_interface.SerialInterface", return_value=iface): - main() - - mocked_node.removeFavorite.assert_called_once_with("!12345678") - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_set_ignored_node(): - """Test --set-ignored-node node""" - sys.argv = ["", "--set-ignored-node", "!12345678"] - mt_config.args = sys.argv - mocked_node = MagicMock(autospec=Node) - iface = MagicMock(autospec=SerialInterface) - iface.getNode.return_value = mocked_node - with patch("meshtastic.serial_interface.SerialInterface", return_value=iface): - main() - - mocked_node.setIgnored.assert_called_once_with("!12345678") - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_remove_ignored_node(): - """Test --remove-ignored-node node""" - sys.argv = ["", "--remove-ignored-node", "!12345678"] - mt_config.args = sys.argv - mocked_node = MagicMock(autospec=Node) - iface = MagicMock(autospec=SerialInterface) - iface.getNode.return_value = mocked_node - mocked_node.iface = iface - with patch("meshtastic.serial_interface.SerialInterface", return_value=iface): - main() - - mocked_node.removeIgnored.assert_called_once_with("!12345678") -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_set_owner_whitespace_only(capsys): - """Test --set-owner with whitespace-only name""" - sys.argv = ["", "--set-owner", " "] - mt_config.args = sys.argv - - with pytest.raises(SystemExit) as excinfo: - main() - - out, _ = capsys.readouterr() - assert "ERROR: Long Name cannot be empty or contain only whitespace characters" in out - assert excinfo.value.code == 1 - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_set_owner_empty_string(capsys): - """Test --set-owner with empty string""" - sys.argv = ["", "--set-owner", ""] - mt_config.args = sys.argv - - with pytest.raises(SystemExit) as excinfo: - main() - - out, _ = capsys.readouterr() - assert "ERROR: Long Name cannot be empty or contain only whitespace characters" in out - assert excinfo.value.code == 1 - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_set_owner_short_whitespace_only(capsys): - """Test --set-owner-short with whitespace-only name""" - sys.argv = ["", "--set-owner-short", " "] - mt_config.args = sys.argv - - with pytest.raises(SystemExit) as excinfo: - main() - - out, _ = capsys.readouterr() - assert "ERROR: Short Name cannot be empty or contain only whitespace characters" in out - assert excinfo.value.code == 1 - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_set_owner_short_empty_string(capsys): - """Test --set-owner-short with empty string""" - sys.argv = ["", "--set-owner-short", ""] - mt_config.args = sys.argv - - with pytest.raises(SystemExit) as excinfo: - main() - - out, _ = capsys.readouterr() - assert "ERROR: Short Name cannot be empty or contain only whitespace characters" in out - assert excinfo.value.code == 1 - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_set_ham_whitespace_only(capsys): - """Test --set-ham with whitespace-only name""" - sys.argv = ["", "--set-ham", " "] - mt_config.args = sys.argv - - with pytest.raises(SystemExit) as excinfo: - main() - - out, _ = capsys.readouterr() - assert "ERROR: Ham radio callsign cannot be empty or contain only whitespace characters" in out - assert excinfo.value.code == 1 - - -@pytest.mark.unit -@pytest.mark.usefixtures("reset_mt_config") -def test_main_set_ham_empty_string(capsys): - """Test --set-ham with empty string""" - sys.argv = ["", "--set-ham", ""] - mt_config.args = sys.argv - - with pytest.raises(SystemExit) as excinfo: - main() - - out, _ = capsys.readouterr() - assert "ERROR: Ham radio callsign cannot be empty or contain only whitespace characters" in out - assert excinfo.value.code == 1