From a29ee840f2bc4c902575d289848c6c009abab140 Mon Sep 17 00:00:00 2001 From: William Stearns Date: Sat, 15 Jun 2024 23:22:43 -0400 Subject: [PATCH 1/7] Adding mypy typing --- meshtastic/__init__.py | 4 +- meshtastic/__main__.py | 37 ++++++------ meshtastic/ble_interface.py | 48 ++++++++------- meshtastic/remote_hardware.py | 4 +- meshtastic/serial_interface.py | 10 ++-- meshtastic/stream_interface.py | 35 ++++++----- meshtastic/tcp_interface.py | 47 ++++++++------- meshtastic/test.py | 47 ++++++++------- meshtastic/tunnel.py | 2 +- meshtastic/util.py | 103 +++++++++++++++++---------------- 10 files changed, 180 insertions(+), 157 deletions(-) diff --git a/meshtastic/__init__.py b/meshtastic/__init__.py index 26d8e94..44a5958 100644 --- a/meshtastic/__init__.py +++ b/meshtastic/__init__.py @@ -104,13 +104,13 @@ from meshtastic.util import DeferredExecution, Timeout, catchAndIgnore, fixme, s LOCAL_ADDR = "^local" """A special ID that means the local node""" -BROADCAST_NUM = 0xFFFFFFFF +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 = 20300 +OUR_APP_VERSION: int = 20300 """The numeric buildnumber (shared with android apps) specifying the level of device code we are guaranteed to understand diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index fa71adc..198752c 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -8,6 +8,7 @@ import os import platform import sys import time +from typing import List import pyqrcode # type: ignore[import-untyped] import yaml @@ -22,7 +23,7 @@ from meshtastic.version import get_active_version from meshtastic.ble_interface import BLEInterface from meshtastic.mesh_interface import MeshInterface -def onReceive(packet, interface): +def onReceive(packet, interface) -> None: """Callback invoked when a packet arrives""" args = mt_config.args try: @@ -53,7 +54,7 @@ def onReceive(packet, interface): print(f"Warning: There is no field {ex} in the packet.") -def onConnection(interface, topic=pub.AUTO_TOPIC): # pylint: disable=W0613 +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()}") @@ -63,7 +64,7 @@ def checkChannel(interface: MeshInterface, channelIndex: int) -> bool: logging.debug(f"ch:{ch}") return (ch and ch.role != channel_pb2.Channel.Role.DISABLED) -def getPref(node, comp_name): +def getPref(node, comp_name) -> bool: """Get a channel or preferences value""" name = splitCompoundName(comp_name) @@ -78,11 +79,11 @@ def getPref(node, comp_name): # First validate the input localConfig = node.localConfig moduleConfig = node.moduleConfig - found = False + found: bool = False for config in [localConfig, moduleConfig]: objDesc = config.DESCRIPTOR config_type = objDesc.fields_by_name.get(name[0]) - pref = False + pref = False #FIXME - checkme - Used here as boolean, but set 2 lines below as a string. if config_type: pref = config_type.message_type.fields_by_name.get(snake_name) if pref or wholeField: @@ -129,15 +130,15 @@ def getPref(node, comp_name): return True -def splitCompoundName(comp_name): +def splitCompoundName(comp_name: str) -> List[str]: """Split compound (dot separated) preference name into parts""" - name = comp_name.split(".") + 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): +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: @@ -884,7 +885,7 @@ def onConnected(interface): sys.exit(1) -def printConfig(config): +def printConfig(config) -> None: """print configuration""" objDesc = config.DESCRIPTOR for config_section in objDesc.fields: @@ -901,12 +902,12 @@ def printConfig(config): print(f" {temp_name}") -def onNode(node): +def onNode(node) -> None: """Callback invoked when the node DB changes""" print(f"Node changed: {node}") -def subscribe(): +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") @@ -917,7 +918,7 @@ def subscribe(): # pub.subscribe(onNode, "meshtastic.node") -def export_config(interface): +def export_config(interface) -> str: """used in --export-config""" configObj = {} @@ -949,7 +950,7 @@ def export_config(interface): if alt: configObj["location"]["alt"] = alt - config = MessageToDict(interface.localNode.localConfig) + config = MessageToDict(interface.localNode.localConfig) #checkme - Used as a dictionary here and a string below if config: # Convert inner keys to correct snake/camelCase prefs = {} @@ -959,7 +960,7 @@ def export_config(interface): else: prefs[pref] = config[pref] if mt_config.camel_case: - configObj["config"] = config + configObj["config"] = config #Identical command here and 2 lines below? else: configObj["config"] = config @@ -975,10 +976,10 @@ def export_config(interface): else: configObj["module_config"] = prefs - config = "# start of Meshtastic configure yaml\n" - config += yaml.dump(configObj) - print(config) - return config + 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) + print(config_txt) + return config_txt def common(): diff --git a/meshtastic/ble_interface.py b/meshtastic/ble_interface.py index 1c12758..2bdc83d 100644 --- a/meshtastic/ble_interface.py +++ b/meshtastic/ble_interface.py @@ -1,11 +1,12 @@ """Bluetooth interface """ +import io import logging import time import struct import asyncio from threading import Thread, Event -from typing import Optional +from typing import List, Optional, Tuple from bleak import BleakScanner, BleakClient @@ -32,7 +33,7 @@ class BLEInterface(MeshInterface): MESH = False - def __init__(self, address: Optional[str], noProto: bool = False, debugOut = None, noNodes: bool = False): + def __init__(self, address: Optional[str], noProto: bool = False, debugOut: Optional[io.TextIOWrapper] = None, noNodes: bool = False) -> None: self.state = BLEInterface.BLEState() if not address: @@ -72,14 +73,14 @@ class BLEInterface(MeshInterface): self.client.start_notify(FROMNUM_UUID, self.from_num_handler) - async def from_num_handler(self, _, b): # pylint: disable=C0116 + async def from_num_handler(self, _, b: bytes) -> None: # pylint: disable=C0116 from_num = struct.unpack(' List[Tuple]: + """Scan for available BLE devices""" with BLEClient() as client: return [ (x[0], x[1]) for x in (client.discover( @@ -89,8 +90,8 @@ class BLEInterface(MeshInterface): ] - def find_device(self, address): - "Find a device by address" + def find_device(self, address: Optional[str]): + """Find a device by address""" meshtastic_devices = self.scan() addressed_devices = list(filter(lambda x: address in (x[1].local_name, x[0].name), meshtastic_devices)) @@ -106,18 +107,21 @@ class BLEInterface(MeshInterface): raise BLEInterface.BLEError(f"More than one Meshtastic BLE peripheral with identifier or address '{address}' found.") return addressed_devices[0][0] - def _sanitize_address(address): # pylint: disable=E0213 - "Standardize BLE address by removing extraneous characters and lowercasing" - return address \ - .replace("-", "") \ - .replace("_", "") \ - .replace(":", "") \ - .lower() + def _sanitize_address(address: Optional[str]) -> Optional[str]: # pylint: disable=E0213 + """Standardize BLE address by removing extraneous characters and lowercasing""" + if address is None: + return None + else: + return address \ + .replace("-", "") \ + .replace("_", "") \ + .replace(":", "") \ + .lower() - def connect(self, address): + def connect(self, address) -> BLEClient: "Connect to a device by address" device = self.find_device(address) - client = BLEClient(device.address) + client: BLEClient = BLEClient(device.address) client.connect() try: client.pair() @@ -128,12 +132,12 @@ class BLEInterface(MeshInterface): return client - def _receiveFromRadioImpl(self): + def _receiveFromRadioImpl(self) -> None: self._receiveThread_started.set() while self._receiveThread_started.is_set(): if self.should_read: self.should_read = False - retries = 0 + retries: int = 0 while True: b = bytes(self.client.read_gatt_char(FROMRADIO_UUID)) if not b: @@ -148,8 +152,8 @@ class BLEInterface(MeshInterface): time.sleep(0.1) self._receiveThread_stopped.set() - def _sendToRadioImpl(self, toRadio): - b = toRadio.SerializeToString() + def _sendToRadioImpl(self, toRadio) -> None: + b: bytes = toRadio.SerializeToString() if b: logging.debug(f"TORADIO write: {b.hex()}") self.client.write_gatt_char(TORADIO_UUID, b, response = True) @@ -158,7 +162,7 @@ class BLEInterface(MeshInterface): self.should_read = True - def close(self): + def close(self) -> None: if self.state.MESH: MeshInterface.close(self) @@ -173,7 +177,7 @@ class BLEInterface(MeshInterface): class BLEClient(): """Client for managing connection to a BLE device""" - def __init__(self, address = None, **kwargs): + def __init__(self, address = None, **kwargs) -> None: self._eventThread = Thread(target = self._run_event_loop) self._eventThread_started = Event() self._eventThread_stopped = Event() diff --git a/meshtastic/remote_hardware.py b/meshtastic/remote_hardware.py index 55c8c18..ea22150 100644 --- a/meshtastic/remote_hardware.py +++ b/meshtastic/remote_hardware.py @@ -8,7 +8,7 @@ from meshtastic import portnums_pb2, remote_hardware_pb2 from meshtastic.util import our_exit -def onGPIOreceive(packet, interface): +def onGPIOreceive(packet, interface) -> None: """Callback for received GPIO responses""" logging.debug(f"packet:{packet} interface:{interface}") gpioValue = 0 @@ -37,7 +37,7 @@ class RemoteHardwareClient: code for how you can connect to your own custom meshtastic services """ - def __init__(self, iface): + def __init__(self, iface) -> None: """ Constructor diff --git a/meshtastic/serial_interface.py b/meshtastic/serial_interface.py index 9a8307d..c25d359 100644 --- a/meshtastic/serial_interface.py +++ b/meshtastic/serial_interface.py @@ -4,7 +4,7 @@ import logging import platform import time -from typing import Optional +from typing import List, Optional import serial # type: ignore[import-untyped] @@ -18,7 +18,7 @@ if platform.system() != "Windows": class SerialInterface(StreamInterface): """Interface class for meshtastic devices over a serial link""" - def __init__(self, devPath: Optional[str]=None, debugOut=None, noProto=False, connectNow=True, noNodes: bool=False): + def __init__(self, devPath: Optional[str]=None, debugOut=None, noProto: bool=False, connectNow: bool=True, noNodes: bool=False) -> None: """Constructor, opens a connection to a specified serial port, or if unspecified try to find one Meshtastic device by probing @@ -31,13 +31,13 @@ class SerialInterface(StreamInterface): self.devPath: Optional[str] = devPath if self.devPath is None: - ports = meshtastic.util.findPorts(True) + ports: List[str] = meshtastic.util.findPorts(True) logging.debug(f"ports:{ports}") if len(ports) == 0: print("No Serial Meshtastic device detected, attempting TCP connection on localhost.") return elif len(ports) > 1: - message = "Warning: Multiple serial ports were detected so one serial port must be specified with the '--port'.\n" + message: str = "Warning: Multiple serial ports were detected so one serial port must be specified with the '--port'.\n" message += f" Ports detected:{ports}" meshtastic.util.our_exit(message) else: @@ -65,7 +65,7 @@ class SerialInterface(StreamInterface): self, debugOut=debugOut, noProto=noProto, connectNow=connectNow, noNodes=noNodes ) - def close(self): + def close(self) -> None: """Close a connection to the device""" self.stream.flush() time.sleep(0.1) diff --git a/meshtastic/stream_interface.py b/meshtastic/stream_interface.py index dea6923..66eb5d3 100644 --- a/meshtastic/stream_interface.py +++ b/meshtastic/stream_interface.py @@ -1,5 +1,6 @@ """Stream Interface base class """ +import io import logging import threading import time @@ -7,6 +8,8 @@ import traceback import serial # type: ignore[import-untyped] +from typing import Optional, cast + from meshtastic.mesh_interface import MeshInterface from meshtastic.util import is_windows11, stripnl @@ -19,7 +22,7 @@ MAX_TO_FROM_RADIO_SIZE = 512 class StreamInterface(MeshInterface): """Interface class for meshtastic devices over a stream link (serial, TCP, etc)""" - def __init__(self, debugOut=None, noProto=False, connectNow=True, noNodes=False): + def __init__(self, debugOut: Optional[io.TextIOWrapper]=None, noProto: bool=False, connectNow: bool=True, noNodes: bool=False) -> None: """Constructor, opens a connection to self.stream Keyword Arguments: @@ -51,7 +54,7 @@ class StreamInterface(MeshInterface): if not noProto: self.waitForConfig() - def connect(self): + def connect(self) -> None: """Connect to our radio Normally this is called automatically by the constructor, but if you @@ -62,7 +65,7 @@ class StreamInterface(MeshInterface): # if the reading statemachine was parsing a bad packet make sure # we write enough start bytes to force it to resync (we don't use START1 # because we want to ensure it is looking for START1) - p = bytearray([START2] * 32) + p: bytes = bytearray([START2] * 32) self._writeBytes(p) time.sleep(0.1) # wait 100ms to give device time to start running @@ -73,7 +76,7 @@ class StreamInterface(MeshInterface): if not self.noProto: # Wait for the db download if using the protocol self._waitConnected() - def _disconnected(self): + def _disconnected(self) -> None: """We override the superclass implementation to close our port""" MeshInterface._disconnected(self) @@ -85,7 +88,7 @@ class StreamInterface(MeshInterface): # pylint: disable=W0201 self.stream = None - def _writeBytes(self, b): + def _writeBytes(self, b: bytes) -> None: """Write an array of bytes to our stream and flush""" if self.stream: # ignore writes when stream is closed self.stream.write(b) @@ -97,24 +100,24 @@ class StreamInterface(MeshInterface): # we sleep here to give the TBeam a chance to work time.sleep(0.1) - def _readBytes(self, length): + def _readBytes(self, length) -> Optional[bytes]: """Read an array of bytes from our stream""" if self.stream: return self.stream.read(length) else: return None - def _sendToRadioImpl(self, toRadio): + def _sendToRadioImpl(self, toRadio) -> None: """Send a ToRadio protobuf to the device""" logging.debug(f"Sending: {stripnl(toRadio)}") - b = toRadio.SerializeToString() - bufLen = len(b) + b: bytes = toRadio.SerializeToString() + bufLen: int = len(b) # We convert into a string, because the TCP code doesn't work with byte arrays - header = bytes([START1, START2, (bufLen >> 8) & 0xFF, bufLen & 0xFF]) + header: bytes = bytes([START1, START2, (bufLen >> 8) & 0xFF, bufLen & 0xFF]) logging.debug(f"sending header:{header} b:{b}") self._writeBytes(header + b) - def close(self): + def close(self) -> None: """Close a connection to the device""" logging.debug("Closing stream") MeshInterface.close(self) @@ -124,7 +127,7 @@ class StreamInterface(MeshInterface): if self._rxThread != threading.current_thread(): self._rxThread.join() # wait for it to exit - def __reader(self): + def __reader(self) -> None: """The reader thread that reads bytes from our stream""" logging.debug("in __reader()") empty = bytes() @@ -132,13 +135,13 @@ class StreamInterface(MeshInterface): try: while not self._wantExit: # logging.debug("reading character") - b = self._readBytes(1) + b: Optional[bytes] = self._readBytes(1) # logging.debug("In reader loop") # logging.debug(f"read returned {b}") - if len(b) > 0: - c = b[0] + if b is not None and len(cast(bytes, b)) > 0: + c: int = b[0] # logging.debug(f'c:{c}') - ptr = len(self._rxBuf) + ptr: int = len(self._rxBuf) # Assume we want to append this byte, fixme use bytearray instead self._rxBuf = self._rxBuf + b diff --git a/meshtastic/tcp_interface.py b/meshtastic/tcp_interface.py index d049dc4..234e3ac 100644 --- a/meshtastic/tcp_interface.py +++ b/meshtastic/tcp_interface.py @@ -2,7 +2,7 @@ """ import logging import socket -from typing import Optional +from typing import Optional, cast from meshtastic.stream_interface import StreamInterface @@ -14,9 +14,9 @@ class TCPInterface(StreamInterface): self, hostname: str, debugOut=None, - noProto=False, - connectNow=True, - portNumber=4403, + noProto: bool=False, + connectNow: bool=True, + portNumber: int=4403, noNodes:bool=False, ): """Constructor, opens a connection to a specified IP address/hostname @@ -27,14 +27,16 @@ class TCPInterface(StreamInterface): self.stream = None - self.hostname = hostname - self.portNumber = portNumber + self.hostname: str = hostname + self.portNumber: int = portNumber + + self.socket: Optional[socket.socket] = None if connectNow: logging.debug(f"Connecting to {hostname}") # type: ignore[str-bytes-safe] - server_address = (hostname, portNumber) - sock = socket.create_connection(server_address) - self.socket: Optional[socket.socket] = sock + server_address: tuple[str, int] = (hostname, portNumber) + sock: Optional[socket.socket] = socket.create_connection(server_address) + self.socket = sock else: self.socket = None @@ -42,25 +44,26 @@ class TCPInterface(StreamInterface): self, debugOut=debugOut, noProto=noProto, connectNow=connectNow, noNodes=noNodes ) - def _socket_shutdown(self): + def _socket_shutdown(self) -> None: """Shutdown the socket. Note: Broke out this line so the exception could be unit tested. """ - self.socket.shutdown(socket.SHUT_RDWR) + if socket: + cast(socket.socket, self.socket).shutdown(socket.SHUT_RDWR) - def myConnect(self): + def myConnect(self) -> None: """Connect to socket""" - server_address = (self.hostname, self.portNumber) - sock = socket.create_connection(server_address) + server_address: tuple[str, int] = (self.hostname, self.portNumber) + sock: Optional[socket.socket] = socket.create_connection(server_address) self.socket = sock - def close(self): + def close(self) -> None: """Close a connection to the device""" logging.debug("Closing TCP stream") StreamInterface.close(self) # Sometimes the socket read might be blocked in the reader thread. # Therefore we force the shutdown by closing the socket here - self._wantExit = True + self._wantExit: bool = True if not self.socket is None: try: self._socket_shutdown() @@ -68,10 +71,14 @@ class TCPInterface(StreamInterface): pass # Ignore errors in shutdown, because we might have a race with the server self.socket.close() - def _writeBytes(self, b): + def _writeBytes(self, b: bytes) -> None: """Write an array of bytes to our stream and flush""" - self.socket.send(b) + if self.socket: + self.socket.send(b) - def _readBytes(self, length): + def _readBytes(self, length) -> Optional[bytes]: """Read an array of bytes from our stream""" - return self.socket.recv(length) + if self.socket: + return self.socket.recv(length) + else: + return None \ No newline at end of file diff --git a/meshtastic/test.py b/meshtastic/test.py index 97947d4..96d6c09 100644 --- a/meshtastic/test.py +++ b/meshtastic/test.py @@ -5,6 +5,9 @@ import logging import sys import time import traceback +import io + +from typing import List, Optional from dotmap import DotMap # type: ignore[import-untyped] from pubsub import pub # type: ignore[import-untyped] @@ -15,19 +18,19 @@ from meshtastic.serial_interface import SerialInterface from meshtastic.tcp_interface import TCPInterface """The interfaces we are using for our tests""" -interfaces = None +interfaces: List = [] """A list of all packets we received while the current test was running""" -receivedPackets = None +receivedPackets: Optional[List] = None -testsRunning = False +testsRunning: bool = False -testNumber = 0 +testNumber: int = 0 sendingInterface = None -def onReceive(packet, interface): +def onReceive(packet, interface) -> None: """Callback invoked when a packet arrives""" if sendingInterface == interface: pass @@ -42,20 +45,20 @@ def onReceive(packet, interface): receivedPackets.append(p) -def onNode(node): +def onNode(node) -> None: """Callback invoked when the node DB changes""" print(f"Node changed: {node}") -def subscribe(): +def subscribe() -> None: """Subscribe to the topics the user probably wants to see, prints output to stdout""" pub.subscribe(onNode, "meshtastic.node") def testSend( - fromInterface, toInterface, isBroadcast=False, asBinary=False, wantAck=False -): + fromInterface, toInterface, isBroadcast: bool=False, asBinary: bool=False, wantAck: bool=False +) -> bool: """ Sends one test packet between two nodes and then returns success or failure @@ -93,16 +96,16 @@ def testSend( return False # Failed to send -def runTests(numTests=50, wantAck=False, maxFailures=0): +def runTests(numTests: int=50, wantAck: bool=False, maxFailures: int=0) -> bool: """Run the tests.""" logging.info(f"Running {numTests} tests with wantAck={wantAck}") - numFail = 0 - numSuccess = 0 + numFail: int = 0 + numSuccess: int = 0 for _ in range(numTests): # pylint: disable=W0603 global testNumber testNumber = testNumber + 1 - isBroadcast = True + isBroadcast:bool = True # asBinary=(i % 2 == 0) success = testSend( interfaces[0], interfaces[1], isBroadcast, asBinary=False, wantAck=wantAck @@ -126,10 +129,10 @@ def runTests(numTests=50, wantAck=False, maxFailures=0): return True -def testThread(numTests=50): +def testThread(numTests=50) -> bool: """Test thread""" logging.info("Found devices, starting tests...") - result = runTests(numTests, wantAck=True) + result: bool = runTests(numTests, wantAck=True) if result: # Run another test # Allow a few dropped packets @@ -137,25 +140,25 @@ def testThread(numTests=50): return result -def onConnection(topic=pub.AUTO_TOPIC): +def onConnection(topic=pub.AUTO_TOPIC) -> None: """Callback invoked when we connect/disconnect from a radio""" print(f"Connection changed: {topic.getName()}") -def openDebugLog(portName): +def openDebugLog(portName) -> io.TextIOWrapper: """Open the debug log file""" debugname = "log" + portName.replace("/", "_") logging.info(f"Writing serial debugging to {debugname}") return open(debugname, "w+", buffering=1, encoding="utf8") -def testAll(numTests=5): +def testAll(numTests: int=5) -> bool: """ Run a series of tests using devices we can find. This is called from the cli with the "--test" option. """ - ports = meshtastic.util.findPorts(True) + ports: List[str] = meshtastic.util.findPorts(True) if len(ports) < 2: meshtastic.util.our_exit( "Warning: Must have at least two devices connected to USB." @@ -175,7 +178,7 @@ def testAll(numTests=5): ) logging.info("Ports opened, starting test") - result = testThread(numTests) + result: bool = testThread(numTests) for i in interfaces: i.close() @@ -183,7 +186,7 @@ def testAll(numTests=5): return result -def testSimulator(): +def testSimulator() -> None: """ Assume that someone has launched meshtastic-native as a simulated node. Talk to that node over TCP, do some operations and if they are successful @@ -195,7 +198,7 @@ def testSimulator(): logging.basicConfig(level=logging.DEBUG) logging.info("Connecting to simulator on localhost!") try: - iface = TCPInterface("localhost") + iface: meshtastic.tcp_interface.TCPInterface = TCPInterface("localhost") iface.showInfo() iface.localNode.showInfo() iface.localNode.exitSimulator() diff --git a/meshtastic/tunnel.py b/meshtastic/tunnel.py index 40a1c2e..5634524 100644 --- a/meshtastic/tunnel.py +++ b/meshtastic/tunnel.py @@ -42,7 +42,7 @@ class Tunnel: self.message = message super().__init__(self.message) - def __init__(self, iface, subnet="10.115", netmask="255.255.0.0"): + def __init__(self, iface, subnet: str="10.115", netmask: str="255.255.0.0") -> None: """ Constructor diff --git a/meshtastic/util.py b/meshtastic/util.py index 14f6a54..e9fec16 100644 --- a/meshtastic/util.py +++ b/meshtastic/util.py @@ -11,7 +11,7 @@ import threading import time import traceback from queue import Queue -from typing import List, NoReturn, Union +from typing import Any, Dict, List, NoReturn, Optional, Set, Tuple, Union from google.protobuf.json_format import MessageToJson from google.protobuf.message import Message @@ -25,23 +25,23 @@ from meshtastic.supported_device import supported_devices from meshtastic.version import get_active_version """Some devices such as a seger jlink we never want to accidentally open""" -blacklistVids = dict.fromkeys([0x1366]) +blacklistVids: Dict = dict.fromkeys([0x1366]) -def quoteBooleans(a_string): +def quoteBooleans(a_string: str) -> str: """Quote booleans given a string that contains ": true", replace with ": 'true'" (or false) """ - tmp = a_string.replace(": true", ": 'true'") + tmp: str = a_string.replace(": true", ": 'true'") tmp = tmp.replace(": false", ": 'false'") return tmp -def genPSK256(): +def genPSK256() -> bytes: """Generate a random preshared key""" return os.urandom(32) -def fromPSK(valstr): +def fromPSK(valstr: str) -> Any: """A special version of fromStr that assumes the user is trying to set a PSK. In that case we also allow "none", "default" or "random" (to have python generate one), or simpleN """ @@ -58,7 +58,7 @@ def fromPSK(valstr): return fromStr(valstr) -def fromStr(valstr): +def fromStr(valstr: str) -> Any: """Try to parse as int, float or bool (and fallback to a string as last resort) Returns: an int, bool, float, str or byte array (for strings of hex digits) @@ -66,6 +66,7 @@ def fromStr(valstr): Args: valstr (string): A user provided string """ + val: Any if len(valstr) == 0: # Treat an emptystring as an empty bytes val = bytes() elif valstr.startswith("0x"): @@ -88,7 +89,7 @@ def fromStr(valstr): return val -def pskToString(psk: bytes): +def pskToString(psk: bytes) -> str: """Given an array of PSK bytes, decode them into a human readable (but privacy protecting) string""" if len(psk) == 0: return "unencrypted" @@ -110,12 +111,12 @@ def stripnl(s) -> str: return " ".join(s.split()) -def fixme(message): +def fixme(message: str) -> None: """Raise an exception for things that needs to be fixed""" raise Exception(f"FIXME: {message}") # pylint: disable=W0719 -def catchAndIgnore(reason, closure): +def catchAndIgnore(reason: str, closure) -> None: """Call a closure but if it throws an exception print it and continue""" try: closure() @@ -130,7 +131,7 @@ def findPorts(eliminate_duplicates: bool=False) -> List[str]: Returns: list -- a list of device paths """ - l = list( + l: List = list( map( lambda port: port.device, filter( @@ -156,12 +157,12 @@ class dotdict(dict): class Timeout: """Timeout class""" - def __init__(self, maxSecs: int=20): + def __init__(self, maxSecs: int=20) -> None: self.expireTime: Union[int, float] = 0 self.sleepInterval: float = 0.1 self.expireTimeout: int = maxSecs - def reset(self): + def reset(self) -> None: """Restart the waitForSet timer""" self.expireTime = time.time() + self.expireTimeout @@ -220,7 +221,7 @@ class Timeout: class Acknowledgment: "A class that records which type of acknowledgment was just received, if any." - def __init__(self): + def __init__(self) -> None: """initialize""" self.receivedAck = False self.receivedNak = False @@ -229,7 +230,7 @@ class Acknowledgment: self.receivedTelemetry = False self.receivedPosition = False - def reset(self): + def reset(self) -> None: """reset""" self.receivedAck = False self.receivedNak = False @@ -242,17 +243,17 @@ class Acknowledgment: class DeferredExecution: """A thread that accepts closures to run, and runs them as they are received""" - def __init__(self, name=None): - self.queue = Queue() + def __init__(self, name=None) -> None: + self.queue: Queue = Queue() self.thread = threading.Thread(target=self._run, args=(), name=name) self.thread.daemon = True self.thread.start() - def queueWork(self, runnable): + def queueWork(self, runnable) -> None: """Queue up the work""" self.queue.put(runnable) - def _run(self): + def _run(self) -> None: while True: try: o = self.queue.get() @@ -272,7 +273,7 @@ def our_exit(message, return_value=1) -> NoReturn: sys.exit(return_value) -def support_info(): +def support_info() -> None: """Print out info that helps troubleshooting of the cli.""" print("") print("If having issues with meshtastic cli or python library") @@ -301,7 +302,7 @@ def support_info(): print("Please add the output from the command: meshtastic --info") -def remove_keys_from_dict(keys, adict): +def remove_keys_from_dict(keys: Union[Tuple, List, Set], adict: Dict) -> Dict: """Return a dictionary without some keys in it. Will removed nested keys. """ @@ -316,33 +317,33 @@ def remove_keys_from_dict(keys, adict): return adict -def hexstr(barray): +def hexstr(barray: bytes) -> str: """Print a string of hex digits""" return ":".join(f"{x:02x}" for x in barray) -def ipstr(barray): +def ipstr(barray: bytes) -> str: """Print a string of ip digits""" return ".".join(f"{x}" for x in barray) -def readnet_u16(p, offset): +def readnet_u16(p, offset: int) -> int: """Read big endian u16 (network byte order)""" return p[offset] * 256 + p[offset + 1] -def convert_mac_addr(val): +def convert_mac_addr(val: bytes) -> Union[str, bytes]: """Convert the base 64 encoded value to a mac address val - base64 encoded value (ex: '/c0gFyhb')) returns: a string formatted like a mac address (ex: 'fd:cd:20:17:28:5b') """ - if not re.match("[0-9a-f]{2}([-:]?)[0-9a-f]{2}(\\1[0-9a-f]{2}){4}$", val): - val_as_bytes = base64.b64decode(val) + if not re.match("[0-9a-f]{2}([-:]?)[0-9a-f]{2}(\\1[0-9a-f]{2}){4}$", val): #FIXME - does the regex have to be bytes too to match val since val is bytes? + val_as_bytes: bytes = base64.b64decode(val) return hexstr(val_as_bytes) return val -def snake_to_camel(a_string): +def snake_to_camel(a_string: str) -> str: """convert snake_case to camelCase""" # split underscore using split temp = a_string.split("_") @@ -351,16 +352,16 @@ def snake_to_camel(a_string): return result -def camel_to_snake(a_string): +def camel_to_snake(a_string: str) -> str: """convert camelCase to snake_case""" return "".join(["_" + i.lower() if i.isupper() else i for i in a_string]).lstrip( "_" ) -def detect_supported_devices(): +def detect_supported_devices() -> Set: """detect supported devices based on vendor id""" - system = platform.system() + system: str = platform.system() # print(f'system:{system}') possible_devices = set() @@ -418,9 +419,9 @@ def detect_supported_devices(): return possible_devices -def detect_windows_needs_driver(sd, print_reason=False): +def detect_windows_needs_driver(sd, print_reason=False) -> bool: """detect if Windows user needs to install driver for a supported device""" - need_to_install_driver = False + need_to_install_driver: bool = False if sd: system = platform.system() @@ -446,7 +447,7 @@ def detect_windows_needs_driver(sd, print_reason=False): return need_to_install_driver -def eliminate_duplicate_port(ports): +def eliminate_duplicate_port(ports: List) -> List: """Sometimes we detect 2 serial ports, but we really only need to use one of the ports. ports is a list of ports @@ -479,9 +480,9 @@ def eliminate_duplicate_port(ports): return new_ports -def is_windows11(): +def is_windows11() -> bool: """Detect if Windows 11""" - is_win11 = False + is_win11: bool = False if platform.system() == "Windows": if float(platform.release()) >= 10.0: patch = platform.version().split(".")[2] @@ -495,7 +496,7 @@ def is_windows11(): return is_win11 -def get_unique_vendor_ids(): +def get_unique_vendor_ids() -> Set[str]: """Return a set of unique vendor ids""" vids = set() for d in supported_devices: @@ -504,7 +505,7 @@ def get_unique_vendor_ids(): return vids -def get_devices_with_vendor_id(vid): +def get_devices_with_vendor_id(vid: str) -> Set: #Set[SupportedDevice] """Return a set of unique devices with the vendor id""" sd = set() for d in supported_devices: @@ -513,11 +514,11 @@ def get_devices_with_vendor_id(vid): return sd -def active_ports_on_supported_devices(sds, eliminate_duplicates=False): +def active_ports_on_supported_devices(sds, eliminate_duplicates=False) -> Set[str]: """Return a set of active ports based on the supplied supported devices""" - ports = set() - baseports = set() - system = platform.system() + ports: Set = set() + baseports: Set = set() + system: str = platform.system() # figure out what possible base ports there are for d in sds: @@ -575,13 +576,13 @@ def active_ports_on_supported_devices(sds, eliminate_duplicates=False): for com_port in com_ports: ports.add(com_port) if eliminate_duplicates: - ports = eliminate_duplicate_port(list(ports)) - ports.sort() - ports = set(ports) + portlist: List = eliminate_duplicate_port(list(ports)) + portlist.sort() + ports = set(portlist) return ports -def detect_windows_port(sd): +def detect_windows_port(sd) -> Set[str]: #"sd" is a SupportedDevice from meshtastic.supported_device """detect if Windows port""" ports = set() @@ -606,11 +607,11 @@ def detect_windows_port(sd): return ports -def check_if_newer_version(): +def check_if_newer_version() -> Optional[str]: """Check pip to see if we are running the latest version.""" - pypi_version = None + pypi_version: Optional[str] = None try: - url = "https://pypi.org/pypi/meshtastic/json" + url: str = "https://pypi.org/pypi/meshtastic/json" data = requests.get(url, timeout=5).json() pypi_version = data["info"]["version"] except Exception: @@ -620,6 +621,10 @@ def check_if_newer_version(): try: parsed_act_version = pkg_version.parse(act_version) parsed_pypi_version = pkg_version.parse(pypi_version) + #Note: if handed "None" when we can't download the pypi_version, + #this gets a TypeError: + #"TypeError: expected string or bytes-like object, got 'NoneType'" + #Handle that below? except pkg_version.InvalidVersion: return pypi_version From 60de9dddb18cd7b5067db5269a5eea00b2187610 Mon Sep 17 00:00:00 2001 From: William Stearns Date: Tue, 9 Jul 2024 19:54:01 -0400 Subject: [PATCH 2/7] Remove references to BLEClient breaking CI checks --- meshtastic/ble_interface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meshtastic/ble_interface.py b/meshtastic/ble_interface.py index 2bdc83d..b7e7230 100644 --- a/meshtastic/ble_interface.py +++ b/meshtastic/ble_interface.py @@ -118,10 +118,10 @@ class BLEInterface(MeshInterface): .replace(":", "") \ .lower() - def connect(self, address) -> BLEClient: + def connect(self, address): "Connect to a device by address" device = self.find_device(address) - client: BLEClient = BLEClient(device.address) + client = BLEClient(device.address) client.connect() try: client.pair() From f77e788aa8f7a09fa39606562913ba45e9cf650f Mon Sep 17 00:00:00 2001 From: William Stearns Date: Thu, 10 Oct 2024 16:33:07 -0400 Subject: [PATCH 3/7] fix missing import --- meshtastic/stream_interface.py | 1 + 1 file changed, 1 insertion(+) diff --git a/meshtastic/stream_interface.py b/meshtastic/stream_interface.py index ca26f3a..471111b 100644 --- a/meshtastic/stream_interface.py +++ b/meshtastic/stream_interface.py @@ -1,5 +1,6 @@ """Stream Interface base class """ +import io import logging import threading import time From 58d9039a047c94d2715ab5720cfbb9fe8c3a9f74 Mon Sep 17 00:00:00 2001 From: William Stearns Date: Thu, 10 Oct 2024 16:49:06 -0400 Subject: [PATCH 4/7] another missing import --- meshtastic/ble_interface.py | 1 + 1 file changed, 1 insertion(+) diff --git a/meshtastic/ble_interface.py b/meshtastic/ble_interface.py index 7b3e24c..a3d076d 100644 --- a/meshtastic/ble_interface.py +++ b/meshtastic/ble_interface.py @@ -5,6 +5,7 @@ import atexit import logging import struct import time +import io from threading import Thread from typing import List, Optional, Tuple From 0da405168f78010b880a6735a486ac8553db1135 Mon Sep 17 00:00:00 2001 From: William Stearns Date: Thu, 10 Oct 2024 23:49:20 -0400 Subject: [PATCH 5/7] pylint cleanups --- meshtastic/__main__.py | 3 ++- meshtastic/ble_interface.py | 2 +- meshtastic/stream_interface.py | 4 ++-- meshtastic/tcp_interface.py | 4 ++-- meshtastic/util.py | 5 +++-- 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index 5f1797e..3c1828d 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -1046,7 +1046,8 @@ def export_config(interface) -> str: 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 = "# 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) print(config_txt) return config_txt diff --git a/meshtastic/ble_interface.py b/meshtastic/ble_interface.py index a3d076d..16d1997 100644 --- a/meshtastic/ble_interface.py +++ b/meshtastic/ble_interface.py @@ -7,7 +7,7 @@ import struct import time import io from threading import Thread -from typing import List, Optional, Tuple +from typing import List, Optional import google.protobuf from bleak import BleakClient, BleakScanner, BLEDevice diff --git a/meshtastic/stream_interface.py b/meshtastic/stream_interface.py index 471111b..abaef4a 100644 --- a/meshtastic/stream_interface.py +++ b/meshtastic/stream_interface.py @@ -6,10 +6,10 @@ import threading import time import traceback -import serial # type: ignore[import-untyped] - from typing import Optional, cast +import serial # type: ignore[import-untyped] + from meshtastic.mesh_interface import MeshInterface from meshtastic.util import is_windows11, stripnl diff --git a/meshtastic/tcp_interface.py b/meshtastic/tcp_interface.py index cff92ba..791b8c2 100644 --- a/meshtastic/tcp_interface.py +++ b/meshtastic/tcp_interface.py @@ -49,7 +49,7 @@ class TCPInterface(StreamInterface): """Shutdown the socket. Note: Broke out this line so the exception could be unit tested. """ - if socket: + if self.socket: #mian: please check that this should be "if self.socket:" cast(socket.socket, self.socket).shutdown(socket.SHUT_RDWR) def myConnect(self) -> None: @@ -82,4 +82,4 @@ class TCPInterface(StreamInterface): if self.socket: return self.socket.recv(length) else: - return None \ No newline at end of file + return None diff --git a/meshtastic/util.py b/meshtastic/util.py index fce9256..d7fd352 100644 --- a/meshtastic/util.py +++ b/meshtastic/util.py @@ -366,7 +366,8 @@ def convert_mac_addr(val: bytes) -> Union[str, bytes]: val - base64 encoded value (ex: '/c0gFyhb')) returns: a string formatted like a mac address (ex: 'fd:cd:20:17:28:5b') """ - if not re.match("[0-9a-f]{2}([-:]?)[0-9a-f]{2}(\\1[0-9a-f]{2}){4}$", val): #FIXME - does the regex have to be bytes too to match val since val is bytes? + if not re.match("[0-9a-f]{2}([-:]?)[0-9a-f]{2}(\\1[0-9a-f]{2}){4}$", val): #FIXME - does the regex have to be bytes too to + #match val since val is bytes? val_as_bytes: bytes = base64.b64decode(val) return hexstr(val_as_bytes) return val @@ -650,7 +651,7 @@ def check_if_newer_version() -> Optional[str]: try: parsed_act_version = pkg_version.parse(act_version) parsed_pypi_version = pkg_version.parse(pypi_version) - #Note: if handed "None" when we can't download the pypi_version, + #Note: if handed "None" when we can't download the pypi_version, #this gets a TypeError: #"TypeError: expected string or bytes-like object, got 'NoneType'" #Handle that below? From e335f12a3b867f582c1b61c98ef510a5609593b9 Mon Sep 17 00:00:00 2001 From: William Stearns Date: Fri, 11 Oct 2024 00:59:02 -0400 Subject: [PATCH 6/7] attempts to fix mypy issues --- meshtastic/__main__.py | 2 +- meshtastic/ble_interface.py | 2 +- meshtastic/serial_interface.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index 3c1828d..2298391 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -104,7 +104,7 @@ def getPref(node, comp_name) -> bool: for config in [localConfig, moduleConfig]: objDesc = config.DESCRIPTOR config_type = objDesc.fields_by_name.get(name[0]) - pref = False #FIXME - checkme - Used here as boolean, but set 2 lines below as a string. + 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: diff --git a/meshtastic/ble_interface.py b/meshtastic/ble_interface.py index 16d1997..a0cb481 100644 --- a/meshtastic/ble_interface.py +++ b/meshtastic/ble_interface.py @@ -151,7 +151,7 @@ class BLEInterface(MeshInterface): ) return addressed_devices[0] - def _sanitize_address(address: Optional[str]) -> Optional[str]: # pylint: disable=E0213 + def _sanitize_address(self, address: Optional[str]) -> Optional[str]: # pylint: disable=E0213 "Standardize BLE address by removing extraneous characters and lowercasing." if address is None: return None diff --git a/meshtastic/serial_interface.py b/meshtastic/serial_interface.py index e437e20..71b2fec 100644 --- a/meshtastic/serial_interface.py +++ b/meshtastic/serial_interface.py @@ -58,7 +58,7 @@ class SerialInterface(StreamInterface): self.stream = serial.Serial( self.devPath, 115200, exclusive=True, timeout=0.5, write_timeout=0 ) - self.stream.flush() + self.stream.flush() # type: ignore[attr-defined] time.sleep(0.1) StreamInterface.__init__( From 384063db19d852ee2c6fae2d5ce34227c874c167 Mon Sep 17 00:00:00 2001 From: Ian McEwen Date: Tue, 29 Oct 2024 06:47:16 -0700 Subject: [PATCH 7/7] Fix some remaining mypy complaints --- meshtastic/__main__.py | 2 +- meshtastic/ble_interface.py | 4 ++++ meshtastic/mt_config.py | 4 +++- meshtastic/stream_interface.py | 3 ++- meshtastic/util.py | 7 ++++--- 5 files changed, 14 insertions(+), 6 deletions(-) diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index 2870556..9aeef2e 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -130,7 +130,7 @@ def getPref(node, comp_name) -> bool: return False # Check if we need to request the config - if len(config.ListFields()) != 0: + 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: diff --git a/meshtastic/ble_interface.py b/meshtastic/ble_interface.py index f020979..76e5dc3 100644 --- a/meshtastic/ble_interface.py +++ b/meshtastic/ble_interface.py @@ -174,6 +174,10 @@ class BLEInterface(MeshInterface): self.should_read = False retries: int = 0 while self._want_receive: + if self.client is None: + logging.debug(f"BLE client is None, shutting down") + self._want_receive = False + continue try: b = bytes(self.client.read_gatt_char(FROMRADIO_UUID)) except BleakDBusError as e: diff --git a/meshtastic/mt_config.py b/meshtastic/mt_config.py index 662a10d..3b40294 100644 --- a/meshtastic/mt_config.py +++ b/meshtastic/mt_config.py @@ -13,6 +13,8 @@ with rather more easily once the code is simplified by this change. """ +from typing import Any, Optional + def reset(): """ Restore the namespace to pristine condition. @@ -33,5 +35,5 @@ args = None parser = None channel_index = None logfile = None -tunnelInstance = None +tunnelInstance: Optional[Any] = None camel_case = False diff --git a/meshtastic/stream_interface.py b/meshtastic/stream_interface.py index abaef4a..c1fecab 100644 --- a/meshtastic/stream_interface.py +++ b/meshtastic/stream_interface.py @@ -38,6 +38,7 @@ class StreamInterface(MeshInterface): raise Exception( # pylint: disable=W0719 "StreamInterface is now abstract (to update existing code create SerialInterface instead)" ) + self.stream: Optional[serial.Serial] # only serial uses this, TCPInterface overrides the relevant methods instead self._rxBuf = bytes() # empty self._wantExit = False @@ -115,7 +116,7 @@ class StreamInterface(MeshInterface): bufLen: int = len(b) # We convert into a string, because the TCP code doesn't work with byte arrays header: bytes = bytes([START1, START2, (bufLen >> 8) & 0xFF, bufLen & 0xFF]) - logging.debug(f"sending header:{header} b:{b}") + logging.debug(f"sending header:{header!r} b:{b!r}") self._writeBytes(header + b) def close(self) -> None: diff --git a/meshtastic/util.py b/meshtastic/util.py index 2490660..2e0a989 100644 --- a/meshtastic/util.py +++ b/meshtastic/util.py @@ -369,13 +369,12 @@ def readnet_u16(p, offset: int) -> int: return p[offset] * 256 + p[offset + 1] -def convert_mac_addr(val: bytes) -> Union[str, bytes]: +def convert_mac_addr(val: str) -> Union[str, bytes]: """Convert the base 64 encoded value to a mac address val - base64 encoded value (ex: '/c0gFyhb')) returns: a string formatted like a mac address (ex: 'fd:cd:20:17:28:5b') """ - if not re.match("[0-9a-f]{2}([-:]?)[0-9a-f]{2}(\\1[0-9a-f]{2}){4}$", val): #FIXME - does the regex have to be bytes too to - #match val since val is bytes? + if not re.match("[0-9a-f]{2}([-:]?)[0-9a-f]{2}(\\1[0-9a-f]{2}){4}$", val): val_as_bytes: bytes = base64.b64decode(val) return hexstr(val_as_bytes) return val @@ -656,6 +655,8 @@ def check_if_newer_version() -> Optional[str]: pass act_version = get_active_version() + if pypi_version is None: + return None try: parsed_act_version = pkg_version.parse(act_version) parsed_pypi_version = pkg_version.parse(pypi_version)