Compare commits

..

16 Commits
2.7.2 ... 2.7.3

Author SHA1 Message Date
github-actions
3615135e97 bump version to 2.7.3 2025-09-18 16:42:32 +00:00
Ian McEwen
cfb23788e1 Merge pull request #812 from shukari/loggingHandler2
Added Logging Handler Names & windows pytest support
2025-09-18 09:26:55 -07:00
Ian McEwen
14ff3eb9c4 Merge branch 'master' into loggingHandler2 2025-09-18 09:26:32 -07:00
Ian McEwen
47e5b04d3b protobufs: v2.7.9 2025-09-18 09:24:33 -07:00
shukari
fe093ac34b fix errors and better linux/windows handling in tests 2025-08-21 15:56:10 +02:00
shukari
dd238dcbe3 ignore pathlib import 2025-08-19 22:03:37 +02:00
shukari
e330afc899 more paths to os.path.join 2025-08-19 13:38:59 +02:00
shukari
a63f3f6e94 use os.path.join for path 2025-08-19 11:23:22 +02:00
shukari
52eb112b95 new Parameter --debuglib for only meshtastic debug, more termios fixes for windows tests
(cherry picked from commit 4fc4d41d3d29998bb7b697bf412be5c1449ea950)
2025-08-19 11:14:38 +02:00
shukari
cbf7b9befe pylint: test_mesh_interface.py line too long 2025-08-09 16:58:48 +02:00
shukari
f3ba660cf4 fix: lint error 2025-08-07 13:30:47 +02:00
shukari
4b143030d3 system independent path separators; pytest for windows fixes 2025-08-07 12:05:43 +02:00
shukari
a79e17a575 fix test_exit_with_exception test, pytest for windows
- test are now runable on windows, some are ignored and a fake termios for the decorators
- test_exit_with_exception with a true exception
2025-08-06 23:01:40 +02:00
shukari
38b163fa89 move logger init to the end of the import block for mesh_interface 2025-08-06 20:00:42 +02:00
shukari
af4947d020 move logger init to the end of the import block 2025-08-06 19:54:08 +02:00
shukari
db1891b651 Added Logging Handler Names 2025-08-06 18:21:32 +02:00
25 changed files with 404 additions and 339 deletions

View File

@@ -129,6 +129,7 @@ NODELESS_WANT_CONFIG_ID = 69420
publishingThread = DeferredExecution("publishing") publishingThread = DeferredExecution("publishing")
logger = logging.getLogger(__name__)
class ResponseHandler(NamedTuple): class ResponseHandler(NamedTuple):
"""A pending response callback, waiting for a response to one of our messages""" """A pending response callback, waiting for a response to one of our messages"""
@@ -160,31 +161,31 @@ def _onTextReceive(iface, asDict):
# #
# Usually btw this problem is caused by apps sending binary data but setting the payload type to # Usually btw this problem is caused by apps sending binary data but setting the payload type to
# text. # text.
logging.debug(f"in _onTextReceive() asDict:{asDict}") logger.debug(f"in _onTextReceive() asDict:{asDict}")
try: try:
asBytes = asDict["decoded"]["payload"] asBytes = asDict["decoded"]["payload"]
asDict["decoded"]["text"] = asBytes.decode("utf-8") asDict["decoded"]["text"] = asBytes.decode("utf-8")
except Exception as ex: except Exception as ex:
logging.error(f"Malformatted utf8 in text message: {ex}") logger.error(f"Malformatted utf8 in text message: {ex}")
_receiveInfoUpdate(iface, asDict) _receiveInfoUpdate(iface, asDict)
def _onPositionReceive(iface, asDict): def _onPositionReceive(iface, asDict):
"""Special auto parsing for received messages""" """Special auto parsing for received messages"""
logging.debug(f"in _onPositionReceive() asDict:{asDict}") logger.debug(f"in _onPositionReceive() asDict:{asDict}")
if "decoded" in asDict: if "decoded" in asDict:
if "position" in asDict["decoded"] and "from" in asDict: if "position" in asDict["decoded"] and "from" in asDict:
p = asDict["decoded"]["position"] p = asDict["decoded"]["position"]
logging.debug(f"p:{p}") logger.debug(f"p:{p}")
p = iface._fixupPosition(p) p = iface._fixupPosition(p)
logging.debug(f"after fixup p:{p}") logger.debug(f"after fixup p:{p}")
# update node DB as needed # update node DB as needed
iface._getOrCreateByNum(asDict["from"])["position"] = p iface._getOrCreateByNum(asDict["from"])["position"] = p
def _onNodeInfoReceive(iface, asDict): def _onNodeInfoReceive(iface, asDict):
"""Special auto parsing for received messages""" """Special auto parsing for received messages"""
logging.debug(f"in _onNodeInfoReceive() asDict:{asDict}") logger.debug(f"in _onNodeInfoReceive() asDict:{asDict}")
if "decoded" in asDict: if "decoded" in asDict:
if "user" in asDict["decoded"] and "from" in asDict: if "user" in asDict["decoded"] and "from" in asDict:
p = asDict["decoded"]["user"] p = asDict["decoded"]["user"]
@@ -198,7 +199,7 @@ def _onNodeInfoReceive(iface, asDict):
def _onTelemetryReceive(iface, asDict): def _onTelemetryReceive(iface, asDict):
"""Automatically update device metrics on received packets""" """Automatically update device metrics on received packets"""
logging.debug(f"in _onTelemetryReceive() asDict:{asDict}") logger.debug(f"in _onTelemetryReceive() asDict:{asDict}")
if "from" not in asDict: if "from" not in asDict:
return return
@@ -222,7 +223,7 @@ def _onTelemetryReceive(iface, asDict):
updateObj = telemetry.get(toUpdate) updateObj = telemetry.get(toUpdate)
newMetrics = node.get(toUpdate, {}) newMetrics = node.get(toUpdate, {})
newMetrics.update(updateObj) newMetrics.update(updateObj)
logging.debug(f"updating {toUpdate} metrics for {asDict['from']} to {newMetrics}") logger.debug(f"updating {toUpdate} metrics for {asDict['from']} to {newMetrics}")
node[toUpdate] = newMetrics node[toUpdate] = newMetrics
def _receiveInfoUpdate(iface, asDict): def _receiveInfoUpdate(iface, asDict):
@@ -234,7 +235,7 @@ def _receiveInfoUpdate(iface, asDict):
def _onAdminReceive(iface, asDict): def _onAdminReceive(iface, asDict):
"""Special auto parsing for received messages""" """Special auto parsing for received messages"""
logging.debug(f"in _onAdminReceive() asDict:{asDict}") logger.debug(f"in _onAdminReceive() asDict:{asDict}")
if "decoded" in asDict and "from" in asDict and "admin" in asDict["decoded"]: if "decoded" in asDict and "from" in asDict and "admin" in asDict["decoded"]:
adminMessage = asDict["decoded"]["admin"]["raw"] adminMessage = asDict["decoded"]["admin"]["raw"]
iface._getOrCreateByNum(asDict["from"])["adminSessionPassKey"] = adminMessage.session_passkey iface._getOrCreateByNum(asDict["from"])["adminSessionPassKey"] = adminMessage.session_passkey

View File

@@ -9,6 +9,7 @@ from typing import List, Optional, Union
from types import ModuleType from types import ModuleType
import argparse import argparse
argcomplete: Union[None, ModuleType] = None argcomplete: Union[None, ModuleType] = None
try: try:
import argcomplete # type: ignore import argcomplete # type: ignore
@@ -62,12 +63,14 @@ except ImportError as e:
from meshtastic.protobuf import channel_pb2, config_pb2, portnums_pb2, mesh_pb2 from meshtastic.protobuf import channel_pb2, config_pb2, portnums_pb2, mesh_pb2
from meshtastic.version import get_active_version from meshtastic.version import get_active_version
logger = logging.getLogger(__name__)
def onReceive(packet, interface) -> None: def onReceive(packet, interface) -> None:
"""Callback invoked when a packet arrives""" """Callback invoked when a packet arrives"""
args = mt_config.args args = mt_config.args
try: try:
d = packet.get("decoded") d = packet.get("decoded")
logging.debug(f"in onReceive() d:{d}") logger.debug(f"in onReceive() d:{d}")
# Exit once we receive a reply # Exit once we receive a reply
if ( if (
@@ -101,7 +104,7 @@ def onConnection(interface, topic=pub.AUTO_TOPIC) -> None: # pylint: disable=W0
def checkChannel(interface: MeshInterface, channelIndex: int) -> bool: 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""" """Given an interface and channel index, return True if that channel is non-disabled on the local node"""
ch = interface.localNode.getChannelByChannelIndex(channelIndex) ch = interface.localNode.getChannelByChannelIndex(channelIndex)
logging.debug(f"ch:{ch}") logger.debug(f"ch:{ch}")
return ch and ch.role != channel_pb2.Channel.Role.DISABLED return ch and ch.role != channel_pb2.Channel.Role.DISABLED
@@ -114,7 +117,7 @@ def getPref(node, comp_name) -> bool:
else: else:
pref_value = meshtastic.util.toStr(pref_value) pref_value = meshtastic.util.toStr(pref_value)
print(f"{str(config_type.name)}.{uni_name}: {str(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)}") logger.debug(f"{str(config_type.name)}.{uni_name}: {str(pref_value)}")
name = splitCompoundName(comp_name) name = splitCompoundName(comp_name)
wholeField = name[0] == name[1] # We want the whole field wholeField = name[0] == name[1] # We want the whole field
@@ -123,8 +126,8 @@ def getPref(node, comp_name) -> bool:
# Note: protobufs has the keys in snake_case, so snake internally # Note: protobufs has the keys in snake_case, so snake internally
snake_name = meshtastic.util.camel_to_snake(name[1]) snake_name = meshtastic.util.camel_to_snake(name[1])
uni_name = camel_name if mt_config.camel_case else snake_name uni_name = camel_name if mt_config.camel_case else snake_name
logging.debug(f"snake_name:{snake_name} camel_name:{camel_name}") logger.debug(f"snake_name:{snake_name} camel_name:{camel_name}")
logging.debug(f"use camel:{mt_config.camel_case}") logger.debug(f"use camel:{mt_config.camel_case}")
# First validate the input # First validate the input
localConfig = node.localConfig localConfig = node.localConfig
@@ -198,8 +201,8 @@ def setPref(config, comp_name, raw_val) -> bool:
snake_name = meshtastic.util.camel_to_snake(name[-1]) snake_name = meshtastic.util.camel_to_snake(name[-1])
camel_name = meshtastic.util.snake_to_camel(name[-1]) camel_name = meshtastic.util.snake_to_camel(name[-1])
uni_name = camel_name if mt_config.camel_case else snake_name uni_name = camel_name if mt_config.camel_case else snake_name
logging.debug(f"snake_name:{snake_name}") logger.debug(f"snake_name:{snake_name}")
logging.debug(f"camel_name:{camel_name}") logger.debug(f"camel_name:{camel_name}")
objDesc = config.DESCRIPTOR objDesc = config.DESCRIPTOR
config_part = config config_part = config
@@ -223,7 +226,7 @@ def setPref(config, comp_name, raw_val) -> bool:
val = meshtastic.util.fromStr(raw_val) val = meshtastic.util.fromStr(raw_val)
else: else:
val = raw_val val = raw_val
logging.debug(f"valStr:{raw_val} val:{val}") logger.debug(f"valStr:{raw_val} val:{val}")
if snake_name == "wifi_psk" and len(str(raw_val)) < 8: if snake_name == "wifi_psk" and len(str(raw_val)) < 8:
print("Warning: network.wifi_psk must be 8 or more characters.") print("Warning: network.wifi_psk must be 8 or more characters.")
@@ -608,7 +611,7 @@ def onConnected(interface):
time.sleep(1) time.sleep(1)
if interface.gotResponse: if interface.gotResponse:
break break
logging.debug(f"end of gpio_rd") logger.debug(f"end of gpio_rd")
if args.gpio_watch: if args.gpio_watch:
bitmask = int(args.gpio_watch, 16) bitmask = int(args.gpio_watch, 16)
@@ -1064,7 +1067,7 @@ def onConnected(interface):
# Even if others said we could close, stay open if the user asked for a tunnel # Even if others said we could close, stay open if the user asked for a tunnel
closeNow = False closeNow = False
if interface.noProto: if interface.noProto:
logging.warning(f"Not starting Tunnel - disabled by noProto") logger.warning(f"Not starting Tunnel - disabled by noProto")
else: else:
if args.tunnel_net: if args.tunnel_net:
tunnel.Tunnel(interface, subnet=args.tunnel_net) tunnel.Tunnel(interface, subnet=args.tunnel_net)
@@ -1255,14 +1258,14 @@ def create_power_meter():
meter = SimPowerSupply() meter = SimPowerSupply()
if meter and v: if meter and v:
logging.info(f"Setting power supply to {v} volts") logger.info(f"Setting power supply to {v} volts")
meter.v = v meter.v = v
meter.powerOn() meter.powerOn()
if args.power_wait: if args.power_wait:
input("Powered on, press enter to continue...") input("Powered on, press enter to continue...")
else: else:
logging.info("Powered-on, waiting for device to boot") logger.info("Powered-on, waiting for device to boot")
time.sleep(5) time.sleep(5)
@@ -1276,6 +1279,10 @@ def common():
format="%(levelname)s file:%(filename)s %(funcName)s line:%(lineno)s %(message)s", format="%(levelname)s file:%(filename)s %(funcName)s line:%(lineno)s %(message)s",
) )
# set all meshtastic loggers to DEBUG
if not (args.debug or args.listen) and args.debuglib:
logging.getLogger('meshtastic').setLevel(logging.DEBUG)
if len(sys.argv) == 1: if len(sys.argv) == 1:
parser.print_help(sys.stderr) parser.print_help(sys.stderr)
meshtastic.util.our_exit("", 1) meshtastic.util.our_exit("", 1)
@@ -1317,7 +1324,7 @@ def common():
args.seriallog = "none" # assume no debug output in this case args.seriallog = "none" # assume no debug output in this case
if args.deprecated is not None: if args.deprecated is not None:
logging.error( logger.error(
"This option has been deprecated, see help below for the correct replacement..." "This option has been deprecated, see help below for the correct replacement..."
) )
parser.print_help(sys.stderr) parser.print_help(sys.stderr)
@@ -1336,10 +1343,10 @@ def common():
logfile = sys.stdout logfile = sys.stdout
elif args.seriallog == "none": elif args.seriallog == "none":
args.seriallog = None args.seriallog = None
logging.debug("Not logging serial output") logger.debug("Not logging serial output")
logfile = None logfile = None
else: else:
logging.info(f"Logging serial output to {args.seriallog}") logger.info(f"Logging serial output to {args.seriallog}")
# Note: using "line buffering" # Note: using "line buffering"
# pylint: disable=R1732 # pylint: disable=R1732
logfile = open(args.seriallog, "w+", buffering=1, encoding="utf8") logfile = open(args.seriallog, "w+", buffering=1, encoding="utf8")
@@ -1347,7 +1354,7 @@ def common():
subscribe() subscribe()
if args.ble_scan: if args.ble_scan:
logging.debug("BLE scan starting") logger.debug("BLE scan starting")
for x in BLEInterface.scan(): for x in BLEInterface.scan():
print(f"Found: name='{x.name}' address='{x.address}'") print(f"Found: name='{x.name}' address='{x.address}'")
meshtastic.util.our_exit("BLE scan finished", 0) meshtastic.util.our_exit("BLE scan finished", 0)
@@ -1438,7 +1445,7 @@ def common():
while True: while True:
time.sleep(1000) time.sleep(1000)
except KeyboardInterrupt: except KeyboardInterrupt:
logging.info("Exiting due to keyboard interrupt") logger.info("Exiting due to keyboard interrupt")
# don't call exit, background threads might be running still # don't call exit, background threads might be running still
# sys.exit(0) # sys.exit(0)
@@ -2045,6 +2052,10 @@ def initParser():
"--debug", help="Show API library debug log messages", action="store_true" "--debug", help="Show API library debug log messages", action="store_true"
) )
group.add_argument(
"--debuglib", help="Show only API library debug log messages", action="store_true"
)
group.add_argument( group.add_argument(
"--test", "--test",
help="Run stress test against all connected Meshtastic devices", help="Run stress test against all connected Meshtastic devices",

View File

@@ -2,6 +2,7 @@
import argparse import argparse
import logging import logging
import os
from typing import cast, List from typing import cast, List
import dash_bootstrap_components as dbc # type: ignore[import-untyped] import dash_bootstrap_components as dbc # type: ignore[import-untyped]
@@ -143,8 +144,8 @@ def create_dash(slog_path: str) -> Dash:
""" """
app = Dash(external_stylesheets=[dbc.themes.BOOTSTRAP]) app = Dash(external_stylesheets=[dbc.themes.BOOTSTRAP])
dpwr = read_pandas(f"{slog_path}/power.feather") dpwr = read_pandas(os.path.join(slog_path, "power.feather"))
dslog = read_pandas(f"{slog_path}/slog.feather") dslog = read_pandas(os.path.join(slog_path, "slog.feather"))
pmon_raises = get_pmon_raises(dslog) pmon_raises = get_pmon_raises(dslog)
@@ -190,7 +191,7 @@ def main():
parser = create_argparser() parser = create_argparser()
args = parser.parse_args() args = parser.parse_args()
if not args.slog: if not args.slog:
args.slog = f"{root_dir()}/latest" args.slog = os.path.join(root_dir(), "latest")
app = create_dash(slog_path=args.slog) app = create_dash(slog_path=args.slog)
port = 8051 port = 8051

View File

@@ -23,6 +23,7 @@ FROMRADIO_UUID = "2c55e69e-4993-11ed-b878-0242ac120002"
FROMNUM_UUID = "ed9da18c-a800-4f66-a670-aa7547e34453" FROMNUM_UUID = "ed9da18c-a800-4f66-a670-aa7547e34453"
LEGACY_LOGRADIO_UUID = "6c6fd238-78fa-436b-aacf-15c5be1ef2e2" LEGACY_LOGRADIO_UUID = "6c6fd238-78fa-436b-aacf-15c5be1ef2e2"
LOGRADIO_UUID = "5a3d6e49-06e6-4423-9944-e9de8cdf9547" LOGRADIO_UUID = "5a3d6e49-06e6-4423-9944-e9de8cdf9547"
logger = logging.getLogger(__name__)
class BLEInterface(MeshInterface): class BLEInterface(MeshInterface):
@@ -44,19 +45,19 @@ class BLEInterface(MeshInterface):
self.should_read = False self.should_read = False
logging.debug("Threads starting") logger.debug("Threads starting")
self._want_receive = True self._want_receive = True
self._receiveThread: Optional[Thread] = Thread( self._receiveThread: Optional[Thread] = Thread(
target=self._receiveFromRadioImpl, name="BLEReceive", daemon=True target=self._receiveFromRadioImpl, name="BLEReceive", daemon=True
) )
self._receiveThread.start() self._receiveThread.start()
logging.debug("Threads running") logger.debug("Threads running")
self.client: Optional[BLEClient] = None self.client: Optional[BLEClient] = None
try: try:
logging.debug(f"BLE connecting to: {address if address else 'any'}") logger.debug(f"BLE connecting to: {address if address else 'any'}")
self.client = self.connect(address) self.client = self.connect(address)
logging.debug("BLE connected") logger.debug("BLE connected")
except BLEInterface.BLEError as e: except BLEInterface.BLEError as e:
self.close() self.close()
raise e raise e
@@ -69,13 +70,13 @@ class BLEInterface(MeshInterface):
if self.client.has_characteristic(LOGRADIO_UUID): if self.client.has_characteristic(LOGRADIO_UUID):
self.client.start_notify(LOGRADIO_UUID, self.log_radio_handler) self.client.start_notify(LOGRADIO_UUID, self.log_radio_handler)
logging.debug("Mesh configure starting") logger.debug("Mesh configure starting")
self._startConfig() self._startConfig()
if not self.noProto: if not self.noProto:
self._waitConnected(timeout=60.0) self._waitConnected(timeout=60.0)
self.waitForConfig() self.waitForConfig()
logging.debug("Register FROMNUM notify callback") logger.debug("Register FROMNUM notify callback")
self.client.start_notify(FROMNUM_UUID, self.from_num_handler) self.client.start_notify(FROMNUM_UUID, self.from_num_handler)
# We MUST run atexit (if we can) because otherwise (at least on linux) the BLE device is not disconnected # We MUST run atexit (if we can) because otherwise (at least on linux) the BLE device is not disconnected
@@ -99,7 +100,7 @@ class BLEInterface(MeshInterface):
Note: this method does not need to be async because it is just setting a bool. Note: this method does not need to be async because it is just setting a bool.
""" """
from_num = struct.unpack("<I", bytes(b))[0] from_num = struct.unpack("<I", bytes(b))[0]
logging.debug(f"FROMNUM notify: {from_num}") logger.debug(f"FROMNUM notify: {from_num}")
self.should_read = True self.should_read = True
async def log_radio_handler(self, _, b): # pylint: disable=C0116 async def log_radio_handler(self, _, b): # pylint: disable=C0116
@@ -114,7 +115,7 @@ class BLEInterface(MeshInterface):
) )
self._handleLogLine(message) self._handleLogLine(message)
except google.protobuf.message.DecodeError: except google.protobuf.message.DecodeError:
logging.warning("Malformed LogRecord received. Skipping.") logger.warning("Malformed LogRecord received. Skipping.")
async def legacy_log_radio_handler(self, _, b): # pylint: disable=C0116 async def legacy_log_radio_handler(self, _, b): # pylint: disable=C0116
log_radio = b.decode("utf-8").replace("\n", "") log_radio = b.decode("utf-8").replace("\n", "")
@@ -124,7 +125,7 @@ class BLEInterface(MeshInterface):
def scan() -> List[BLEDevice]: def scan() -> List[BLEDevice]:
"""Scan for available BLE devices.""" """Scan for available BLE devices."""
with BLEClient() as client: with BLEClient() as client:
logging.info("Scanning for BLE devices (takes 10 seconds)...") logger.info("Scanning for BLE devices (takes 10 seconds)...")
response = client.discover( response = client.discover(
timeout=10, return_adv=True, service_uuids=[SERVICE_UUID] timeout=10, return_adv=True, service_uuids=[SERVICE_UUID]
) )
@@ -186,19 +187,19 @@ class BLEInterface(MeshInterface):
retries: int = 0 retries: int = 0
while self._want_receive: while self._want_receive:
if self.client is None: if self.client is None:
logging.debug(f"BLE client is None, shutting down") logger.debug(f"BLE client is None, shutting down")
self._want_receive = False self._want_receive = False
continue continue
try: try:
b = bytes(self.client.read_gatt_char(FROMRADIO_UUID)) b = bytes(self.client.read_gatt_char(FROMRADIO_UUID))
except BleakDBusError as e: except BleakDBusError as e:
# Device disconnected probably, so end our read loop immediately # Device disconnected probably, so end our read loop immediately
logging.debug(f"Device disconnected, shutting down {e}") logger.debug(f"Device disconnected, shutting down {e}")
self._want_receive = False self._want_receive = False
except BleakError as e: except BleakError as e:
# We were definitely disconnected # We were definitely disconnected
if "Not connected" in str(e): if "Not connected" in str(e):
logging.debug(f"Device disconnected, shutting down {e}") logger.debug(f"Device disconnected, shutting down {e}")
self._want_receive = False self._want_receive = False
else: else:
raise BLEInterface.BLEError("Error reading BLE") from e raise BLEInterface.BLEError("Error reading BLE") from e
@@ -208,7 +209,7 @@ class BLEInterface(MeshInterface):
retries += 1 retries += 1
continue continue
break break
logging.debug(f"FROMRADIO read: {b.hex()}") logger.debug(f"FROMRADIO read: {b.hex()}")
self._handleFromRadio(b) self._handleFromRadio(b)
else: else:
time.sleep(0.01) time.sleep(0.01)
@@ -216,7 +217,7 @@ class BLEInterface(MeshInterface):
def _sendToRadioImpl(self, toRadio) -> None: def _sendToRadioImpl(self, toRadio) -> None:
b: bytes = toRadio.SerializeToString() b: bytes = toRadio.SerializeToString()
if b and self.client: # we silently ignore writes while we are shutting down if b and self.client: # we silently ignore writes while we are shutting down
logging.debug(f"TORADIO write: {b.hex()}") logger.debug(f"TORADIO write: {b.hex()}")
try: try:
self.client.write_gatt_char( self.client.write_gatt_char(
TORADIO_UUID, b, response=True TORADIO_UUID, b, response=True
@@ -234,7 +235,7 @@ class BLEInterface(MeshInterface):
try: try:
MeshInterface.close(self) MeshInterface.close(self)
except Exception as e: except Exception as e:
logging.error(f"Error closing mesh interface: {e}") logger.error(f"Error closing mesh interface: {e}")
if self._want_receive: if self._want_receive:
self._want_receive = False # Tell the thread we want it to stop self._want_receive = False # Tell the thread we want it to stop
@@ -263,7 +264,7 @@ class BLEClient:
self._eventThread.start() self._eventThread.start()
if not address: if not address:
logging.debug("No address provided - only discover method will work.") logger.debug("No address provided - only discover method will work.")
return return
self.bleak_client = BleakClient(address, **kwargs) self.bleak_client = BleakClient(address, **kwargs)

View File

@@ -17,6 +17,7 @@ from decimal import Decimal
from typing import Any, Callable, Dict, List, Optional, Union from typing import Any, Callable, Dict, List, Optional, Union
import google.protobuf.json_format import google.protobuf.json_format
try: try:
import print_color # type: ignore[import-untyped] import print_color # type: ignore[import-untyped]
except ImportError as e: except ImportError as e:
@@ -46,6 +47,7 @@ from meshtastic.util import (
stripnl, stripnl,
) )
logger = logging.getLogger(__name__)
def _timeago(delta_secs: int) -> str: def _timeago(delta_secs: int) -> str:
"""Convert a number of seconds in the past into a short, friendly string """Convert a number of seconds in the past into a short, friendly string
@@ -149,11 +151,11 @@ class MeshInterface: # pylint: disable=R0902
def __exit__(self, exc_type, exc_value, trace): def __exit__(self, exc_type, exc_value, trace):
if exc_type is not None and exc_value is not None: if exc_type is not None and exc_value is not None:
logging.error( logger.error(
f"An exception of type {exc_type} with value {exc_value} has occurred" f"An exception of type {exc_type} with value {exc_value} has occurred"
) )
if trace is not None: if trace is not None:
logging.error(f"Traceback: {trace}") logger.error(f"Traceback:\n{''.join(traceback.format_tb(trace))}")
self.close() self.close()
@staticmethod @staticmethod
@@ -281,7 +283,7 @@ class MeshInterface: # pylint: disable=R0902
def getNestedValue(node_dict: Dict[str, Any], key_path: str) -> Any: def getNestedValue(node_dict: Dict[str, Any], key_path: str) -> Any:
if key_path.index(".") < 0: if key_path.index(".") < 0:
logging.debug("getNestedValue was called without a nested path.") logger.debug("getNestedValue was called without a nested path.")
return None return None
keys = key_path.split(".") keys = key_path.split(".")
value: Optional[Union[str, dict]] = node_dict value: Optional[Union[str, dict]] = node_dict
@@ -304,7 +306,7 @@ class MeshInterface: # pylint: disable=R0902
rows: List[Dict[str, Any]] = [] rows: List[Dict[str, Any]] = []
if self.nodesByNum: if self.nodesByNum:
logging.debug(f"self.nodes:{self.nodes}") logger.debug(f"self.nodes:{self.nodes}")
for node in self.nodesByNum.values(): for node in self.nodesByNum.values():
if not includeSelf and node["num"] == self.localNode.nodeNum: if not includeSelf and node["num"] == self.localNode.nodeNum:
continue continue
@@ -383,7 +385,7 @@ class MeshInterface: # pylint: disable=R0902
n = meshtastic.node.Node(self, nodeId, timeout=timeout) n = meshtastic.node.Node(self, nodeId, timeout=timeout)
# Only request device settings and channel info when necessary # Only request device settings and channel info when necessary
if requestChannels: if requestChannels:
logging.debug("About to requestChannels") logger.debug("About to requestChannels")
n.requestChannels() n.requestChannels()
retries_left = requestChannelAttempts retries_left = requestChannelAttempts
last_index: int = 0 last_index: int = 0
@@ -538,11 +540,11 @@ class MeshInterface: # pylint: disable=R0902
""" """
if getattr(data, "SerializeToString", None): if getattr(data, "SerializeToString", None):
logging.debug(f"Serializing protobuf as data: {stripnl(data)}") logger.debug(f"Serializing protobuf as data: {stripnl(data)}")
data = data.SerializeToString() data = data.SerializeToString()
logging.debug(f"len(data): {len(data)}") logger.debug(f"len(data): {len(data)}")
logging.debug( logger.debug(
f"mesh_pb2.Constants.DATA_PAYLOAD_LEN: {mesh_pb2.Constants.DATA_PAYLOAD_LEN}" f"mesh_pb2.Constants.DATA_PAYLOAD_LEN: {mesh_pb2.Constants.DATA_PAYLOAD_LEN}"
) )
if len(data) > mesh_pb2.Constants.DATA_PAYLOAD_LEN: if len(data) > mesh_pb2.Constants.DATA_PAYLOAD_LEN:
@@ -565,7 +567,7 @@ class MeshInterface: # pylint: disable=R0902
meshPacket.priority = priority meshPacket.priority = priority
if onResponse is not None: if onResponse is not None:
logging.debug(f"Setting a response handler for requestId {meshPacket.id}") logger.debug(f"Setting a response handler for requestId {meshPacket.id}")
self._addResponseHandler(meshPacket.id, onResponse, ackPermitted=onResponseAckPermitted) self._addResponseHandler(meshPacket.id, onResponse, ackPermitted=onResponseAckPermitted)
p = self._sendPacket(meshPacket, destinationId, wantAck=wantAck, hopLimit=hopLimit, pkiEncrypted=pkiEncrypted, publicKey=publicKey) p = self._sendPacket(meshPacket, destinationId, wantAck=wantAck, hopLimit=hopLimit, pkiEncrypted=pkiEncrypted, publicKey=publicKey)
return p return p
@@ -592,15 +594,15 @@ class MeshInterface: # pylint: disable=R0902
p = mesh_pb2.Position() p = mesh_pb2.Position()
if latitude != 0.0: if latitude != 0.0:
p.latitude_i = int(latitude / 1e-7) p.latitude_i = int(latitude / 1e-7)
logging.debug(f"p.latitude_i:{p.latitude_i}") logger.debug(f"p.latitude_i:{p.latitude_i}")
if longitude != 0.0: if longitude != 0.0:
p.longitude_i = int(longitude / 1e-7) p.longitude_i = int(longitude / 1e-7)
logging.debug(f"p.longitude_i:{p.longitude_i}") logger.debug(f"p.longitude_i:{p.longitude_i}")
if altitude != 0: if altitude != 0:
p.altitude = int(altitude) p.altitude = int(altitude)
logging.debug(f"p.altitude:{p.altitude}") logger.debug(f"p.altitude:{p.altitude}")
if wantResponse: if wantResponse:
onResponse = self.onResponsePosition onResponse = self.onResponsePosition
@@ -851,15 +853,15 @@ class MeshInterface: # pylint: disable=R0902
# same algorithm as https://github.com/meshtastic/js/blob/715e35d2374276a43ffa93c628e3710875d43907/src/meshDevice.ts#L791 # same algorithm as https://github.com/meshtastic/js/blob/715e35d2374276a43ffa93c628e3710875d43907/src/meshDevice.ts#L791
seed = secrets.randbits(32) seed = secrets.randbits(32)
w.id = math.floor(seed * math.pow(2, -32) * 1e9) w.id = math.floor(seed * math.pow(2, -32) * 1e9)
logging.debug(f"w.id:{w.id}") logger.debug(f"w.id:{w.id}")
else: else:
w.id = waypoint_id w.id = waypoint_id
if latitude != 0.0: if latitude != 0.0:
w.latitude_i = int(latitude * 1e7) w.latitude_i = int(latitude * 1e7)
logging.debug(f"w.latitude_i:{w.latitude_i}") logger.debug(f"w.latitude_i:{w.latitude_i}")
if longitude != 0.0: if longitude != 0.0:
w.longitude_i = int(longitude * 1e7) w.longitude_i = int(longitude * 1e7)
logging.debug(f"w.longitude_i:{w.longitude_i}") logger.debug(f"w.longitude_i:{w.longitude_i}")
if wantResponse: if wantResponse:
onResponse = self.onResponseWaypoint onResponse = self.onResponseWaypoint
@@ -974,7 +976,7 @@ class MeshInterface: # pylint: disable=R0902
else: else:
nodeNum = node["num"] nodeNum = node["num"]
else: else:
logging.warning("Warning: There were no self.nodes.") logger.warning("Warning: There were no self.nodes.")
meshPacket.to = nodeNum meshPacket.to = nodeNum
meshPacket.want_ack = wantAck meshPacket.want_ack = wantAck
@@ -998,11 +1000,11 @@ class MeshInterface: # pylint: disable=R0902
toRadio.packet.CopyFrom(meshPacket) toRadio.packet.CopyFrom(meshPacket)
if self.noProto: if self.noProto:
logging.warning( logger.warning(
"Not sending packet because protocol use is disabled by noProto" "Not sending packet because protocol use is disabled by noProto"
) )
else: else:
logging.debug(f"Sending packet: {stripnl(meshPacket)}") logger.debug(f"Sending packet: {stripnl(meshPacket)}")
self._sendToRadio(toRadio) self._sendToRadio(toRadio)
return meshPacket return meshPacket
@@ -1053,7 +1055,7 @@ class MeshInterface: # pylint: disable=R0902
"""Get info about my node.""" """Get info about my node."""
if self.myInfo is None or self.nodesByNum is None: if self.myInfo is None or self.nodesByNum is None:
return None return None
logging.debug(f"self.nodesByNum:{self.nodesByNum}") logger.debug(f"self.nodesByNum:{self.nodesByNum}")
return self.nodesByNum.get(self.myInfo.my_node_num) return self.nodesByNum.get(self.myInfo.my_node_num)
def getMyUser(self): def getMyUser(self):
@@ -1143,7 +1145,7 @@ class MeshInterface: # pylint: disable=R0902
def callback(): def callback():
self.heartbeatTimer = None self.heartbeatTimer = None
interval = 300 interval = 300
logging.debug(f"Sending heartbeat, interval {interval} seconds") logger.debug(f"Sending heartbeat, interval {interval} seconds")
self.heartbeatTimer = threading.Timer(interval, callback) self.heartbeatTimer = threading.Timer(interval, callback)
self.heartbeatTimer.start() self.heartbeatTimer.start()
self.sendHeartbeat() self.sendHeartbeat()
@@ -1201,11 +1203,11 @@ class MeshInterface: # pylint: disable=R0902
def _sendToRadio(self, toRadio: mesh_pb2.ToRadio) -> None: def _sendToRadio(self, toRadio: mesh_pb2.ToRadio) -> None:
"""Send a ToRadio protobuf to the device""" """Send a ToRadio protobuf to the device"""
if self.noProto: if self.noProto:
logging.warning( logger.warning(
"Not sending packet because protocol use is disabled by noProto" "Not sending packet because protocol use is disabled by noProto"
) )
else: else:
# logging.debug(f"Sending toRadio: {stripnl(toRadio)}") # logger.debug(f"Sending toRadio: {stripnl(toRadio)}")
if not toRadio.HasField("packet"): if not toRadio.HasField("packet"):
# not a meshpacket -- send immediately, give queue a chance, # not a meshpacket -- send immediately, give queue a chance,
@@ -1218,38 +1220,38 @@ class MeshInterface: # pylint: disable=R0902
resentQueue = collections.OrderedDict() resentQueue = collections.OrderedDict()
while self.queue: while self.queue:
# logging.warn("queue: " + " ".join(f'{k:08x}' for k in self.queue)) # logger.warn("queue: " + " ".join(f'{k:08x}' for k in self.queue))
while not self._queueHasFreeSpace(): while not self._queueHasFreeSpace():
logging.debug("Waiting for free space in TX Queue") logger.debug("Waiting for free space in TX Queue")
time.sleep(0.5) time.sleep(0.5)
try: try:
toResend = self.queue.popitem(last=False) toResend = self.queue.popitem(last=False)
except KeyError: except KeyError:
break break
packetId, packet = toResend packetId, packet = toResend
# logging.warn(f"packet: {packetId:08x} {packet}") # logger.warn(f"packet: {packetId:08x} {packet}")
resentQueue[packetId] = packet resentQueue[packetId] = packet
if packet is False: if packet is False:
continue continue
self._queueClaim() self._queueClaim()
if packet != toRadio: if packet != toRadio:
logging.debug(f"Resending packet ID {packetId:08x} {packet}") logger.debug(f"Resending packet ID {packetId:08x} {packet}")
self._sendToRadioImpl(packet) self._sendToRadioImpl(packet)
# logging.warn("resentQueue: " + " ".join(f'{k:08x}' for k in resentQueue)) # logger.warn("resentQueue: " + " ".join(f'{k:08x}' for k in resentQueue))
for packetId, packet in resentQueue.items(): for packetId, packet in resentQueue.items():
if ( if (
self.queue.pop(packetId, False) is False self.queue.pop(packetId, False) is False
): # Packet got acked under us ): # Packet got acked under us
logging.debug(f"packet {packetId:08x} got acked under us") logger.debug(f"packet {packetId:08x} got acked under us")
continue continue
if packet: if packet:
self.queue[packetId] = packet self.queue[packetId] = packet
# logging.warn("queue + resentQueue: " + " ".join(f'{k:08x}' for k in self.queue)) # logger.warn("queue + resentQueue: " + " ".join(f'{k:08x}' for k in self.queue))
def _sendToRadioImpl(self, toRadio: mesh_pb2.ToRadio) -> None: def _sendToRadioImpl(self, toRadio: mesh_pb2.ToRadio) -> None:
"""Send a ToRadio protobuf to the device""" """Send a ToRadio protobuf to the device"""
logging.error(f"Subclass must provide toradio: {toRadio}") logger.error(f"Subclass must provide toradio: {toRadio}")
def _handleConfigComplete(self) -> None: def _handleConfigComplete(self) -> None:
""" """
@@ -1265,22 +1267,22 @@ class MeshInterface: # pylint: disable=R0902
def _handleQueueStatusFromRadio(self, queueStatus) -> None: def _handleQueueStatusFromRadio(self, queueStatus) -> None:
self.queueStatus = queueStatus self.queueStatus = queueStatus
logging.debug( logger.debug(
f"TX QUEUE free {queueStatus.free} of {queueStatus.maxlen}, res = {queueStatus.res}, id = {queueStatus.mesh_packet_id:08x} " f"TX QUEUE free {queueStatus.free} of {queueStatus.maxlen}, res = {queueStatus.res}, id = {queueStatus.mesh_packet_id:08x} "
) )
if queueStatus.res: if queueStatus.res:
return return
# logging.warn("queue: " + " ".join(f'{k:08x}' for k in self.queue)) # logger.warn("queue: " + " ".join(f'{k:08x}' for k in self.queue))
justQueued = self.queue.pop(queueStatus.mesh_packet_id, None) justQueued = self.queue.pop(queueStatus.mesh_packet_id, None)
if justQueued is None and queueStatus.mesh_packet_id != 0: if justQueued is None and queueStatus.mesh_packet_id != 0:
self.queue[queueStatus.mesh_packet_id] = False self.queue[queueStatus.mesh_packet_id] = False
logging.debug( logger.debug(
f"Reply for unexpected packet ID {queueStatus.mesh_packet_id:08x}" f"Reply for unexpected packet ID {queueStatus.mesh_packet_id:08x}"
) )
# logging.warn("queue: " + " ".join(f'{k:08x}' for k in self.queue)) # logger.warn("queue: " + " ".join(f'{k:08x}' for k in self.queue))
def _handleFromRadio(self, fromRadioBytes): def _handleFromRadio(self, fromRadioBytes):
""" """
@@ -1288,30 +1290,30 @@ class MeshInterface: # pylint: disable=R0902
Called by subclasses.""" Called by subclasses."""
fromRadio = mesh_pb2.FromRadio() fromRadio = mesh_pb2.FromRadio()
logging.debug( logger.debug(
f"in mesh_interface.py _handleFromRadio() fromRadioBytes: {fromRadioBytes}" f"in mesh_interface.py _handleFromRadio() fromRadioBytes: {fromRadioBytes}"
) )
try: try:
fromRadio.ParseFromString(fromRadioBytes) fromRadio.ParseFromString(fromRadioBytes)
except Exception as ex: except Exception as ex:
logging.error( logger.error(
f"Error while parsing FromRadio bytes:{fromRadioBytes} {ex}" f"Error while parsing FromRadio bytes:{fromRadioBytes} {ex}"
) )
traceback.print_exc() traceback.print_exc()
raise ex raise ex
asDict = google.protobuf.json_format.MessageToDict(fromRadio) asDict = google.protobuf.json_format.MessageToDict(fromRadio)
logging.debug(f"Received from radio: {fromRadio}") logger.debug(f"Received from radio: {fromRadio}")
if fromRadio.HasField("my_info"): if fromRadio.HasField("my_info"):
self.myInfo = fromRadio.my_info self.myInfo = fromRadio.my_info
self.localNode.nodeNum = self.myInfo.my_node_num self.localNode.nodeNum = self.myInfo.my_node_num
logging.debug(f"Received myinfo: {stripnl(fromRadio.my_info)}") logger.debug(f"Received myinfo: {stripnl(fromRadio.my_info)}")
elif fromRadio.HasField("metadata"): elif fromRadio.HasField("metadata"):
self.metadata = fromRadio.metadata self.metadata = fromRadio.metadata
logging.debug(f"Received device metadata: {stripnl(fromRadio.metadata)}") logger.debug(f"Received device metadata: {stripnl(fromRadio.metadata)}")
elif fromRadio.HasField("node_info"): elif fromRadio.HasField("node_info"):
logging.debug(f"Received nodeinfo: {asDict['nodeInfo']}") logger.debug(f"Received nodeinfo: {asDict['nodeInfo']}")
node = self._getOrCreateByNum(asDict["nodeInfo"]["num"]) node = self._getOrCreateByNum(asDict["nodeInfo"]["num"])
node.update(asDict["nodeInfo"]) node.update(asDict["nodeInfo"])
@@ -1319,7 +1321,7 @@ class MeshInterface: # pylint: disable=R0902
newpos = self._fixupPosition(node["position"]) newpos = self._fixupPosition(node["position"])
node["position"] = newpos node["position"] = newpos
except: except:
logging.debug("Node without position") logger.debug("Node without position")
# no longer necessary since we're mutating directly in nodesByNum via _getOrCreateByNum # no longer necessary since we're mutating directly in nodesByNum via _getOrCreateByNum
# self.nodesByNum[node["num"]] = node # self.nodesByNum[node["num"]] = node
@@ -1334,7 +1336,7 @@ class MeshInterface: # pylint: disable=R0902
elif fromRadio.config_complete_id == self.configId: elif fromRadio.config_complete_id == self.configId:
# we ignore the config_complete_id, it is unneeded for our # we ignore the config_complete_id, it is unneeded for our
# stream API fromRadio.config_complete_id # stream API fromRadio.config_complete_id
logging.debug(f"Config complete ID {self.configId}") logger.debug(f"Config complete ID {self.configId}")
self._handleConfigComplete() self._handleConfigComplete()
elif fromRadio.HasField("channel"): elif fromRadio.HasField("channel"):
self._handleChannel(fromRadio.channel) self._handleChannel(fromRadio.channel)
@@ -1449,7 +1451,7 @@ class MeshInterface: # pylint: disable=R0902
) )
else: else:
logging.debug("Unexpected FromRadio payload") logger.debug("Unexpected FromRadio payload")
def _fixupPosition(self, position: Dict) -> Dict: def _fixupPosition(self, position: Dict) -> Dict:
"""Convert integer lat/lon into floats """Convert integer lat/lon into floats
@@ -1483,7 +1485,7 @@ class MeshInterface: # pylint: disable=R0902
try: try:
return self.nodesByNum[num]["user"]["id"] # type: ignore[index] return self.nodesByNum[num]["user"]["id"] # type: ignore[index]
except: except:
logging.debug(f"Node {num} not found for fromId") logger.debug(f"Node {num} not found for fromId")
return None return None
def _getOrCreateByNum(self, nodeNum): def _getOrCreateByNum(self, nodeNum):
@@ -1539,7 +1541,7 @@ class MeshInterface: # pylint: disable=R0902
# from might be missing if the nodenum was zero. # from might be missing if the nodenum was zero.
if not hack and "from" not in asDict: if not hack and "from" not in asDict:
asDict["from"] = 0 asDict["from"] = 0
logging.error( logger.error(
f"Device returned a packet we sent, ignoring: {stripnl(asDict)}" f"Device returned a packet we sent, ignoring: {stripnl(asDict)}"
) )
print( print(
@@ -1553,11 +1555,11 @@ class MeshInterface: # pylint: disable=R0902
try: try:
asDict["fromId"] = self._nodeNumToId(asDict["from"], False) asDict["fromId"] = self._nodeNumToId(asDict["from"], False)
except Exception as ex: except Exception as ex:
logging.warning(f"Not populating fromId {ex}") logger.warning(f"Not populating fromId {ex}")
try: try:
asDict["toId"] = self._nodeNumToId(asDict["to"]) asDict["toId"] = self._nodeNumToId(asDict["to"])
except Exception as ex: except Exception as ex:
logging.warning(f"Not populating toId {ex}") logger.warning(f"Not populating toId {ex}")
# We could provide our objects as DotMaps - which work with . notation or as dictionaries # We could provide our objects as DotMaps - which work with . notation or as dictionaries
# asObj = DotMap(asDict) # asObj = DotMap(asDict)
@@ -1577,7 +1579,7 @@ class MeshInterface: # pylint: disable=R0902
# it to prevent confusion # it to prevent confusion
if "portnum" not in decoded: if "portnum" not in decoded:
decoded["portnum"] = portnum decoded["portnum"] = portnum
logging.warning(f"portnum was not in decoded. Setting to:{portnum}") logger.warning(f"portnum was not in decoded. Setting to:{portnum}")
else: else:
portnum = decoded["portnum"] portnum = decoded["portnum"]
@@ -1609,7 +1611,7 @@ class MeshInterface: # pylint: disable=R0902
# Is this message in response to a request, if so, look for a handler # Is this message in response to a request, if so, look for a handler
requestId = decoded.get("requestId") requestId = decoded.get("requestId")
if requestId is not None: if requestId is not None:
logging.debug(f"Got a response for requestId {requestId}") logger.debug(f"Got a response for requestId {requestId}")
# We ignore ACK packets unless the callback is named `onAckNak` # We ignore ACK packets unless the callback is named `onAckNak`
# or the handler is set as ackPermitted, but send NAKs and # or the handler is set as ackPermitted, but send NAKs and
# other, data-containing responses to the handlers # other, data-containing responses to the handlers
@@ -1626,12 +1628,12 @@ class MeshInterface: # pylint: disable=R0902
or handler.ackPermitted or handler.ackPermitted
): ):
handler = self.responseHandlers.pop(requestId, None) handler = self.responseHandlers.pop(requestId, None)
logging.debug( logger.debug(
f"Calling response handler for requestId {requestId}" f"Calling response handler for requestId {requestId}"
) )
handler.callback(asDict) handler.callback(asDict)
logging.debug(f"Publishing {topic}: packet={stripnl(asDict)} ") logger.debug(f"Publishing {topic}: packet={stripnl(asDict)} ")
publishingThread.queueWork( publishingThread.queueWork(
lambda: pub.sendMessage(topic, packet=asDict, interface=self) lambda: pub.sendMessage(topic, packet=asDict, interface=self)
) )

View File

@@ -18,6 +18,7 @@ from meshtastic.util import (
message_to_json, message_to_json,
) )
logger = logging.getLogger(__name__)
class Node: class Node:
"""A model of a (local or remote) node in the mesh """A model of a (local or remote) node in the mesh
@@ -65,7 +66,7 @@ class Node:
"""Show human readable description of our channels.""" """Show human readable description of our channels."""
print("Channels:") print("Channels:")
if self.channels: if self.channels:
logging.debug(f"self.channels:{self.channels}") logger.debug(f"self.channels:{self.channels}")
for c in self.channels: for c in self.channels:
cStr = message_to_json(c.settings) cStr = message_to_json(c.settings)
# don't show disabled channels # don't show disabled channels
@@ -98,7 +99,7 @@ class Node:
def requestChannels(self, startingIndex: int = 0): def requestChannels(self, startingIndex: int = 0):
"""Send regular MeshPackets to ask channels.""" """Send regular MeshPackets to ask channels."""
logging.debug(f"requestChannels for nodeNum:{self.nodeNum}") logger.debug(f"requestChannels for nodeNum:{self.nodeNum}")
# only initialize if we're starting out fresh # only initialize if we're starting out fresh
if startingIndex == 0: if startingIndex == 0:
self.channels = None self.channels = None
@@ -107,7 +108,7 @@ class Node:
def onResponseRequestSettings(self, p): def onResponseRequestSettings(self, p):
"""Handle the response packets for requesting settings _requestSettings()""" """Handle the response packets for requesting settings _requestSettings()"""
logging.debug(f"onResponseRequestSetting() p:{p}") logger.debug(f"onResponseRequestSetting() p:{p}")
config_values = None config_values = None
if "routing" in p["decoded"]: if "routing" in p["decoded"]:
if p["decoded"]["routing"]["errorReason"] != "NONE": if p["decoded"]["routing"]["errorReason"] != "NONE":
@@ -234,7 +235,7 @@ class Node:
else: else:
our_exit(f"Error: No valid config with name {config_name}") our_exit(f"Error: No valid config with name {config_name}")
logging.debug(f"Wrote: {config_name}") logger.debug(f"Wrote: {config_name}")
if self == self.iface.localNode: if self == self.iface.localNode:
onResponse = None onResponse = None
else: else:
@@ -247,7 +248,7 @@ class Node:
p = admin_pb2.AdminMessage() p = admin_pb2.AdminMessage()
p.set_channel.CopyFrom(self.channels[channelIndex]) p.set_channel.CopyFrom(self.channels[channelIndex])
self._sendAdmin(p, adminIndex=adminIndex) self._sendAdmin(p, adminIndex=adminIndex)
logging.debug(f"Wrote channel {channelIndex}") logger.debug(f"Wrote channel {channelIndex}")
def getChannelByChannelIndex(self, channelIndex): def getChannelByChannelIndex(self, channelIndex):
"""Get channel by channelIndex """Get channel by channelIndex
@@ -310,7 +311,7 @@ class Node:
def setOwner(self, long_name: Optional[str]=None, short_name: Optional[str]=None, is_licensed: bool=False, is_unmessagable: Optional[bool]=None): def setOwner(self, long_name: Optional[str]=None, short_name: Optional[str]=None, is_licensed: bool=False, is_unmessagable: Optional[bool]=None):
"""Set device owner name""" """Set device owner name"""
logging.debug(f"in setOwner nodeNum:{self.nodeNum}") logger.debug(f"in setOwner nodeNum:{self.nodeNum}")
self.ensureSessionKey() self.ensureSessionKey()
p = admin_pb2.AdminMessage() p = admin_pb2.AdminMessage()
@@ -335,10 +336,10 @@ class Node:
p.set_owner.is_unmessagable = is_unmessagable p.set_owner.is_unmessagable = is_unmessagable
# Note: These debug lines are used in unit tests # Note: These debug lines are used in unit tests
logging.debug(f"p.set_owner.long_name:{p.set_owner.long_name}:") logger.debug(f"p.set_owner.long_name:{p.set_owner.long_name}:")
logging.debug(f"p.set_owner.short_name:{p.set_owner.short_name}:") logger.debug(f"p.set_owner.short_name:{p.set_owner.short_name}:")
logging.debug(f"p.set_owner.is_licensed:{p.set_owner.is_licensed}") logger.debug(f"p.set_owner.is_licensed:{p.set_owner.is_licensed}")
logging.debug(f"p.set_owner.is_unmessagable:{p.set_owner.is_unmessagable}:") logger.debug(f"p.set_owner.is_unmessagable:{p.set_owner.is_unmessagable}:")
# If sending to a remote node, wait for ACK/NAK # If sending to a remote node, wait for ACK/NAK
if self == self.iface.localNode: if self == self.iface.localNode:
onResponse = None onResponse = None
@@ -421,7 +422,7 @@ class Node:
ch.index = i ch.index = i
ch.settings.CopyFrom(chs) ch.settings.CopyFrom(chs)
self.channels[ch.index] = ch self.channels[ch.index] = ch
logging.debug(f"Channel i:{i} ch:{ch}") logger.debug(f"Channel i:{i} ch:{ch}")
self.writeChannel(ch.index) self.writeChannel(ch.index)
i = i + 1 i = i + 1
@@ -432,7 +433,7 @@ class Node:
def onResponseRequestRingtone(self, p): def onResponseRequestRingtone(self, p):
"""Handle the response packet for requesting ringtone part 1""" """Handle the response packet for requesting ringtone part 1"""
logging.debug(f"onResponseRequestRingtone() p:{p}") logger.debug(f"onResponseRequestRingtone() p:{p}")
errorFound = False errorFound = False
if "routing" in p["decoded"]: if "routing" in p["decoded"]:
if p["decoded"]["routing"]["errorReason"] != "NONE": if p["decoded"]["routing"]["errorReason"] != "NONE":
@@ -445,12 +446,12 @@ class Node:
self.ringtonePart = p["decoded"]["admin"][ self.ringtonePart = p["decoded"]["admin"][
"raw" "raw"
].get_ringtone_response ].get_ringtone_response
logging.debug(f"self.ringtonePart:{self.ringtonePart}") logger.debug(f"self.ringtonePart:{self.ringtonePart}")
self.gotResponse = True self.gotResponse = True
def get_ringtone(self): def get_ringtone(self):
"""Get the ringtone. Concatenate all pieces together and return a single string.""" """Get the ringtone. Concatenate all pieces together and return a single string."""
logging.debug(f"in get_ringtone()") logger.debug(f"in get_ringtone()")
if not self.module_available(mesh_pb2.EXTNOTIF_CONFIG): if not self.module_available(mesh_pb2.EXTNOTIF_CONFIG):
logging.warning("External Notification module not present (excluded by firmware)") logging.warning("External Notification module not present (excluded by firmware)")
return None return None
@@ -465,13 +466,13 @@ class Node:
while self.gotResponse is False: while self.gotResponse is False:
time.sleep(0.1) time.sleep(0.1)
logging.debug(f"self.ringtone:{self.ringtone}") logger.debug(f"self.ringtone:{self.ringtone}")
self.ringtone = "" self.ringtone = ""
if self.ringtonePart: if self.ringtonePart:
self.ringtone += self.ringtonePart self.ringtone += self.ringtonePart
logging.debug(f"ringtone:{self.ringtone}") logger.debug(f"ringtone:{self.ringtone}")
return self.ringtone return self.ringtone
def set_ringtone(self, ringtone): def set_ringtone(self, ringtone):
@@ -498,7 +499,7 @@ class Node:
if i == 0: if i == 0:
p.set_ringtone_message = chunk p.set_ringtone_message = chunk
logging.debug(f"Setting ringtone '{chunk}' part {i+1}") logger.debug(f"Setting ringtone '{chunk}' part {i+1}")
# If sending to a remote node, wait for ACK/NAK # If sending to a remote node, wait for ACK/NAK
if self == self.iface.localNode: if self == self.iface.localNode:
onResponse = None onResponse = None
@@ -508,7 +509,7 @@ class Node:
def onResponseRequestCannedMessagePluginMessageMessages(self, p): def onResponseRequestCannedMessagePluginMessageMessages(self, p):
"""Handle the response packet for requesting canned message plugin message part 1""" """Handle the response packet for requesting canned message plugin message part 1"""
logging.debug(f"onResponseRequestCannedMessagePluginMessageMessages() p:{p}") logger.debug(f"onResponseRequestCannedMessagePluginMessageMessages() p:{p}")
errorFound = False errorFound = False
if "routing" in p["decoded"]: if "routing" in p["decoded"]:
if p["decoded"]["routing"]["errorReason"] != "NONE": if p["decoded"]["routing"]["errorReason"] != "NONE":
@@ -521,14 +522,14 @@ class Node:
self.cannedPluginMessageMessages = p["decoded"]["admin"][ self.cannedPluginMessageMessages = p["decoded"]["admin"][
"raw" "raw"
].get_canned_message_module_messages_response ].get_canned_message_module_messages_response
logging.debug( logger.debug(
f"self.cannedPluginMessageMessages:{self.cannedPluginMessageMessages}" f"self.cannedPluginMessageMessages:{self.cannedPluginMessageMessages}"
) )
self.gotResponse = True self.gotResponse = True
def get_canned_message(self): def get_canned_message(self):
"""Get the canned message string. Concatenate all pieces together and return a single string.""" """Get the canned message string. Concatenate all pieces together and return a single string."""
logging.debug(f"in get_canned_message()") logger.debug(f"in get_canned_message()")
if not self.module_available(mesh_pb2.CANNEDMSG_CONFIG): if not self.module_available(mesh_pb2.CANNEDMSG_CONFIG):
logging.warning("Canned Message module not present (excluded by firmware)") logging.warning("Canned Message module not present (excluded by firmware)")
return None return None
@@ -544,7 +545,7 @@ class Node:
while self.gotResponse is False: while self.gotResponse is False:
time.sleep(0.1) time.sleep(0.1)
logging.debug( logger.debug(
f"self.cannedPluginMessageMessages:{self.cannedPluginMessageMessages}" f"self.cannedPluginMessageMessages:{self.cannedPluginMessageMessages}"
) )
@@ -552,7 +553,7 @@ class Node:
if self.cannedPluginMessageMessages: if self.cannedPluginMessageMessages:
self.cannedPluginMessage += self.cannedPluginMessageMessages self.cannedPluginMessage += self.cannedPluginMessageMessages
logging.debug(f"canned_plugin_message:{self.cannedPluginMessage}") logger.debug(f"canned_plugin_message:{self.cannedPluginMessage}")
return self.cannedPluginMessage return self.cannedPluginMessage
def set_canned_message(self, message): def set_canned_message(self, message):
@@ -579,7 +580,7 @@ class Node:
if i == 0: if i == 0:
p.set_canned_message_module_messages = chunk p.set_canned_message_module_messages = chunk
logging.debug(f"Setting canned message '{chunk}' part {i+1}") logger.debug(f"Setting canned message '{chunk}' part {i+1}")
# If sending to a remote node, wait for ACK/NAK # If sending to a remote node, wait for ACK/NAK
if self == self.iface.localNode: if self == self.iface.localNode:
onResponse = None onResponse = None
@@ -593,7 +594,7 @@ class Node:
self.ensureSessionKey() self.ensureSessionKey()
p = admin_pb2.AdminMessage() p = admin_pb2.AdminMessage()
p.exit_simulator = True p.exit_simulator = True
logging.debug("in exitSimulator()") logger.debug("in exitSimulator()")
return self._sendAdmin(p) return self._sendAdmin(p)
@@ -602,7 +603,7 @@ class Node:
self.ensureSessionKey() self.ensureSessionKey()
p = admin_pb2.AdminMessage() p = admin_pb2.AdminMessage()
p.reboot_seconds = secs p.reboot_seconds = secs
logging.info(f"Telling node to reboot in {secs} seconds") logger.info(f"Telling node to reboot in {secs} seconds")
# If sending to a remote node, wait for ACK/NAK # If sending to a remote node, wait for ACK/NAK
if self == self.iface.localNode: if self == self.iface.localNode:
@@ -616,7 +617,7 @@ class Node:
self.ensureSessionKey() self.ensureSessionKey()
p = admin_pb2.AdminMessage() p = admin_pb2.AdminMessage()
p.begin_edit_settings = True p.begin_edit_settings = True
logging.info(f"Telling open a transaction to edit settings") logger.info(f"Telling open a transaction to edit settings")
# If sending to a remote node, wait for ACK/NAK # If sending to a remote node, wait for ACK/NAK
if self == self.iface.localNode: if self == self.iface.localNode:
@@ -630,7 +631,7 @@ class Node:
self.ensureSessionKey() self.ensureSessionKey()
p = admin_pb2.AdminMessage() p = admin_pb2.AdminMessage()
p.commit_edit_settings = True p.commit_edit_settings = True
logging.info(f"Telling node to commit open transaction for editing settings") logger.info(f"Telling node to commit open transaction for editing settings")
# If sending to a remote node, wait for ACK/NAK # If sending to a remote node, wait for ACK/NAK
if self == self.iface.localNode: if self == self.iface.localNode:
@@ -644,7 +645,7 @@ class Node:
self.ensureSessionKey() self.ensureSessionKey()
p = admin_pb2.AdminMessage() p = admin_pb2.AdminMessage()
p.reboot_ota_seconds = secs p.reboot_ota_seconds = secs
logging.info(f"Telling node to reboot to OTA in {secs} seconds") logger.info(f"Telling node to reboot to OTA in {secs} seconds")
# If sending to a remote node, wait for ACK/NAK # If sending to a remote node, wait for ACK/NAK
if self == self.iface.localNode: if self == self.iface.localNode:
@@ -658,7 +659,7 @@ class Node:
self.ensureSessionKey() self.ensureSessionKey()
p = admin_pb2.AdminMessage() p = admin_pb2.AdminMessage()
p.enter_dfu_mode_request = True p.enter_dfu_mode_request = True
logging.info(f"Telling node to enable DFU mode") logger.info(f"Telling node to enable DFU mode")
# If sending to a remote node, wait for ACK/NAK # If sending to a remote node, wait for ACK/NAK
if self == self.iface.localNode: if self == self.iface.localNode:
@@ -672,7 +673,7 @@ class Node:
self.ensureSessionKey() self.ensureSessionKey()
p = admin_pb2.AdminMessage() p = admin_pb2.AdminMessage()
p.shutdown_seconds = secs p.shutdown_seconds = secs
logging.info(f"Telling node to shutdown in {secs} seconds") logger.info(f"Telling node to shutdown in {secs} seconds")
# If sending to a remote node, wait for ACK/NAK # If sending to a remote node, wait for ACK/NAK
if self == self.iface.localNode: if self == self.iface.localNode:
@@ -685,7 +686,7 @@ class Node:
"""Get the node's metadata.""" """Get the node's metadata."""
p = admin_pb2.AdminMessage() p = admin_pb2.AdminMessage()
p.get_device_metadata_request = True p.get_device_metadata_request = True
logging.info(f"Requesting device metadata") logger.info(f"Requesting device metadata")
self._sendAdmin( self._sendAdmin(
p, wantResponse=True, onResponse=self.onRequestGetMetadata p, wantResponse=True, onResponse=self.onRequestGetMetadata
@@ -698,10 +699,10 @@ class Node:
p = admin_pb2.AdminMessage() p = admin_pb2.AdminMessage()
if full: if full:
p.factory_reset_device = True p.factory_reset_device = True
logging.info(f"Telling node to factory reset (full device reset)") logger.info(f"Telling node to factory reset (full device reset)")
else: else:
p.factory_reset_config = True p.factory_reset_config = True
logging.info(f"Telling node to factory reset (config reset)") logger.info(f"Telling node to factory reset (config reset)")
# If sending to a remote node, wait for ACK/NAK # If sending to a remote node, wait for ACK/NAK
if self == self.iface.localNode: if self == self.iface.localNode:
@@ -805,7 +806,7 @@ class Node:
self.ensureSessionKey() self.ensureSessionKey()
p = admin_pb2.AdminMessage() p = admin_pb2.AdminMessage()
p.nodedb_reset = True p.nodedb_reset = True
logging.info(f"Telling node to reset the NodeDB") logger.info(f"Telling node to reset the NodeDB")
# If sending to a remote node, wait for ACK/NAK # If sending to a remote node, wait for ACK/NAK
if self == self.iface.localNode: if self == self.iface.localNode:
@@ -846,7 +847,7 @@ class Node:
self.ensureSessionKey() self.ensureSessionKey()
p = admin_pb2.AdminMessage() p = admin_pb2.AdminMessage()
p.remove_fixed_position = True p.remove_fixed_position = True
logging.info(f"Telling node to remove fixed position") logger.info(f"Telling node to remove fixed position")
if self == self.iface.localNode: if self == self.iface.localNode:
onResponse = None onResponse = None
@@ -861,7 +862,7 @@ class Node:
timeSec = int(time.time()) timeSec = int(time.time())
p = admin_pb2.AdminMessage() p = admin_pb2.AdminMessage()
p.set_time_only = timeSec p.set_time_only = timeSec
logging.info(f"Setting node time to {timeSec}") logger.info(f"Setting node time to {timeSec}")
if self == self.iface.localNode: if self == self.iface.localNode:
onResponse = None onResponse = None
@@ -893,7 +894,7 @@ class Node:
def onRequestGetMetadata(self, p): def onRequestGetMetadata(self, p):
"""Handle the response packet for requesting device metadata getMetadata()""" """Handle the response packet for requesting device metadata getMetadata()"""
logging.debug(f"onRequestGetMetadata() p:{p}") logger.debug(f"onRequestGetMetadata() p:{p}")
if "routing" in p["decoded"]: if "routing" in p["decoded"]:
if p["decoded"]["routing"]["errorReason"] != "NONE": if p["decoded"]["routing"]["errorReason"] != "NONE":
@@ -905,30 +906,30 @@ class Node:
portnums_pb2.PortNum.ROUTING_APP portnums_pb2.PortNum.ROUTING_APP
): ):
if p["decoded"]["routing"]["errorReason"] != "NONE": if p["decoded"]["routing"]["errorReason"] != "NONE":
logging.warning( logger.warning(
f'Metadata request failed, error reason: {p["decoded"]["routing"]["errorReason"]}' f'Metadata request failed, error reason: {p["decoded"]["routing"]["errorReason"]}'
) )
self._timeout.expireTime = time.time() # Do not wait any longer self._timeout.expireTime = time.time() # Do not wait any longer
return # Don't try to parse this routing message return # Don't try to parse this routing message
logging.debug(f"Retrying metadata request.") logger.debug(f"Retrying metadata request.")
self.getMetadata() self.getMetadata()
return return
c = p["decoded"]["admin"]["raw"].get_device_metadata_response c = p["decoded"]["admin"]["raw"].get_device_metadata_response
self._timeout.reset() # We made forward progress self._timeout.reset() # We made forward progress
logging.debug(f"Received metadata {stripnl(c)}") logger.debug(f"Received metadata {stripnl(c)}")
print(f"\nfirmware_version: {c.firmware_version}") print(f"\nfirmware_version: {c.firmware_version}")
print(f"device_state_version: {c.device_state_version}") print(f"device_state_version: {c.device_state_version}")
def onResponseRequestChannel(self, p): def onResponseRequestChannel(self, p):
"""Handle the response packet for requesting a channel _requestChannel()""" """Handle the response packet for requesting a channel _requestChannel()"""
logging.debug(f"onResponseRequestChannel() p:{p}") logger.debug(f"onResponseRequestChannel() p:{p}")
if p["decoded"]["portnum"] == portnums_pb2.PortNum.Name( if p["decoded"]["portnum"] == portnums_pb2.PortNum.Name(
portnums_pb2.PortNum.ROUTING_APP portnums_pb2.PortNum.ROUTING_APP
): ):
if p["decoded"]["routing"]["errorReason"] != "NONE": if p["decoded"]["routing"]["errorReason"] != "NONE":
logging.warning( logger.warning(
f'Channel request failed, error reason: {p["decoded"]["routing"]["errorReason"]}' f'Channel request failed, error reason: {p["decoded"]["routing"]["errorReason"]}'
) )
self._timeout.expireTime = time.time() # Do not wait any longer self._timeout.expireTime = time.time() # Do not wait any longer
@@ -936,18 +937,18 @@ class Node:
lastTried = 0 lastTried = 0
if len(self.partialChannels) > 0: if len(self.partialChannels) > 0:
lastTried = self.partialChannels[-1].index lastTried = self.partialChannels[-1].index
logging.debug(f"Retrying previous channel request.") logger.debug(f"Retrying previous channel request.")
self._requestChannel(lastTried) self._requestChannel(lastTried)
return return
c = p["decoded"]["admin"]["raw"].get_channel_response c = p["decoded"]["admin"]["raw"].get_channel_response
self.partialChannels.append(c) self.partialChannels.append(c)
self._timeout.reset() # We made forward progress self._timeout.reset() # We made forward progress
logging.debug(f"Received channel {stripnl(c)}") logger.debug(f"Received channel {stripnl(c)}")
index = c.index index = c.index
if index >= 8 - 1: if index >= 8 - 1:
logging.debug("Finished downloading channels") logger.debug("Finished downloading channels")
self.channels = self.partialChannels self.channels = self.partialChannels
self._fixupChannels() self._fixupChannels()
@@ -982,11 +983,11 @@ class Node:
print( print(
f"Requesting channel {channelNum} info from remote node (this could take a while)" f"Requesting channel {channelNum} info from remote node (this could take a while)"
) )
logging.debug( logger.debug(
f"Requesting channel {channelNum} info from remote node (this could take a while)" f"Requesting channel {channelNum} info from remote node (this could take a while)"
) )
else: else:
logging.debug(f"Requesting channel {channelNum}") logger.debug(f"Requesting channel {channelNum}")
return self._sendAdmin( return self._sendAdmin(
p, wantResponse=True, onResponse=self.onResponseRequestChannel p, wantResponse=True, onResponse=self.onResponseRequestChannel
@@ -1003,7 +1004,7 @@ class Node:
"""Send an admin message to the specified node (or the local node if destNodeNum is zero)""" """Send an admin message to the specified node (or the local node if destNodeNum is zero)"""
if self.noProto: if self.noProto:
logging.warning( logger.warning(
f"Not sending packet because protocol use is disabled by noProto" f"Not sending packet because protocol use is disabled by noProto"
) )
else: else:
@@ -1011,7 +1012,7 @@ class Node:
adminIndex == 0 adminIndex == 0
): # unless a special channel index was used, we want to use the admin index ): # unless a special channel index was used, we want to use the admin index
adminIndex = self.iface.localNode._getAdminChannelIndex() adminIndex = self.iface.localNode._getAdminChannelIndex()
logging.debug(f"adminIndex:{adminIndex}") logger.debug(f"adminIndex:{adminIndex}")
if isinstance(self.nodeNum, int): if isinstance(self.nodeNum, int):
nodeid = self.nodeNum nodeid = self.nodeNum
else: # assume string starting with ! else: # assume string starting with !
@@ -1032,7 +1033,7 @@ class Node:
def ensureSessionKey(self): def ensureSessionKey(self):
"""If our entry in iface.nodesByNum doesn't already have an adminSessionPassKey, make a request to get one""" """If our entry in iface.nodesByNum doesn't already have an adminSessionPassKey, make a request to get one"""
if self.noProto: if self.noProto:
logging.warning( logger.warning(
f"Not ensuring session key, because protocol use is disabled by noProto" f"Not ensuring session key, because protocol use is disabled by noProto"
) )
else: else:

View File

File diff suppressed because one or more lines are too long

View File

@@ -116,6 +116,13 @@ class Config(google.protobuf.message.Message):
but should not be given priority over other routers in order to avoid unnecessaraily but should not be given priority over other routers in order to avoid unnecessaraily
consuming hops. consuming hops.
""" """
CLIENT_BASE: Config.DeviceConfig._Role.ValueType # 12
"""
Description: Treats packets from or to favorited nodes as ROUTER, and all other packets as CLIENT.
Technical Details: Used for stronger attic/roof nodes to distribute messages more widely
from weaker, indoor, or less-well-positioned nodes. Recommended for users with multiple nodes
where one CLIENT_BASE acts as a more powerful base station, such as an attic/roof node.
"""
class Role(_Role, metaclass=_RoleEnumTypeWrapper): class Role(_Role, metaclass=_RoleEnumTypeWrapper):
""" """
@@ -200,6 +207,13 @@ class Config(google.protobuf.message.Message):
but should not be given priority over other routers in order to avoid unnecessaraily but should not be given priority over other routers in order to avoid unnecessaraily
consuming hops. consuming hops.
""" """
CLIENT_BASE: Config.DeviceConfig.Role.ValueType # 12
"""
Description: Treats packets from or to favorited nodes as ROUTER, and all other packets as CLIENT.
Technical Details: Used for stronger attic/roof nodes to distribute messages more widely
from weaker, indoor, or less-well-positioned nodes. Recommended for users with multiple nodes
where one CLIENT_BASE acts as a more powerful base station, such as an attic/roof node.
"""
class _RebroadcastMode: class _RebroadcastMode:
ValueType = typing.NewType("ValueType", builtins.int) ValueType = typing.NewType("ValueType", builtins.int)

View File

@@ -13,7 +13,7 @@ _sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n#meshtastic/protobuf/device_ui.proto\x12\x13meshtastic.protobuf\"\xda\x04\n\x0e\x44\x65viceUIConfig\x12\x0f\n\x07version\x18\x01 \x01(\r\x12\x19\n\x11screen_brightness\x18\x02 \x01(\r\x12\x16\n\x0escreen_timeout\x18\x03 \x01(\r\x12\x13\n\x0bscreen_lock\x18\x04 \x01(\x08\x12\x15\n\rsettings_lock\x18\x05 \x01(\x08\x12\x10\n\x08pin_code\x18\x06 \x01(\r\x12)\n\x05theme\x18\x07 \x01(\x0e\x32\x1a.meshtastic.protobuf.Theme\x12\x15\n\ralert_enabled\x18\x08 \x01(\x08\x12\x16\n\x0e\x62\x61nner_enabled\x18\t \x01(\x08\x12\x14\n\x0cring_tone_id\x18\n \x01(\r\x12/\n\x08language\x18\x0b \x01(\x0e\x32\x1d.meshtastic.protobuf.Language\x12\x34\n\x0bnode_filter\x18\x0c \x01(\x0b\x32\x1f.meshtastic.protobuf.NodeFilter\x12:\n\x0enode_highlight\x18\r \x01(\x0b\x32\".meshtastic.protobuf.NodeHighlight\x12\x18\n\x10\x63\x61libration_data\x18\x0e \x01(\x0c\x12*\n\x08map_data\x18\x0f \x01(\x0b\x32\x18.meshtastic.protobuf.Map\x12\x36\n\x0c\x63ompass_mode\x18\x10 \x01(\x0e\x32 .meshtastic.protobuf.CompassMode\x12\x18\n\x10screen_rgb_color\x18\x11 \x01(\r\x12\x1b\n\x13is_clockface_analog\x18\x12 \x01(\x08\"\xa7\x01\n\nNodeFilter\x12\x16\n\x0eunknown_switch\x18\x01 \x01(\x08\x12\x16\n\x0eoffline_switch\x18\x02 \x01(\x08\x12\x19\n\x11public_key_switch\x18\x03 \x01(\x08\x12\x11\n\thops_away\x18\x04 \x01(\x05\x12\x17\n\x0fposition_switch\x18\x05 \x01(\x08\x12\x11\n\tnode_name\x18\x06 \x01(\t\x12\x0f\n\x07\x63hannel\x18\x07 \x01(\x05\"~\n\rNodeHighlight\x12\x13\n\x0b\x63hat_switch\x18\x01 \x01(\x08\x12\x17\n\x0fposition_switch\x18\x02 \x01(\x08\x12\x18\n\x10telemetry_switch\x18\x03 \x01(\x08\x12\x12\n\niaq_switch\x18\x04 \x01(\x08\x12\x11\n\tnode_name\x18\x05 \x01(\t\"=\n\x08GeoPoint\x12\x0c\n\x04zoom\x18\x01 \x01(\x05\x12\x10\n\x08latitude\x18\x02 \x01(\x05\x12\x11\n\tlongitude\x18\x03 \x01(\x05\"U\n\x03Map\x12+\n\x04home\x18\x01 \x01(\x0b\x32\x1d.meshtastic.protobuf.GeoPoint\x12\r\n\x05style\x18\x02 \x01(\t\x12\x12\n\nfollow_gps\x18\x03 \x01(\x08*>\n\x0b\x43ompassMode\x12\x0b\n\x07\x44YNAMIC\x10\x00\x12\x0e\n\nFIXED_RING\x10\x01\x12\x12\n\x0e\x46REEZE_HEADING\x10\x02*%\n\x05Theme\x12\x08\n\x04\x44\x41RK\x10\x00\x12\t\n\x05LIGHT\x10\x01\x12\x07\n\x03RED\x10\x02*\xa9\x02\n\x08Language\x12\x0b\n\x07\x45NGLISH\x10\x00\x12\n\n\x06\x46RENCH\x10\x01\x12\n\n\x06GERMAN\x10\x02\x12\x0b\n\x07ITALIAN\x10\x03\x12\x0e\n\nPORTUGUESE\x10\x04\x12\x0b\n\x07SPANISH\x10\x05\x12\x0b\n\x07SWEDISH\x10\x06\x12\x0b\n\x07\x46INNISH\x10\x07\x12\n\n\x06POLISH\x10\x08\x12\x0b\n\x07TURKISH\x10\t\x12\x0b\n\x07SERBIAN\x10\n\x12\x0b\n\x07RUSSIAN\x10\x0b\x12\t\n\x05\x44UTCH\x10\x0c\x12\t\n\x05GREEK\x10\r\x12\r\n\tNORWEGIAN\x10\x0e\x12\r\n\tSLOVENIAN\x10\x0f\x12\r\n\tUKRAINIAN\x10\x10\x12\r\n\tBULGARIAN\x10\x11\x12\x16\n\x12SIMPLIFIED_CHINESE\x10\x1e\x12\x17\n\x13TRADITIONAL_CHINESE\x10\x1f\x42\x63\n\x13\x63om.geeksville.meshB\x0e\x44\x65viceUIProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3') DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n#meshtastic/protobuf/device_ui.proto\x12\x13meshtastic.protobuf\"\xda\x04\n\x0e\x44\x65viceUIConfig\x12\x0f\n\x07version\x18\x01 \x01(\r\x12\x19\n\x11screen_brightness\x18\x02 \x01(\r\x12\x16\n\x0escreen_timeout\x18\x03 \x01(\r\x12\x13\n\x0bscreen_lock\x18\x04 \x01(\x08\x12\x15\n\rsettings_lock\x18\x05 \x01(\x08\x12\x10\n\x08pin_code\x18\x06 \x01(\r\x12)\n\x05theme\x18\x07 \x01(\x0e\x32\x1a.meshtastic.protobuf.Theme\x12\x15\n\ralert_enabled\x18\x08 \x01(\x08\x12\x16\n\x0e\x62\x61nner_enabled\x18\t \x01(\x08\x12\x14\n\x0cring_tone_id\x18\n \x01(\r\x12/\n\x08language\x18\x0b \x01(\x0e\x32\x1d.meshtastic.protobuf.Language\x12\x34\n\x0bnode_filter\x18\x0c \x01(\x0b\x32\x1f.meshtastic.protobuf.NodeFilter\x12:\n\x0enode_highlight\x18\r \x01(\x0b\x32\".meshtastic.protobuf.NodeHighlight\x12\x18\n\x10\x63\x61libration_data\x18\x0e \x01(\x0c\x12*\n\x08map_data\x18\x0f \x01(\x0b\x32\x18.meshtastic.protobuf.Map\x12\x36\n\x0c\x63ompass_mode\x18\x10 \x01(\x0e\x32 .meshtastic.protobuf.CompassMode\x12\x18\n\x10screen_rgb_color\x18\x11 \x01(\r\x12\x1b\n\x13is_clockface_analog\x18\x12 \x01(\x08\"\xa7\x01\n\nNodeFilter\x12\x16\n\x0eunknown_switch\x18\x01 \x01(\x08\x12\x16\n\x0eoffline_switch\x18\x02 \x01(\x08\x12\x19\n\x11public_key_switch\x18\x03 \x01(\x08\x12\x11\n\thops_away\x18\x04 \x01(\x05\x12\x17\n\x0fposition_switch\x18\x05 \x01(\x08\x12\x11\n\tnode_name\x18\x06 \x01(\t\x12\x0f\n\x07\x63hannel\x18\x07 \x01(\x05\"~\n\rNodeHighlight\x12\x13\n\x0b\x63hat_switch\x18\x01 \x01(\x08\x12\x17\n\x0fposition_switch\x18\x02 \x01(\x08\x12\x18\n\x10telemetry_switch\x18\x03 \x01(\x08\x12\x12\n\niaq_switch\x18\x04 \x01(\x08\x12\x11\n\tnode_name\x18\x05 \x01(\t\"=\n\x08GeoPoint\x12\x0c\n\x04zoom\x18\x01 \x01(\x05\x12\x10\n\x08latitude\x18\x02 \x01(\x05\x12\x11\n\tlongitude\x18\x03 \x01(\x05\"U\n\x03Map\x12+\n\x04home\x18\x01 \x01(\x0b\x32\x1d.meshtastic.protobuf.GeoPoint\x12\r\n\x05style\x18\x02 \x01(\t\x12\x12\n\nfollow_gps\x18\x03 \x01(\x08*>\n\x0b\x43ompassMode\x12\x0b\n\x07\x44YNAMIC\x10\x00\x12\x0e\n\nFIXED_RING\x10\x01\x12\x12\n\x0e\x46REEZE_HEADING\x10\x02*%\n\x05Theme\x12\x08\n\x04\x44\x41RK\x10\x00\x12\t\n\x05LIGHT\x10\x01\x12\x07\n\x03RED\x10\x02*\xb4\x02\n\x08Language\x12\x0b\n\x07\x45NGLISH\x10\x00\x12\n\n\x06\x46RENCH\x10\x01\x12\n\n\x06GERMAN\x10\x02\x12\x0b\n\x07ITALIAN\x10\x03\x12\x0e\n\nPORTUGUESE\x10\x04\x12\x0b\n\x07SPANISH\x10\x05\x12\x0b\n\x07SWEDISH\x10\x06\x12\x0b\n\x07\x46INNISH\x10\x07\x12\n\n\x06POLISH\x10\x08\x12\x0b\n\x07TURKISH\x10\t\x12\x0b\n\x07SERBIAN\x10\n\x12\x0b\n\x07RUSSIAN\x10\x0b\x12\t\n\x05\x44UTCH\x10\x0c\x12\t\n\x05GREEK\x10\r\x12\r\n\tNORWEGIAN\x10\x0e\x12\r\n\tSLOVENIAN\x10\x0f\x12\r\n\tUKRAINIAN\x10\x10\x12\r\n\tBULGARIAN\x10\x11\x12\t\n\x05\x43ZECH\x10\x12\x12\x16\n\x12SIMPLIFIED_CHINESE\x10\x1e\x12\x17\n\x13TRADITIONAL_CHINESE\x10\x1f\x42\x63\n\x13\x63om.geeksville.meshB\x0e\x44\x65viceUIProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
_globals = globals() _globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
@@ -26,7 +26,7 @@ if _descriptor._USE_C_DESCRIPTORS == False:
_globals['_THEME']._serialized_start=1177 _globals['_THEME']._serialized_start=1177
_globals['_THEME']._serialized_end=1214 _globals['_THEME']._serialized_end=1214
_globals['_LANGUAGE']._serialized_start=1217 _globals['_LANGUAGE']._serialized_start=1217
_globals['_LANGUAGE']._serialized_end=1514 _globals['_LANGUAGE']._serialized_end=1525
_globals['_DEVICEUICONFIG']._serialized_start=61 _globals['_DEVICEUICONFIG']._serialized_start=61
_globals['_DEVICEUICONFIG']._serialized_end=663 _globals['_DEVICEUICONFIG']._serialized_end=663
_globals['_NODEFILTER']._serialized_start=666 _globals['_NODEFILTER']._serialized_start=666

View File

@@ -165,6 +165,10 @@ class _LanguageEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumT
""" """
Bulgarian Bulgarian
""" """
CZECH: _Language.ValueType # 18
"""
Czech
"""
SIMPLIFIED_CHINESE: _Language.ValueType # 30 SIMPLIFIED_CHINESE: _Language.ValueType # 30
""" """
Simplified Chinese (experimental) Simplified Chinese (experimental)
@@ -251,6 +255,10 @@ BULGARIAN: Language.ValueType # 17
""" """
Bulgarian Bulgarian
""" """
CZECH: Language.ValueType # 18
"""
Czech
"""
SIMPLIFIED_CHINESE: Language.ValueType # 30 SIMPLIFIED_CHINESE: Language.ValueType # 30
""" """
Simplified Chinese (experimental) Simplified Chinese (experimental)

View File

File diff suppressed because one or more lines are too long

View File

@@ -490,6 +490,10 @@ class _HardwareModelEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._
""" """
Lilygo T-Echo Lite Lilygo T-Echo Lite
""" """
HELTEC_V4: _HardwareModel.ValueType # 110
"""
New Heltec LoRA32 with ESP32-S3 CPU
"""
PRIVATE_HW: _HardwareModel.ValueType # 255 PRIVATE_HW: _HardwareModel.ValueType # 255
""" """
------------------------------------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------------------------------
@@ -963,6 +967,10 @@ T_ECHO_LITE: HardwareModel.ValueType # 109
""" """
Lilygo T-Echo Lite Lilygo T-Echo Lite
""" """
HELTEC_V4: HardwareModel.ValueType # 110
"""
New Heltec LoRA32 with ESP32-S3 CPU
"""
PRIVATE_HW: HardwareModel.ValueType # 255 PRIVATE_HW: HardwareModel.ValueType # 255
""" """
------------------------------------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------------------------------

View File

@@ -7,10 +7,11 @@ from pubsub import pub # type: ignore[import-untyped]
from meshtastic.protobuf import portnums_pb2, remote_hardware_pb2 from meshtastic.protobuf import portnums_pb2, remote_hardware_pb2
from meshtastic.util import our_exit from meshtastic.util import our_exit
logger = logging.getLogger(__name__)
def onGPIOreceive(packet, interface) -> None: def onGPIOreceive(packet, interface) -> None:
"""Callback for received GPIO responses""" """Callback for received GPIO responses"""
logging.debug(f"packet:{packet} interface:{interface}") logger.debug(f"packet:{packet} interface:{interface}")
gpioValue = 0 gpioValue = 0
hw = packet["decoded"]["remotehw"] hw = packet["decoded"]["remotehw"]
if "gpioValue" in hw: if "gpioValue" in hw:
@@ -76,7 +77,7 @@ class RemoteHardwareClient:
Write the specified vals bits to the device GPIOs. Only bits in mask that Write the specified vals bits to the device GPIOs. Only bits in mask that
are 1 will be changed are 1 will be changed
""" """
logging.debug(f"writeGPIOs nodeid:{nodeid} mask:{mask} vals:{vals}") logger.debug(f"writeGPIOs nodeid:{nodeid} mask:{mask} vals:{vals}")
r = remote_hardware_pb2.HardwareMessage() r = remote_hardware_pb2.HardwareMessage()
r.type = remote_hardware_pb2.HardwareMessage.Type.WRITE_GPIOS r.type = remote_hardware_pb2.HardwareMessage.Type.WRITE_GPIOS
r.gpio_mask = mask r.gpio_mask = mask
@@ -85,7 +86,7 @@ class RemoteHardwareClient:
def readGPIOs(self, nodeid, mask, onResponse=None): def readGPIOs(self, nodeid, mask, onResponse=None):
"""Read the specified bits from GPIO inputs on the device""" """Read the specified bits from GPIO inputs on the device"""
logging.debug(f"readGPIOs nodeid:{nodeid} mask:{mask}") logger.debug(f"readGPIOs nodeid:{nodeid} mask:{mask}")
r = remote_hardware_pb2.HardwareMessage() r = remote_hardware_pb2.HardwareMessage()
r.type = remote_hardware_pb2.HardwareMessage.Type.READ_GPIOS r.type = remote_hardware_pb2.HardwareMessage.Type.READ_GPIOS
r.gpio_mask = mask r.gpio_mask = mask
@@ -93,7 +94,7 @@ class RemoteHardwareClient:
def watchGPIOs(self, nodeid, mask): def watchGPIOs(self, nodeid, mask):
"""Watch the specified bits from GPIO inputs on the device for changes""" """Watch the specified bits from GPIO inputs on the device for changes"""
logging.debug(f"watchGPIOs nodeid:{nodeid} mask:{mask}") logger.debug(f"watchGPIOs nodeid:{nodeid} mask:{mask}")
r = remote_hardware_pb2.HardwareMessage() r = remote_hardware_pb2.HardwareMessage()
r.type = remote_hardware_pb2.HardwareMessage.Type.WATCH_GPIOS r.type = remote_hardware_pb2.HardwareMessage.Type.WATCH_GPIOS
r.gpio_mask = mask r.gpio_mask = mask

View File

@@ -2,8 +2,9 @@
""" """
# pylint: disable=R0917 # pylint: disable=R0917
import logging import logging
import platform import sys
import time import time
from io import TextIOWrapper
from typing import List, Optional from typing import List, Optional
@@ -12,9 +13,7 @@ import serial # type: ignore[import-untyped]
import meshtastic.util import meshtastic.util
from meshtastic.stream_interface import StreamInterface from meshtastic.stream_interface import StreamInterface
if platform.system() != "Windows": logger = logging.getLogger(__name__)
import termios
class SerialInterface(StreamInterface): class SerialInterface(StreamInterface):
"""Interface class for meshtastic devices over a serial link""" """Interface class for meshtastic devices over a serial link"""
@@ -33,7 +32,7 @@ class SerialInterface(StreamInterface):
if self.devPath is None: if self.devPath is None:
ports: List[str] = meshtastic.util.findPorts(True) ports: List[str] = meshtastic.util.findPorts(True)
logging.debug(f"ports:{ports}") logger.debug(f"ports:{ports}")
if len(ports) == 0: if len(ports) == 0:
print("No Serial Meshtastic device detected, attempting TCP connection on localhost.") print("No Serial Meshtastic device detected, attempting TCP connection on localhost.")
return return
@@ -44,16 +43,11 @@ class SerialInterface(StreamInterface):
else: else:
self.devPath = ports[0] self.devPath = ports[0]
logging.debug(f"Connecting to {self.devPath}") logger.debug(f"Connecting to {self.devPath}")
# first we need to set the HUPCL so the device will not reboot based on RTS and/or DTR if sys.platform != "win32":
# see https://github.com/pyserial/pyserial/issues/124
if platform.system() != "Windows":
with open(self.devPath, encoding="utf8") as f: with open(self.devPath, encoding="utf8") as f:
attrs = termios.tcgetattr(f) self._set_hupcl_with_termios(f)
attrs[2] = attrs[2] & ~termios.HUPCL
termios.tcsetattr(f, termios.TCSAFLUSH, attrs)
f.close()
time.sleep(0.1) time.sleep(0.1)
self.stream = serial.Serial( self.stream = serial.Serial(
@@ -66,6 +60,18 @@ class SerialInterface(StreamInterface):
self, debugOut=debugOut, noProto=noProto, connectNow=connectNow, noNodes=noNodes self, debugOut=debugOut, noProto=noProto, connectNow=connectNow, noNodes=noNodes
) )
def _set_hupcl_with_termios(self, f: TextIOWrapper):
"""first we need to set the HUPCL so the device will not reboot based on RTS and/or DTR
see https://github.com/pyserial/pyserial/issues/124
"""
if sys.platform == "win32":
return
import termios # pylint: disable=C0415,E0401
attrs = termios.tcgetattr(f)
attrs[2] = attrs[2] & ~termios.HUPCL
termios.tcsetattr(f, termios.TCSAFLUSH, attrs)
def __repr__(self): def __repr__(self):
rep = f"SerialInterface(devPath={self.devPath!r}" rep = f"SerialInterface(devPath={self.devPath!r}"
if hasattr(self, 'debugOut') and self.debugOut is not None: if hasattr(self, 'debugOut') and self.debugOut is not None:
@@ -84,5 +90,5 @@ class SerialInterface(StreamInterface):
time.sleep(0.1) time.sleep(0.1)
self.stream.flush() self.stream.flush()
time.sleep(0.1) time.sleep(0.1)
logging.debug("Closing Serial stream") logger.debug("Closing Serial stream")
StreamInterface.close(self) StreamInterface.close(self)

View File

@@ -10,6 +10,7 @@ import time
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime from datetime import datetime
from functools import reduce from functools import reduce
from pathlib import Path
from typing import Optional, List, Tuple from typing import Optional, List, Tuple
import parse # type: ignore[import-untyped] import parse # type: ignore[import-untyped]
@@ -22,6 +23,7 @@ from meshtastic.powermon import PowerMeter
from .arrow import FeatherWriter from .arrow import FeatherWriter
logger = logging.getLogger(__name__)
def root_dir() -> str: def root_dir() -> str:
"""Return the root directory for slog files.""" """Return the root directory for slog files."""
@@ -29,9 +31,9 @@ def root_dir() -> str:
app_name = "meshtastic" app_name = "meshtastic"
app_author = "meshtastic" app_author = "meshtastic"
app_dir = platformdirs.user_data_dir(app_name, app_author) app_dir = platformdirs.user_data_dir(app_name, app_author)
dir_name = f"{app_dir}/slogs" dir_name = Path(app_dir, "slogs")
os.makedirs(dir_name, exist_ok=True) dir_name.mkdir(exist_ok=True, parents=True)
return dir_name return str(dir_name)
@dataclass(init=False) @dataclass(init=False)
@@ -144,7 +146,7 @@ class StructuredLogger:
self.power_logger = power_logger self.power_logger = power_logger
# Setup the arrow writer (and its schema) # Setup the arrow writer (and its schema)
self.writer = FeatherWriter(f"{dir_path}/slog") self.writer = FeatherWriter(os.path.join(dir_path, "slog"))
all_fields = reduce( all_fields = reduce(
(lambda x, y: x + y), map(lambda x: x.fields, log_defs.values()) (lambda x, y: x + y), map(lambda x: x.fields, log_defs.values())
) )
@@ -164,7 +166,7 @@ class StructuredLogger:
self.raw_file: Optional[ self.raw_file: Optional[
io.TextIOWrapper io.TextIOWrapper
] = open( # pylint: disable=consider-using-with ] = open( # pylint: disable=consider-using-with
f"{dir_path}/raw.txt", "w", encoding="utf8" os.path.join(dir_path, "raw.txt"), "w", encoding="utf8"
) )
# We need a closure here because the subscription API is very strict about exact arg matching # We need a closure here because the subscription API is very strict about exact arg matching
@@ -197,7 +199,7 @@ class StructuredLogger:
if m: if m:
src = m.group(1) src = m.group(1)
args = m.group(2) args = m.group(2)
logging.debug(f"SLog {src}, args: {args}") logger.debug(f"SLog {src}, args: {args}")
d = log_defs.get(src) d = log_defs.get(src)
if d: if d:
@@ -219,9 +221,9 @@ class StructuredLogger:
# If the last field is an empty string, remove it # If the last field is an empty string, remove it
del di[last_field[0]] del di[last_field[0]]
else: else:
logging.warning(f"Failed to parse slog {line} with {d.format}") logger.warning(f"Failed to parse slog {line} with {d.format}")
else: else:
logging.warning(f"Unknown Structured Log: {line}") logger.warning(f"Unknown Structured Log: {line}")
# Store our structured log record # Store our structured log record
if di or self.include_raw: if di or self.include_raw:
@@ -256,23 +258,28 @@ class LogSet:
if not dir_name: if not dir_name:
app_dir = root_dir() app_dir = root_dir()
dir_name = f"{app_dir}/{datetime.now().strftime('%Y%m%d-%H%M%S')}" app_time_dir = Path(app_dir, datetime.now().strftime('%Y%m%d-%H%M%S'))
os.makedirs(dir_name, exist_ok=True) app_time_dir.mkdir(exist_ok=True)
dir_name = str(app_time_dir)
# Also make a 'latest' directory that always points to the most recent logs # Also make a 'latest' directory that always points to the most recent logs
latest_dir = Path(app_dir, "latest")
latest_dir.unlink(missing_ok=True)
# symlink might fail on some platforms, if it does fail silently # symlink might fail on some platforms, if it does fail silently
if os.path.exists(f"{app_dir}/latest"): try:
os.unlink(f"{app_dir}/latest") latest_dir.symlink_to(dir_name, target_is_directory=True)
os.symlink(dir_name, f"{app_dir}/latest", target_is_directory=True) except OSError:
pass
self.dir_name = dir_name self.dir_name = dir_name
logging.info(f"Writing slogs to {dir_name}") logger.info(f"Writing slogs to {dir_name}")
self.power_logger: Optional[PowerLogger] = ( self.power_logger: Optional[PowerLogger] = (
None None
if not power_meter if not power_meter
else PowerLogger(power_meter, f"{self.dir_name}/power") else PowerLogger(power_meter, os.path.join(self.dir_name, "power"))
) )
self.slog_logger: Optional[StructuredLogger] = StructuredLogger( self.slog_logger: Optional[StructuredLogger] = StructuredLogger(
@@ -286,7 +293,7 @@ class LogSet:
"""Close the log set.""" """Close the log set."""
if self.slog_logger: if self.slog_logger:
logging.info(f"Closing slogs in {self.dir_name}") logger.info(f"Closing slogs in {self.dir_name}")
atexit.unregister( atexit.unregister(
self.atexit_handler self.atexit_handler
) # docs say it will silently ignore if not found ) # docs say it will silently ignore if not found

View File

@@ -17,6 +17,7 @@ START1 = 0x94
START2 = 0xC3 START2 = 0xC3
HEADER_LEN = 4 HEADER_LEN = 4
MAX_TO_FROM_RADIO_SIZE = 512 MAX_TO_FROM_RADIO_SIZE = 512
logger = logging.getLogger(__name__)
class StreamInterface(MeshInterface): class StreamInterface(MeshInterface):
@@ -82,7 +83,7 @@ class StreamInterface(MeshInterface):
"""We override the superclass implementation to close our port""" """We override the superclass implementation to close our port"""
MeshInterface._disconnected(self) MeshInterface._disconnected(self)
logging.debug("Closing our port") logger.debug("Closing our port")
# pylint: disable=E0203 # pylint: disable=E0203
if not self.stream is None: if not self.stream is None:
# pylint: disable=E0203 # pylint: disable=E0203
@@ -111,17 +112,17 @@ class StreamInterface(MeshInterface):
def _sendToRadioImpl(self, toRadio) -> None: def _sendToRadioImpl(self, toRadio) -> None:
"""Send a ToRadio protobuf to the device""" """Send a ToRadio protobuf to the device"""
logging.debug(f"Sending: {stripnl(toRadio)}") logger.debug(f"Sending: {stripnl(toRadio)}")
b: bytes = toRadio.SerializeToString() b: bytes = toRadio.SerializeToString()
bufLen: int = len(b) bufLen: int = len(b)
# We convert into a string, because the TCP code doesn't work with byte arrays # 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]) header: bytes = bytes([START1, START2, (bufLen >> 8) & 0xFF, bufLen & 0xFF])
logging.debug(f"sending header:{header!r} b:{b!r}") logger.debug(f"sending header:{header!r} b:{b!r}")
self._writeBytes(header + b) self._writeBytes(header + b)
def close(self) -> None: def close(self) -> None:
"""Close a connection to the device""" """Close a connection to the device"""
logging.debug("Closing stream") logger.debug("Closing stream")
MeshInterface.close(self) MeshInterface.close(self)
# pyserial cancel_read doesn't seem to work, therefore we ask the # pyserial cancel_read doesn't seem to work, therefore we ask the
# reader thread to close things for us # reader thread to close things for us
@@ -148,18 +149,18 @@ class StreamInterface(MeshInterface):
def __reader(self) -> None: def __reader(self) -> None:
"""The reader thread that reads bytes from our stream""" """The reader thread that reads bytes from our stream"""
logging.debug("in __reader()") logger.debug("in __reader()")
empty = bytes() empty = bytes()
try: try:
while not self._wantExit: while not self._wantExit:
# logging.debug("reading character") # logger.debug("reading character")
b: Optional[bytes] = self._readBytes(1) b: Optional[bytes] = self._readBytes(1)
# logging.debug("In reader loop") # logger.debug("In reader loop")
# logging.debug(f"read returned {b}") # logger.debug(f"read returned {b}")
if b is not None and len(cast(bytes, b)) > 0: if b is not None and len(cast(bytes, b)) > 0:
c: int = b[0] c: int = b[0]
# logging.debug(f'c:{c}') # logger.debug(f'c:{c}')
ptr: int = len(self._rxBuf) ptr: int = len(self._rxBuf)
# Assume we want to append this byte, fixme use bytearray instead # Assume we want to append this byte, fixme use bytearray instead
@@ -176,7 +177,7 @@ class StreamInterface(MeshInterface):
if c != START2: if c != START2:
self._rxBuf = empty # failed to find start2 self._rxBuf = empty # failed to find start2
elif ptr >= HEADER_LEN - 1: # we've at least got a header elif ptr >= HEADER_LEN - 1: # we've at least got a header
# logging.debug('at least we received a header') # logger.debug('at least we received a header')
# big endian length follows header # big endian length follows header
packetlen = (self._rxBuf[2] << 8) + self._rxBuf[3] packetlen = (self._rxBuf[2] << 8) + self._rxBuf[3]
@@ -192,32 +193,32 @@ class StreamInterface(MeshInterface):
try: try:
self._handleFromRadio(self._rxBuf[HEADER_LEN:]) self._handleFromRadio(self._rxBuf[HEADER_LEN:])
except Exception as ex: except Exception as ex:
logging.error( logger.error(
f"Error while handling message from radio {ex}" f"Error while handling message from radio {ex}"
) )
traceback.print_exc() traceback.print_exc()
self._rxBuf = empty self._rxBuf = empty
else: else:
# logging.debug(f"timeout") # logger.debug(f"timeout")
pass pass
except serial.SerialException as ex: except serial.SerialException as ex:
if ( if (
not self._wantExit not self._wantExit
): # We might intentionally get an exception during shutdown ): # We might intentionally get an exception during shutdown
logging.warning( logger.warning(
f"Meshtastic serial port disconnected, disconnecting... {ex}" f"Meshtastic serial port disconnected, disconnecting... {ex}"
) )
except OSError as ex: except OSError as ex:
if ( if (
not self._wantExit not self._wantExit
): # We might intentionally get an exception during shutdown ): # We might intentionally get an exception during shutdown
logging.error( logger.error(
f"Unexpected OSError, terminating meshtastic reader... {ex}" f"Unexpected OSError, terminating meshtastic reader... {ex}"
) )
except Exception as ex: except Exception as ex:
logging.error( logger.error(
f"Unexpected exception, terminating meshtastic reader... {ex}" f"Unexpected exception, terminating meshtastic reader... {ex}"
) )
finally: finally:
logging.debug("reader is exiting") logger.debug("reader is exiting")
self._disconnected() self._disconnected()

View File

@@ -10,6 +10,7 @@ from typing import Optional
from meshtastic.stream_interface import StreamInterface from meshtastic.stream_interface import StreamInterface
DEFAULT_TCP_PORT = 4403 DEFAULT_TCP_PORT = 4403
logger = logging.getLogger(__name__)
class TCPInterface(StreamInterface): class TCPInterface(StreamInterface):
"""Interface class for meshtastic devices over a TCP link""" """Interface class for meshtastic devices over a TCP link"""
@@ -67,13 +68,13 @@ class TCPInterface(StreamInterface):
def myConnect(self) -> None: def myConnect(self) -> None:
"""Connect to socket""" """Connect to socket"""
logging.debug(f"Connecting to {self.hostname}") # type: ignore[str-bytes-safe] logger.debug(f"Connecting to {self.hostname}") # type: ignore[str-bytes-safe]
server_address = (self.hostname, self.portNumber) server_address = (self.hostname, self.portNumber)
self.socket = socket.create_connection(server_address) self.socket = socket.create_connection(server_address)
def close(self) -> None: def close(self) -> None:
"""Close a connection to the device""" """Close a connection to the device"""
logging.debug("Closing TCP stream") logger.debug("Closing TCP stream")
super().close() super().close()
# Sometimes the socket read might be blocked in the reader thread. # Sometimes the socket read might be blocked in the reader thread.
# Therefore we force the shutdown by closing the socket here # Therefore we force the shutdown by closing the socket here
@@ -97,7 +98,7 @@ class TCPInterface(StreamInterface):
# empty byte indicates a disconnected socket, # empty byte indicates a disconnected socket,
# we need to handle it to avoid an infinite loop reading from null socket # we need to handle it to avoid an infinite loop reading from null socket
if data == b'': if data == b'':
logging.debug("dead socket, re-connecting") logger.debug("dead socket, re-connecting")
# cleanup and reconnect socket without breaking reader thread # cleanup and reconnect socket without breaking reader thread
with contextlib.suppress(Exception): with contextlib.suppress(Exception):
self._socket_shutdown() self._socket_shutdown()

View File

@@ -29,6 +29,7 @@ testNumber: int = 0
sendingInterface = None sendingInterface = None
logger = logging.getLogger(__name__)
def onReceive(packet, interface) -> None: def onReceive(packet, interface) -> None:
"""Callback invoked when a packet arrives""" """Callback invoked when a packet arrives"""
@@ -79,7 +80,7 @@ def testSend(
else: else:
toNode = toInterface.myInfo.my_node_num toNode = toInterface.myInfo.my_node_num
logging.debug(f"Sending test wantAck={wantAck} packet from {fromNode} to {toNode}") logger.debug(f"Sending test wantAck={wantAck} packet from {fromNode} to {toNode}")
# pylint: disable=W0603 # pylint: disable=W0603
global sendingInterface global sendingInterface
sendingInterface = fromInterface sendingInterface = fromInterface
@@ -98,7 +99,7 @@ def testSend(
def runTests(numTests: int=50, wantAck: bool=False, maxFailures: int=0) -> bool: def runTests(numTests: int=50, wantAck: bool=False, maxFailures: int=0) -> bool:
"""Run the tests.""" """Run the tests."""
logging.info(f"Running {numTests} tests with wantAck={wantAck}") logger.info(f"Running {numTests} tests with wantAck={wantAck}")
numFail: int = 0 numFail: int = 0
numSuccess: int = 0 numSuccess: int = 0
for _ in range(numTests): for _ in range(numTests):
@@ -112,26 +113,26 @@ def runTests(numTests: int=50, wantAck: bool=False, maxFailures: int=0) -> bool:
) )
if not success: if not success:
numFail = numFail + 1 numFail = numFail + 1
logging.error( logger.error(
f"Test {testNumber} failed, expected packet not received ({numFail} failures so far)" f"Test {testNumber} failed, expected packet not received ({numFail} failures so far)"
) )
else: else:
numSuccess = numSuccess + 1 numSuccess = numSuccess + 1
logging.info( logger.info(
f"Test {testNumber} succeeded {numSuccess} successes {numFail} failures so far" f"Test {testNumber} succeeded {numSuccess} successes {numFail} failures so far"
) )
time.sleep(1) time.sleep(1)
if numFail > maxFailures: if numFail > maxFailures:
logging.error("Too many failures! Test failed!") logger.error("Too many failures! Test failed!")
return False return False
return True return True
def testThread(numTests=50) -> bool: def testThread(numTests=50) -> bool:
"""Test thread""" """Test thread"""
logging.info("Found devices, starting tests...") logger.info("Found devices, starting tests...")
result: bool = runTests(numTests, wantAck=True) result: bool = runTests(numTests, wantAck=True)
if result: if result:
# Run another test # Run another test
@@ -148,7 +149,7 @@ def onConnection(topic=pub.AUTO_TOPIC) -> None:
def openDebugLog(portName) -> io.TextIOWrapper: def openDebugLog(portName) -> io.TextIOWrapper:
"""Open the debug log file""" """Open the debug log file"""
debugname = "log" + portName.replace("/", "_") debugname = "log" + portName.replace("/", "_")
logging.info(f"Writing serial debugging to {debugname}") logger.info(f"Writing serial debugging to {debugname}")
return open(debugname, "w+", buffering=1, encoding="utf8") return open(debugname, "w+", buffering=1, encoding="utf8")
@@ -177,7 +178,7 @@ def testAll(numTests: int=5) -> bool:
) )
) )
logging.info("Ports opened, starting test") logger.info("Ports opened, starting test")
result: bool = testThread(numTests) result: bool = testThread(numTests)
for i in interfaces: for i in interfaces:
@@ -196,14 +197,14 @@ def testSimulator() -> None:
python3 -c 'from meshtastic.test import testSimulator; testSimulator()' python3 -c 'from meshtastic.test import testSimulator; testSimulator()'
""" """
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
logging.info("Connecting to simulator on localhost!") logger.info("Connecting to simulator on localhost!")
try: try:
iface: meshtastic.tcp_interface.TCPInterface = TCPInterface("localhost") iface: meshtastic.tcp_interface.TCPInterface = TCPInterface("localhost")
iface.showInfo() iface.showInfo()
iface.localNode.showInfo() iface.localNode.showInfo()
iface.localNode.exitSimulator() iface.localNode.exitSimulator()
iface.close() iface.close()
logging.info("Integration test successful!") logger.info("Integration test successful!")
except: except:
print("Error while testing simulator:", sys.exc_info()[0]) print("Error while testing simulator:", sys.exc_info()[0])
traceback.print_exc() traceback.print_exc()

View File

@@ -35,7 +35,6 @@ from ..tcp_interface import TCPInterface
# from ..remote_hardware import onGPIOreceive # from ..remote_hardware import onGPIOreceive
# from ..config_pb2 import Config # from ..config_pb2 import Config
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config") @pytest.mark.usefixtures("reset_mt_config")
def test_main_init_parser_no_args(capsys): def test_main_init_parser_no_args(capsys):
@@ -758,12 +757,11 @@ def test_main_sendtext_with_invalid_channel_nine(caplog, capsys):
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config") @pytest.mark.usefixtures("reset_mt_config")
@patch("termios.tcsetattr") @patch("meshtastic.serial_interface.SerialInterface._set_hupcl_with_termios")
@patch("termios.tcgetattr")
@patch("builtins.open", new_callable=mock_open, read_data="data") @patch("builtins.open", new_callable=mock_open, read_data="data")
@patch("serial.Serial") @patch("serial.Serial")
@patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"]) @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): def test_main_sendtext_with_dest(mock_findPorts, mock_serial, mocked_open, mock_hupcl, capsys, caplog, iface_with_nodes):
"""Test --sendtext with --dest""" """Test --sendtext with --dest"""
sys.argv = ["", "--sendtext", "hello", "--dest", "foo"] sys.argv = ["", "--sendtext", "hello", "--dest", "foo"]
mt_config.args = sys.argv mt_config.args = sys.argv
@@ -957,12 +955,11 @@ def test_main_seturl(capsys):
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config") @pytest.mark.usefixtures("reset_mt_config")
@patch("termios.tcsetattr") @patch("meshtastic.serial_interface.SerialInterface._set_hupcl_with_termios")
@patch("termios.tcgetattr")
@patch("builtins.open", new_callable=mock_open, read_data="data") @patch("builtins.open", new_callable=mock_open, read_data="data")
@patch("serial.Serial") @patch("serial.Serial")
@patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"]) @patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"])
def test_main_set_valid(mocked_findports, mocked_serial, mocked_open, mocked_get, mocked_set, capsys): def test_main_set_valid(mocked_findports, mocked_serial, mocked_open, mocked_hupcl, capsys):
"""Test --set with valid field""" """Test --set with valid field"""
sys.argv = ["", "--set", "network.wifi_ssid", "foo"] sys.argv = ["", "--set", "network.wifi_ssid", "foo"]
mt_config.args = sys.argv mt_config.args = sys.argv
@@ -982,12 +979,11 @@ def test_main_set_valid(mocked_findports, mocked_serial, mocked_open, mocked_get
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config") @pytest.mark.usefixtures("reset_mt_config")
@patch("termios.tcsetattr") @patch("meshtastic.serial_interface.SerialInterface._set_hupcl_with_termios")
@patch("termios.tcgetattr")
@patch("builtins.open", new_callable=mock_open, read_data="data") @patch("builtins.open", new_callable=mock_open, read_data="data")
@patch("serial.Serial") @patch("serial.Serial")
@patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"]) @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): def test_main_set_valid_wifi_psk(mocked_findports, mocked_serial, mocked_open, mocked_hupcl, capsys):
"""Test --set with valid field""" """Test --set with valid field"""
sys.argv = ["", "--set", "network.wifi_psk", "123456789"] sys.argv = ["", "--set", "network.wifi_psk", "123456789"]
mt_config.args = sys.argv mt_config.args = sys.argv
@@ -1007,12 +1003,11 @@ def test_main_set_valid_wifi_psk(mocked_findports, mocked_serial, mocked_open, m
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config") @pytest.mark.usefixtures("reset_mt_config")
@patch("termios.tcsetattr") @patch("meshtastic.serial_interface.SerialInterface._set_hupcl_with_termios")
@patch("termios.tcgetattr")
@patch("builtins.open", new_callable=mock_open, read_data="data") @patch("builtins.open", new_callable=mock_open, read_data="data")
@patch("serial.Serial") @patch("serial.Serial")
@patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"]) @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): def test_main_set_invalid_wifi_psk(mocked_findports, mocked_serial, mocked_open, mocked_hupcl, capsys):
"""Test --set with an invalid value (psk must be 8 or more characters)""" """Test --set with an invalid value (psk must be 8 or more characters)"""
sys.argv = ["", "--set", "network.wifi_psk", "1234567"] sys.argv = ["", "--set", "network.wifi_psk", "1234567"]
mt_config.args = sys.argv mt_config.args = sys.argv
@@ -1035,12 +1030,11 @@ def test_main_set_invalid_wifi_psk(mocked_findports, mocked_serial, mocked_open,
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config") @pytest.mark.usefixtures("reset_mt_config")
@patch("termios.tcsetattr") @patch("meshtastic.serial_interface.SerialInterface._set_hupcl_with_termios")
@patch("termios.tcgetattr")
@patch("builtins.open", new_callable=mock_open, read_data="data") @patch("builtins.open", new_callable=mock_open, read_data="data")
@patch("serial.Serial") @patch("serial.Serial")
@patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"]) @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): def test_main_set_valid_camel_case(mocked_findports, mocked_serial, mocked_open, mocked_hupcl, capsys):
"""Test --set with valid field""" """Test --set with valid field"""
sys.argv = ["", "--set", "network.wifi_ssid", "foo"] sys.argv = ["", "--set", "network.wifi_ssid", "foo"]
mt_config.args = sys.argv mt_config.args = sys.argv
@@ -1061,12 +1055,11 @@ def test_main_set_valid_camel_case(mocked_findports, mocked_serial, mocked_open,
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config") @pytest.mark.usefixtures("reset_mt_config")
@patch("termios.tcsetattr") @patch("meshtastic.serial_interface.SerialInterface._set_hupcl_with_termios")
@patch("termios.tcgetattr")
@patch("builtins.open", new_callable=mock_open, read_data="data") @patch("builtins.open", new_callable=mock_open, read_data="data")
@patch("serial.Serial") @patch("serial.Serial")
@patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"]) @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): def test_main_set_with_invalid(mocked_findports, mocked_serial, mocked_open, mocked_hupcl, capsys):
"""Test --set with invalid field""" """Test --set with invalid field"""
sys.argv = ["", "--set", "foo", "foo"] sys.argv = ["", "--set", "foo", "foo"]
mt_config.args = sys.argv mt_config.args = sys.argv
@@ -1087,12 +1080,11 @@ def test_main_set_with_invalid(mocked_findports, mocked_serial, mocked_open, moc
# TODO: write some negative --configure tests # TODO: write some negative --configure tests
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config") @pytest.mark.usefixtures("reset_mt_config")
@patch("termios.tcsetattr") @patch("meshtastic.serial_interface.SerialInterface._set_hupcl_with_termios")
@patch("termios.tcgetattr")
@patch("builtins.open", new_callable=mock_open, read_data="data") @patch("builtins.open", new_callable=mock_open, read_data="data")
@patch("serial.Serial") @patch("serial.Serial")
@patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"]) @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): def test_main_configure_with_snake_case(mocked_findports, mocked_serial, mocked_open, mocked_hupcl, capsys):
"""Test --configure with valid file""" """Test --configure with valid file"""
sys.argv = ["", "--configure", "example_config.yaml"] sys.argv = ["", "--configure", "example_config.yaml"]
mt_config.args = sys.argv mt_config.args = sys.argv
@@ -1120,12 +1112,11 @@ def test_main_configure_with_snake_case(mocked_findports, mocked_serial, mocked_
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config") @pytest.mark.usefixtures("reset_mt_config")
@patch("termios.tcsetattr") @patch("meshtastic.serial_interface.SerialInterface._set_hupcl_with_termios")
@patch("termios.tcgetattr")
@patch("builtins.open", new_callable=mock_open, read_data="data") @patch("builtins.open", new_callable=mock_open, read_data="data")
@patch("serial.Serial") @patch("serial.Serial")
@patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"]) @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): def test_main_configure_with_camel_case_keys(mocked_findports, mocked_serial, mocked_open, mocked_hupcl, capsys):
"""Test --configure with valid file""" """Test --configure with valid file"""
sys.argv = ["", "--configure", "exampleConfig.yaml"] sys.argv = ["", "--configure", "exampleConfig.yaml"]
mt_config.args = sys.argv mt_config.args = sys.argv
@@ -2722,16 +2713,16 @@ def test_tunnel_subnet_arg_with_no_devices(mock_platform_system, caplog, capsys)
assert err == "" assert err == ""
@pytest.mark.skipif(sys.platform == "win32", reason="on windows is no fcntl module")
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config") @pytest.mark.usefixtures("reset_mt_config")
@patch("platform.system") @patch("platform.system")
@patch("termios.tcsetattr") @patch("meshtastic.serial_interface.SerialInterface._set_hupcl_with_termios")
@patch("termios.tcgetattr")
@patch("builtins.open", new_callable=mock_open, read_data="data") @patch("builtins.open", new_callable=mock_open, read_data="data")
@patch("serial.Serial") @patch("serial.Serial")
@patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"]) @patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"])
def test_tunnel_tunnel_arg( def test_tunnel_tunnel_arg(
mocked_findPorts, mocked_serial, mocked_open, mock_get, mock_set, mock_platform_system, caplog, iface_with_nodes, capsys mocked_findPorts, mocked_serial, mocked_open, mock_hupcl, mock_platform_system, caplog, iface_with_nodes, capsys
): ):
"""Test tunnel with tunnel arg (act like we are on a linux system)""" """Test tunnel with tunnel arg (act like we are on a linux system)"""

View File

@@ -672,15 +672,21 @@ def test_getOrCreateByNum(iface_with_nodes):
@pytest.mark.unit @pytest.mark.unit
def test_exit_with_exception(caplog): def test_exit_with_exception(caplog):
"""Test __exit__()""" """Test __exit__()"""
iface = MeshInterface(noProto=True)
with caplog.at_level(logging.ERROR): with caplog.at_level(logging.ERROR):
iface.__exit__("foo", "bar", "baz") try:
assert re.search( with MeshInterface(noProto=True):
r"An exception of type foo with value bar has occurred", raise ValueError("Something went wrong")
caplog.text, except:
re.MULTILINE, assert re.search(
) r"An exception of type <class \'ValueError\'> with value Something went wrong has occurred",
assert re.search(r"Traceback: baz", caplog.text, re.MULTILINE) caplog.text,
re.MULTILINE,
)
assert re.search(
r"Traceback:\n.*in test_exit_with_exception\n {4}raise ValueError\(\"Something went wrong\"\)",
caplog.text,
re.MULTILINE
)
@pytest.mark.unit @pytest.mark.unit

View File

@@ -2,6 +2,7 @@
# pylint: disable=R0917 # pylint: disable=R0917
import re import re
import sys
from unittest.mock import mock_open, patch from unittest.mock import mock_open, patch
import pytest import pytest
@@ -9,16 +10,14 @@ import pytest
from ..serial_interface import SerialInterface from ..serial_interface import SerialInterface
from ..protobuf import config_pb2 from ..protobuf import config_pb2
@pytest.mark.unit @pytest.mark.unit
@patch("time.sleep") @patch("time.sleep")
@patch("termios.tcsetattr") @patch("meshtastic.serial_interface.SerialInterface._set_hupcl_with_termios")
@patch("termios.tcgetattr")
@patch("builtins.open", new_callable=mock_open, read_data="data") @patch("builtins.open", new_callable=mock_open, read_data="data")
@patch("serial.Serial") @patch("serial.Serial")
@patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"]) @patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"])
def test_SerialInterface_single_port( def test_SerialInterface_single_port(
mocked_findPorts, mocked_serial, mocked_open, mock_get, mock_set, mock_sleep, capsys mocked_findPorts, mocked_serial, mocked_open, mock_hupcl, mock_sleep, capsys
): ):
"""Test that we can instantiate a SerialInterface with a single port""" """Test that we can instantiate a SerialInterface with a single port"""
iface = SerialInterface(noProto=True) iface = SerialInterface(noProto=True)
@@ -28,9 +27,12 @@ def test_SerialInterface_single_port(
iface.close() iface.close()
mocked_findPorts.assert_called() mocked_findPorts.assert_called()
mocked_serial.assert_called() mocked_serial.assert_called()
mocked_open.assert_called()
mock_get.assert_called() # doesn't get called in SerialInterface on windows
mock_set.assert_called() if sys.platform != "win32":
mocked_open.assert_called()
mock_hupcl.assert_called()
mock_sleep.assert_called() mock_sleep.assert_called()
out, err = capsys.readouterr() out, err = capsys.readouterr()
assert re.search(r"Nodes in mesh", out, re.MULTILINE) assert re.search(r"Nodes in mesh", out, re.MULTILINE)

View File

@@ -26,10 +26,11 @@ from meshtastic.protobuf import portnums_pb2
from meshtastic import mt_config from meshtastic import mt_config
from meshtastic.util import ipstr, readnet_u16 from meshtastic.util import ipstr, readnet_u16
logger = logging.getLogger(__name__)
def onTunnelReceive(packet, interface): # pylint: disable=W0613 def onTunnelReceive(packet, interface): # pylint: disable=W0613
"""Callback for received tunneled messages from mesh.""" """Callback for received tunneled messages from mesh."""
logging.debug(f"in onTunnelReceive()") logger.debug(f"in onTunnelReceive()")
tunnelInstance = mt_config.tunnelInstance tunnelInstance = mt_config.tunnelInstance
tunnelInstance.onReceive(packet) tunnelInstance.onReceive(packet)
@@ -92,7 +93,7 @@ class Tunnel:
self.LOG_TRACE = 5 self.LOG_TRACE = 5
# TODO: check if root? # TODO: check if root?
logging.info( logger.info(
"Starting IP to mesh tunnel (you must be root for this *pre-alpha* " "Starting IP to mesh tunnel (you must be root for this *pre-alpha* "
"feature to work). Mesh members:" "feature to work). Mesh members:"
) )
@@ -104,13 +105,13 @@ class Tunnel:
for node in self.iface.nodes.values(): for node in self.iface.nodes.values():
nodeId = node["user"]["id"] nodeId = node["user"]["id"]
ip = self._nodeNumToIp(node["num"]) ip = self._nodeNumToIp(node["num"])
logging.info(f"Node { nodeId } has IP address { ip }") logger.info(f"Node { nodeId } has IP address { ip }")
logging.debug("creating TUN device with MTU=200") logger.debug("creating TUN device with MTU=200")
# FIXME - figure out real max MTU, it should be 240 - the overhead bytes for SubPacket and Data # FIXME - figure out real max MTU, it should be 240 - the overhead bytes for SubPacket and Data
self.tun = None self.tun = None
if self.iface.noProto: if self.iface.noProto:
logging.warning( logger.warning(
f"Not creating a TapDevice() because it is disabled by noProto" f"Not creating a TapDevice() because it is disabled by noProto"
) )
else: else:
@@ -120,11 +121,11 @@ class Tunnel:
self._rxThread = None self._rxThread = None
if self.iface.noProto: if self.iface.noProto:
logging.warning( logger.warning(
f"Not starting TUN reader because it is disabled by noProto" f"Not starting TUN reader because it is disabled by noProto"
) )
else: else:
logging.debug(f"starting TUN reader, our IP address is {myAddr}") logger.debug(f"starting TUN reader, our IP address is {myAddr}")
self._rxThread = threading.Thread( self._rxThread = threading.Thread(
target=self.__tunReader, args=(), daemon=True target=self.__tunReader, args=(), daemon=True
) )
@@ -134,9 +135,9 @@ class Tunnel:
"""onReceive""" """onReceive"""
p = packet["decoded"]["payload"] p = packet["decoded"]["payload"]
if packet["from"] == self.iface.myInfo.my_node_num: if packet["from"] == self.iface.myInfo.my_node_num:
logging.debug("Ignoring message we sent") logger.debug("Ignoring message we sent")
else: else:
logging.debug(f"Received mesh tunnel message type={type(p)} len={len(p)}") logger.debug(f"Received mesh tunnel message type={type(p)} len={len(p)}")
# we don't really need to check for filtering here (sender should have checked), # we don't really need to check for filtering here (sender should have checked),
# but this provides useful debug printing on types of packets received # but this provides useful debug printing on types of packets received
if not self.iface.noProto: if not self.iface.noProto:
@@ -152,7 +153,7 @@ class Tunnel:
ignore = False # Assume we will be forwarding the packet ignore = False # Assume we will be forwarding the packet
if protocol in self.protocolBlacklist: if protocol in self.protocolBlacklist:
ignore = True ignore = True
logging.log( logger.log(
self.LOG_TRACE, f"Ignoring blacklisted protocol 0x{protocol:02x}" self.LOG_TRACE, f"Ignoring blacklisted protocol 0x{protocol:02x}"
) )
elif protocol == 0x01: # ICMP elif protocol == 0x01: # ICMP
@@ -160,7 +161,7 @@ class Tunnel:
icmpCode = p[21] icmpCode = p[21]
checksum = p[22:24] checksum = p[22:24]
# pylint: disable=line-too-long # pylint: disable=line-too-long
logging.debug( logger.debug(
f"forwarding ICMP message src={ipstr(srcaddr)}, dest={ipstr(destAddr)}, type={icmpType}, code={icmpCode}, checksum={checksum}" f"forwarding ICMP message src={ipstr(srcaddr)}, dest={ipstr(destAddr)}, type={icmpType}, code={icmpCode}, checksum={checksum}"
) )
# reply to pings (swap src and dest but keep rest of packet unchanged) # reply to pings (swap src and dest but keep rest of packet unchanged)
@@ -171,19 +172,19 @@ class Tunnel:
destport = readnet_u16(p, subheader + 2) destport = readnet_u16(p, subheader + 2)
if destport in self.udpBlacklist: if destport in self.udpBlacklist:
ignore = True ignore = True
logging.log(self.LOG_TRACE, f"ignoring blacklisted UDP port {destport}") logger.log(self.LOG_TRACE, f"ignoring blacklisted UDP port {destport}")
else: else:
logging.debug(f"forwarding udp srcport={srcport}, destport={destport}") logger.debug(f"forwarding udp srcport={srcport}, destport={destport}")
elif protocol == 0x06: # TCP elif protocol == 0x06: # TCP
srcport = readnet_u16(p, subheader) srcport = readnet_u16(p, subheader)
destport = readnet_u16(p, subheader + 2) destport = readnet_u16(p, subheader + 2)
if destport in self.tcpBlacklist: if destport in self.tcpBlacklist:
ignore = True ignore = True
logging.log(self.LOG_TRACE, f"ignoring blacklisted TCP port {destport}") logger.log(self.LOG_TRACE, f"ignoring blacklisted TCP port {destport}")
else: else:
logging.debug(f"forwarding tcp srcport={srcport}, destport={destport}") logger.debug(f"forwarding tcp srcport={srcport}, destport={destport}")
else: else:
logging.warning( logger.warning(
f"forwarding unexpected protocol 0x{protocol:02x}, " f"forwarding unexpected protocol 0x{protocol:02x}, "
"src={ipstr(srcaddr)}, dest={ipstr(destAddr)}" "src={ipstr(srcaddr)}, dest={ipstr(destAddr)}"
) )
@@ -192,10 +193,10 @@ class Tunnel:
def __tunReader(self): def __tunReader(self):
tap = self.tun tap = self.tun
logging.debug("TUN reader running") logger.debug("TUN reader running")
while True: while True:
p = tap.read() p = tap.read()
# logging.debug(f"IP packet received on TUN interface, type={type(p)}") # logger.debug(f"IP packet received on TUN interface, type={type(p)}")
destAddr = p[16:20] destAddr = p[16:20]
if not self._shouldFilterPacket(p): if not self._shouldFilterPacket(p):
@@ -210,7 +211,7 @@ class Tunnel:
for node in self.iface.nodes.values(): for node in self.iface.nodes.values():
nodeNum = node["num"] & 0xFFFF nodeNum = node["num"] & 0xFFFF
# logging.debug(f"Considering nodenum 0x{nodeNum:x} for ipBits 0x{ipBits:x}") # logger.debug(f"Considering nodenum 0x{nodeNum:x} for ipBits 0x{ipBits:x}")
if (nodeNum) == ipBits: if (nodeNum) == ipBits:
return node["user"]["id"] return node["user"]["id"]
return None return None
@@ -222,12 +223,12 @@ class Tunnel:
"""Forward the provided IP packet into the mesh""" """Forward the provided IP packet into the mesh"""
nodeId = self._ipToNodeId(destAddr) nodeId = self._ipToNodeId(destAddr)
if nodeId is not None: if nodeId is not None:
logging.debug( logger.debug(
f"Forwarding packet bytelen={len(p)} dest={ipstr(destAddr)}, destNode={nodeId}" f"Forwarding packet bytelen={len(p)} dest={ipstr(destAddr)}, destNode={nodeId}"
) )
self.iface.sendData(p, nodeId, portnums_pb2.IP_TUNNEL_APP, wantAck=False) self.iface.sendData(p, nodeId, portnums_pb2.IP_TUNNEL_APP, wantAck=False)
else: else:
logging.warning( logger.warning(
f"Dropping packet because no node found for destIP={ipstr(destAddr)}" f"Dropping packet because no node found for destIP={ipstr(destAddr)}"
) )

View File

@@ -38,6 +38,7 @@ blacklistVids: Dict = dict.fromkeys([0x1366, 0x0483, 0x1915, 0x0925, 0x04b4])
0x303a Heltec tracker""" 0x303a Heltec tracker"""
whitelistVids = dict.fromkeys([0x239a, 0x303a]) whitelistVids = dict.fromkeys([0x239a, 0x303a])
logger = logging.getLogger(__name__)
def quoteBooleans(a_string: str) -> str: def quoteBooleans(a_string: str) -> str:
"""Quote booleans """Quote booleans
@@ -141,7 +142,7 @@ def catchAndIgnore(reason: str, closure) -> None:
try: try:
closure() closure()
except BaseException as ex: except BaseException as ex:
logging.error(f"Exception thrown in {reason}: {ex}") logger.error(f"Exception thrown in {reason}: {ex}")
def findPorts(eliminate_duplicates: bool=False) -> List[str]: def findPorts(eliminate_duplicates: bool=False) -> List[str]:
@@ -307,7 +308,7 @@ class DeferredExecution:
o = self.queue.get() o = self.queue.get()
o() o()
except: except:
logging.error( logger.error(
f"Unexpected error in deferred execution {sys.exc_info()[0]}" f"Unexpected error in deferred execution {sys.exc_info()[0]}"
) )
print(traceback.format_exc()) print(traceback.format_exc())

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "meshtastic" name = "meshtastic"
version = "2.7.2" version = "2.7.3"
description = "Python API & client shell for talking to Meshtastic devices" description = "Python API & client shell for talking to Meshtastic devices"
authors = ["Meshtastic Developers <contact@meshtastic.org>"] authors = ["Meshtastic Developers <contact@meshtastic.org>"]
license = "GPL-3.0-only" license = "GPL-3.0-only"