Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion meshtastic/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1260,7 +1260,7 @@ def initParser():
help="Request telemetry from a node. "
"You need pass the destination ID as argument with '--dest'. "
"For repeaters, the nodeNum is required.",
action="store_true",
action="store_true",
)

parser.add_argument(
Expand Down
55 changes: 31 additions & 24 deletions meshtastic/ble_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
import logging
import time
import struct
import asyncio
from threading import Thread, Event
from meshtastic.mesh_interface import MeshInterface
from meshtastic.util import our_exit
from bleak import BleakScanner, BleakClient
import asyncio

from meshtastic.mesh_interface import MeshInterface
from meshtastic.util import our_exit

SERVICE_UUID = "6ba1b218-15a8-461f-9fa8-5dcae273eafd"
TORADIO_UUID = "f75c76d2-129e-4dad-a1dd-7866124401e7"
Expand All @@ -17,13 +17,14 @@


class BLEInterface(MeshInterface):
"""MeshInterface using BLE to connect to devices"""
class BLEError(Exception):
"""An exception class for BLE errors"""
def __init__(self, message):
self.message = message
super().__init__(self.message)


class BLEState():
class BLEState(): # pylint: disable=C0115
THREADS = False
BLE = False
MESH = False
Expand Down Expand Up @@ -69,13 +70,14 @@ def __init__(self, address, noProto = False, debugOut = None):
self.client.start_notify(FROMNUM_UUID, self.from_num_handler)


async def from_num_handler(self, _, b):
async def from_num_handler(self, _, b): # pylint: disable=C0116
from_num = struct.unpack('<I', bytes(b))[0]
logging.debug(f"FROMNUM notify: {from_num}")
self.should_read = True


def scan(self):
"Scan for available BLE devices"
with BLEClient() as client:
return [
(x[0], x[1]) for x in (client.discover(
Expand All @@ -86,27 +88,32 @@ def scan(self):


def find_device(self, address):
"Find a device by address"
meshtastic_devices = self.scan()

addressed_devices = list(filter(lambda x: address == x[1].local_name or address == x[0].name, meshtastic_devices))
addressed_devices = list(filter(lambda x: address in (x[1].local_name, x[0].name), meshtastic_devices))
# If nothing is found try on the address
if len(addressed_devices) == 0:
addressed_devices = list(filter(lambda x: BLEInterface._sanitize_address(address) == BLEInterface._sanitize_address(x[0].address), meshtastic_devices))
addressed_devices = list(filter(
lambda x: BLEInterface._sanitize_address(address) == BLEInterface._sanitize_address(x[0].address),
meshtastic_devices))

if len(addressed_devices) == 0:
raise BLEInterface.BLEError(f"No Meshtastic BLE peripheral with identifier or address '{address}' found. Try --ble-scan to find it.")
if len(addressed_devices) > 1:
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):
def _sanitize_address(address): # pylint: disable=E0213
"Standardize BLE address by removing extraneous characters and lowercasing"
return address \
.replace("-", "") \
.replace("_", "") \
.replace(":", "") \
.lower()

def connect(self, address):
"Connect to a device by address"
device = self.find_device(address)
client = BLEClient(device.address)
client.connect()
Expand Down Expand Up @@ -156,13 +163,14 @@ def close(self):
if self.state.THREADS:
self._receiveThread_started.clear()
self._receiveThread_stopped.wait(5)

if self.state.BLE:
self.client.disconnect()
self.client.close()


class BLEClient():
"""Client for managing connection to a BLE device"""
def __init__(self, address = None, **kwargs):
self._eventThread = Thread(target = self._run_event_loop)
self._eventThread_started = Event()
Expand All @@ -177,47 +185,46 @@ def __init__(self, address = None, **kwargs):
self.bleak_client = BleakClient(address, **kwargs)


def discover(self, **kwargs):
def discover(self, **kwargs): # pylint: disable=C0116
return self.async_await(BleakScanner.discover(**kwargs))

def pair(self, **kwargs):
def pair(self, **kwargs): # pylint: disable=C0116
return self.async_await(self.bleak_client.pair(**kwargs))

def connect(self, **kwargs):
def connect(self, **kwargs): # pylint: disable=C0116
return self.async_await(self.bleak_client.connect(**kwargs))

def disconnect(self, **kwargs):
def disconnect(self, **kwargs): # pylint: disable=C0116
self.async_await(self.bleak_client.disconnect(**kwargs))

def read_gatt_char(self, *args, **kwargs):
def read_gatt_char(self, *args, **kwargs): # pylint: disable=C0116
return self.async_await(self.bleak_client.read_gatt_char(*args, **kwargs))

def write_gatt_char(self, *args, **kwargs):
def write_gatt_char(self, *args, **kwargs): # pylint: disable=C0116
self.async_await(self.bleak_client.write_gatt_char(*args, **kwargs))

def start_notify(self, *args, **kwargs):
def start_notify(self, *args, **kwargs): # pylint: disable=C0116
self.async_await(self.bleak_client.start_notify(*args, **kwargs))


def close(self):
def close(self): # pylint: disable=C0116
self.async_run(self._stop_event_loop())
self._eventThread_stopped.wait(5)

def __enter__(self):
return self

def __exit__(self, type, value, traceback):
def __exit__(self, _type, _value, _traceback):
self.close()


def async_await(self, coro, timeout = None):
def async_await(self, coro, timeout = None): # pylint: disable=C0116
return self.async_run(coro).result(timeout)

def async_run(self, coro):
def async_run(self, coro): # pylint: disable=C0116
return asyncio.run_coroutine_threadsafe(coro, self._eventLoop)

def _run_event_loop(self):
self._eventLoop = asyncio.new_event_loop()
# I don't know if the event loop can be initialized in __init__ so silencing pylint
self._eventLoop = asyncio.new_event_loop() # pylint: disable=W0201
self._eventThread_started.set()
try:
self._eventLoop.run_forever()
Expand Down
2 changes: 1 addition & 1 deletion meshtastic/globals.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def getInstance():
def __init__(self):
"""Constructor for the Globals CLass"""
if Globals.__instance is not None:
raise Exception("This class is a singleton")
raise Exception("This class is a singleton") # pylint: disable=W0719
else:
Globals.__instance = self
self.args = None
Expand Down
31 changes: 18 additions & 13 deletions meshtastic/mesh_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
BROADCAST_ADDR,
BROADCAST_NUM,
LOCAL_ADDR,
OUR_APP_VERSION,
ResponseHandler,
protocols,
publishingThread,
Expand All @@ -48,6 +47,12 @@ class MeshInterface:
debugOut
"""

class MeshInterfaceError(Exception):
"""An exception class for general mesh interface errors"""
def __init__(self, message):
self.message = message
super().__init__(self.message)

def __init__(self, debugOut=None, noProto=False):
"""Constructor

Expand Down Expand Up @@ -314,7 +319,7 @@ def sendData(
f"mesh_pb2.Constants.DATA_PAYLOAD_LEN: {mesh_pb2.Constants.DATA_PAYLOAD_LEN}"
)
if len(data) > mesh_pb2.Constants.DATA_PAYLOAD_LEN:
raise Exception("Data payload too big")
raise MeshInterface.MeshInterfaceError("Data payload too big")

if (
portNum == portnums_pb2.PortNum.UNKNOWN_APP
Expand Down Expand Up @@ -440,7 +445,7 @@ def sendTelemetry(self, destinationId=BROADCAST_ADDR, wantResponse=False):
destinationId = int(destinationId[1:], 16)
else:
destinationId = int(destinationId)

self.sendData(
r,
destinationId=destinationId,
Expand Down Expand Up @@ -469,7 +474,7 @@ def onResponseTelemetry(self, p):
)
if telemetry.device_metrics.air_util_tx is not None:
print(f"Transmit air utilization: {telemetry.device_metrics.air_util_tx:.2f}%")

elif p["decoded"]["portnum"] == 'ROUTING_APP':
if p["decoded"]["routing"]["errorReason"] == 'NO_RESPONSE':
our_exit("No response from node. At least firmware 2.1.22 is required on the destination node.")
Expand Down Expand Up @@ -543,25 +548,25 @@ def waitForConfig(self):
and self.localNode.waitForConfig()
)
if not success:
raise Exception("Timed out waiting for interface config")
raise MeshInterface.MeshInterfaceError("Timed out waiting for interface config")

def waitForAckNak(self):
"""Wait for the ack/nak"""
success = self._timeout.waitForAckNak(self._acknowledgment)
if not success:
raise Exception("Timed out waiting for an acknowledgment")
raise MeshInterface.MeshInterfaceError("Timed out waiting for an acknowledgment")

def waitForTraceRoute(self, waitFactor):
"""Wait for trace route"""
success = self._timeout.waitForTraceRoute(waitFactor, self._acknowledgment)
if not success:
raise Exception("Timed out waiting for traceroute")
raise MeshInterface.MeshInterfaceError("Timed out waiting for traceroute")

def waitForTelemetry(self):
"""Wait for telemetry"""
success = self._timeout.waitForTelemetry(self._acknowledgment)
if not success:
raise Exception("Timed out waiting for telemetry")
raise MeshInterface.MeshInterfaceError("Timed out waiting for telemetry")

def getMyNodeInfo(self):
"""Get info about my node."""
Expand Down Expand Up @@ -596,7 +601,7 @@ def _waitConnected(self, timeout=30.0):
and raise an exception"""
if not self.noProto:
if not self.isConnected.wait(timeout): # timeout after x seconds
raise Exception("Timed out waiting for connection completion")
raise MeshInterface.MeshInterfaceError("Timed out waiting for connection completion")

# If we failed while connecting, raise the connection to the client
if self.failure:
Expand All @@ -605,7 +610,7 @@ def _waitConnected(self, timeout=30.0):
def _generatePacketId(self):
"""Get a new unique packet ID"""
if self.currentPacketId is None:
raise Exception("Not connected yet, can not generate packet")
raise MeshInterface.MeshInterfaceError("Not connected yet, can not generate packet")
else:
self.currentPacketId = (self.currentPacketId + 1) & 0xFFFFFFFF
return self.currentPacketId
Expand Down Expand Up @@ -774,7 +779,7 @@ def _handleFromRadio(self, fromRadioBytes):
failmsg = None

if failmsg:
self.failure = Exception(failmsg)
self.failure = MeshInterface.MeshInterfaceError(failmsg)
self.isConnected.set() # let waitConnected return this exception
self.close()

Expand Down Expand Up @@ -920,7 +925,7 @@ def _nodeNumToId(self, num):
def _getOrCreateByNum(self, nodeNum):
"""Given a nodenum find the NodeInfo in the DB (or create if necessary)"""
if nodeNum == BROADCAST_NUM:
raise Exception("Can not create/find nodenum by the broadcast num")
raise MeshInterface.MeshInterfaceError("Can not create/find nodenum by the broadcast num")

if nodeNum in self.nodesByNum:
return self.nodesByNum[nodeNum]
Expand Down
5 changes: 2 additions & 3 deletions meshtastic/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ def onResponseRequestSettings(self, p):
print(f"{str(camel_to_snake(field))}:\n{str(config_values)}")

def requestConfig(self, configType):
"""Request the config from the node via admin message"""
if self == self.iface.localNode:
onResponse = None
else:
Expand Down Expand Up @@ -688,9 +689,6 @@ def onResponseRequestChannel(self, p):
logging.debug(f"Received channel {stripnl(c)}")
index = c.index

# for stress testing, we can always download all channels
fastChannelDownload = True

if index >= 8 - 1:
logging.debug("Finished downloading channels")

Expand All @@ -703,6 +701,7 @@ def onResponseRequestChannel(self, p):
self._requestChannel(index + 1)

def onAckNak(self, p):
"""Informative handler for ACK/NAK responses"""
if p["decoded"]["routing"]["errorReason"] != "NONE":
print(
f'Received a NAK, error reason: {p["decoded"]["routing"]["errorReason"]}'
Expand Down
2 changes: 1 addition & 1 deletion meshtastic/stream_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def __init__(self, debugOut=None, noProto=False, connectNow=True):
"""

if not hasattr(self, "stream") and not noProto:
raise Exception(
raise Exception( # pylint: disable=W0719
"StreamInterface is now abstract (to update existing code create SerialInterface instead)"
)
self._rxBuf = bytes() # empty
Expand Down
4 changes: 2 additions & 2 deletions meshtastic/tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
tunnelMain,
)

from ..channel_pb2 import Channel
from ..channel_pb2 import Channel # pylint: disable=E0611

# from ..ble_interface import BLEInterface
from ..node import Node
Expand Down Expand Up @@ -388,7 +388,7 @@ def test_main_onConnected_exception(capsys):
Globals.getInstance().set_args(sys.argv)

def throw_an_exception(junk):
raise Exception("Fake exception.")
raise Exception("Fake exception.") # pylint: disable=W0719

iface = MagicMock(autospec=SerialInterface)
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface):
Expand Down
2 changes: 1 addition & 1 deletion meshtastic/tests/test_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import pytest

# from ..admin_pb2 import AdminMessage
from ..channel_pb2 import Channel
from ..channel_pb2 import Channel # pylint: disable=E0611
from ..node import Node
from ..serial_interface import SerialInterface

Expand Down
2 changes: 1 addition & 1 deletion meshtastic/tests/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ def test_catchAndIgnore(caplog):
"""Test catchAndIgnore() does not actually throw an exception, but just logs"""

def some_closure():
raise Exception("foo")
raise Exception("foo") # pylint: disable=W0719

with caplog.at_level(logging.DEBUG):
catchAndIgnore("something", some_closure)
Expand Down
Loading