Merge pull request #636 from geeksville/pr-powermon2

final powermon / power analysis reporting changes
This commit is contained in:
Ian McEwen
2024-08-08 09:36:16 -07:00
committed by GitHub
20 changed files with 1702 additions and 616 deletions

View File

@@ -30,7 +30,7 @@ jobs:
pip3 install poetry
- name: Install meshtastic from local
run: |
poetry install
poetry install --all-extras --with dev
poetry run meshtastic --version
- name: Run pylint
run: poetry run pylint meshtastic examples/ --ignore-patterns ".*_pb2.pyi?$"

10
.vscode/launch.json vendored
View File

@@ -36,6 +36,14 @@
"justMyCode": true,
"args": ["--tunnel", "--debug"]
},
{
"name": "meshtastic analysis",
"type": "debugpy",
"request": "launch",
"module": "meshtastic.analysis",
"justMyCode": false,
"args": []
},
{
"name": "meshtastic set chan",
"type": "debugpy",
@@ -204,7 +212,7 @@
"request": "launch",
"module": "meshtastic",
"justMyCode": false,
"args": ["--slog-out", "default", "--power-ppk2-meter", "--power-stress", "--power-voltage", "3.3", "--seriallog"]
"args": ["--slog", "--power-ppk2-supply", "--power-stress", "--power-voltage", "3.3", "--ble"]
},
{
"name": "meshtastic test",

View File

@@ -2,6 +2,7 @@
"cSpell.words": [
"bitmask",
"boardid",
"DEEPSLEEP",
"Meshtastic",
"milliwatt",
"portnums",
@@ -12,5 +13,10 @@
"Vids"
],
"python.pythonPath": "/usr/bin/python3",
"flake8.enabled" : false // we are using trunk for formatting/linting rules, don't yell at us about line length
"flake8.enabled": false,
"python.testing.pytestArgs": [
"meshtastic/tests"
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true // we are using trunk for formatting/linting rules, don't yell at us about line length
}

View File

@@ -13,24 +13,30 @@ import sys
import time
from typing import Optional
import pyqrcode # type: ignore[import-untyped]
import pyqrcode # type: ignore[import-untyped]
import yaml
from google.protobuf.json_format import MessageToDict
from pubsub import pub # type: ignore[import-untyped]
from pubsub import pub # type: ignore[import-untyped]
import meshtastic.test
import meshtastic.util
from meshtastic import mt_config
from meshtastic.protobuf import channel_pb2, config_pb2, portnums_pb2
from meshtastic import remote_hardware, BROADCAST_ADDR
from meshtastic.version import get_active_version
from meshtastic import BROADCAST_ADDR, mt_config, remote_hardware
from meshtastic.ble_interface import BLEInterface
from meshtastic.mesh_interface import MeshInterface
from meshtastic.powermon import RidenPowerSupply, PPK2PowerSupply, SimPowerSupply, PowerStress, PowerMeter
from meshtastic.powermon import (
PowerMeter,
PowerStress,
PPK2PowerSupply,
RidenPowerSupply,
SimPowerSupply,
)
from meshtastic.protobuf import channel_pb2, config_pb2, portnums_pb2
from meshtastic.slog import LogSet
from meshtastic.version import get_active_version
meter: Optional[PowerMeter] = None
def onReceive(packet, interface):
"""Callback invoked when a packet arrives"""
args = mt_config.args
@@ -66,11 +72,13 @@ def onConnection(interface, topic=pub.AUTO_TOPIC): # pylint: disable=W0613
"""Callback invoked when we connect/disconnect from a radio"""
print(f"Connection changed: {topic.getName()}")
def checkChannel(interface: MeshInterface, channelIndex: int) -> bool:
"""Given an interface and channel index, return True if that channel is non-disabled on the local node"""
ch = interface.localNode.getChannelByChannelIndex(channelIndex)
logging.debug(f"ch:{ch}")
return (ch and ch.role != channel_pb2.Channel.Role.DISABLED)
return ch and ch.role != channel_pb2.Channel.Role.DISABLED
def getPref(node, comp_name):
"""Get a channel or preferences value"""
@@ -146,6 +154,7 @@ def splitCompoundName(comp_name):
name.append(comp_name)
return name
def traverseConfig(config_root, config, interface_config):
"""Iterate through current config level preferences and either traverse deeper if preference is a dict or set preference"""
snake_name = meshtastic.util.camel_to_snake(config_root)
@@ -154,14 +163,11 @@ def traverseConfig(config_root, config, interface_config):
if isinstance(config[pref], dict):
traverseConfig(pref_name, config[pref], interface_config)
else:
setPref(
interface_config,
pref_name,
str(config[pref])
)
setPref(interface_config, pref_name, str(config[pref]))
return True
def setPref(config, comp_name, valStr) -> bool:
"""Set a channel or preferences value"""
@@ -275,7 +281,9 @@ def onConnected(interface):
interface.localNode.removeFixedPosition()
elif args.setlat or args.setlon or args.setalt:
if args.dest != BROADCAST_ADDR:
print("Setting latitude, longitude, and altitude of remote nodes is not supported.")
print(
"Setting latitude, longitude, and altitude of remote nodes is not supported."
)
return
closeNow = True
@@ -303,10 +311,17 @@ def onConnected(interface):
interface.localNode.setFixedPosition(lat, lon, alt)
elif not args.no_time:
# We normally provide a current time to the mesh when we connect
if interface.localNode.nodeNum in interface.nodesByNum and "position" in interface.nodesByNum[interface.localNode.nodeNum]:
if (
interface.localNode.nodeNum in interface.nodesByNum
and "position" in interface.nodesByNum[interface.localNode.nodeNum]
):
# send the same position the node already knows, just to update time
position = interface.nodesByNum[interface.localNode.nodeNum]["position"]
interface.sendPosition(position.get("latitude", 0.0), position.get("longitude", 0.0), position.get("altitude", 0.0))
interface.sendPosition(
position.get("latitude", 0.0),
position.get("longitude", 0.0),
position.get("altitude", 0.0),
)
else:
interface.sendPosition()
@@ -454,7 +469,9 @@ def onConnected(interface):
dest = str(args.traceroute)
channelIndex = mt_config.channel_index or 0
if checkChannel(interface, channelIndex):
print(f"Sending traceroute request to {dest} on channelIndex:{channelIndex} (this could take a while)")
print(
f"Sending traceroute request to {dest} on channelIndex:{channelIndex} (this could take a while)"
)
interface.sendTraceRoute(dest, hopLimit, channelIndex=channelIndex)
if args.request_telemetry:
@@ -463,8 +480,14 @@ def onConnected(interface):
else:
channelIndex = mt_config.channel_index or 0
if checkChannel(interface, channelIndex):
print(f"Sending telemetry request to {args.dest} on channelIndex:{channelIndex} (this could take a while)")
interface.sendTelemetry(destinationId=args.dest, wantResponse=True, channelIndex=channelIndex)
print(
f"Sending telemetry request to {args.dest} on channelIndex:{channelIndex} (this could take a while)"
)
interface.sendTelemetry(
destinationId=args.dest,
wantResponse=True,
channelIndex=channelIndex,
)
if args.request_position:
if args.dest == BROADCAST_ADDR:
@@ -472,8 +495,14 @@ def onConnected(interface):
else:
channelIndex = mt_config.channel_index or 0
if checkChannel(interface, channelIndex):
print(f"Sending position request to {args.dest} on channelIndex:{channelIndex} (this could take a while)")
interface.sendPosition(destinationId=args.dest, wantResponse=True, channelIndex=channelIndex)
print(
f"Sending position request to {args.dest} on channelIndex:{channelIndex} (this could take a while)"
)
interface.sendPosition(
destinationId=args.dest,
wantResponse=True,
channelIndex=channelIndex,
)
if args.gpio_wrb or args.gpio_rd or args.gpio_watch:
if args.dest == BROADCAST_ADDR:
@@ -615,7 +644,9 @@ def onConnected(interface):
if "config" in configuration:
localConfig = interface.getNode(args.dest).localConfig
for section in configuration["config"]:
traverseConfig(section, configuration["config"][section], localConfig)
traverseConfig(
section, configuration["config"][section], localConfig
)
interface.getNode(args.dest).writeConfig(
meshtastic.util.camel_to_snake(section)
)
@@ -623,7 +654,11 @@ def onConnected(interface):
if "module_config" in configuration:
moduleConfig = interface.getNode(args.dest).moduleConfig
for section in configuration["module_config"]:
traverseConfig(section, configuration["module_config"][section], moduleConfig)
traverseConfig(
section,
configuration["module_config"][section],
moduleConfig,
)
interface.getNode(args.dest).writeConfig(
meshtastic.util.camel_to_snake(section)
)
@@ -676,7 +711,9 @@ def onConnected(interface):
print(f"Writing modified channels to device")
n.writeChannel(ch.index)
if channelIndex is None:
print(f"Setting newly-added channel's {ch.index} as '--ch-index' for further modifications")
print(
f"Setting newly-added channel's {ch.index} as '--ch-index' for further modifications"
)
mt_config.channel_index = ch.index
if args.ch_del:
@@ -762,7 +799,7 @@ def onConnected(interface):
else:
found = setPref(ch.settings, pref[0], pref[1])
if not found:
category_settings = ['module_settings']
category_settings = ["module_settings"]
print(
f"{ch.settings.__class__.__name__} does not have an attribute {pref[0]}."
)
@@ -772,7 +809,9 @@ def onConnected(interface):
print(f"{field.name}")
else:
print(f"{field.name}:")
config = ch.settings.DESCRIPTOR.fields_by_name.get(field.name)
config = ch.settings.DESCRIPTOR.fields_by_name.get(
field.name
)
names = []
for sub_field in config.message_type.fields:
tmp_name = f"{field.name}.{sub_field.name}"
@@ -852,14 +891,20 @@ def onConnected(interface):
qr = pyqrcode.create(url)
print(qr.terminal())
log_set: Optional[LogSet] = None # type: ignore[annotation-unchecked]
# we need to keep a reference to the logset so it doesn't get GCed early
if args.slog or args.power_stress:
# Setup loggers
global meter # pylint: disable=global-variable-not-assigned
LogSet(interface, args.slog if args.slog != 'default' else None, meter)
log_set = LogSet(
interface, args.slog if args.slog != "default" else None, meter
)
if args.power_stress:
stress = PowerStress(interface)
stress.run()
closeNow = True # exit immediately after stress test
if args.listen:
closeNow = False
@@ -889,13 +934,17 @@ def onConnected(interface):
interface.getNode(args.dest, False).iface.waitForAckNak()
if args.wait_to_disconnect:
print(f"Waiting {args.wait_to_disconnect} seconds before disconnecting" )
print(f"Waiting {args.wait_to_disconnect} seconds before disconnecting")
time.sleep(int(args.wait_to_disconnect))
# if the user didn't ask for serial debugging output, we might want to exit after we've done our operation
if (not args.seriallog) and closeNow:
interface.close() # after running command then exit
# Close any structured logs after we've done all of our API operations
if log_set:
log_set.close()
except Exception as ex:
print(f"Aborting due to: {ex}")
interface.close() # close the connection now, so that our app exits
@@ -998,29 +1047,41 @@ def export_config(interface):
print(config)
return config
def create_power_meter():
"""Setup the power meter."""
global meter # pylint: disable=global-statement
args = mt_config.args
# If the user specified a voltage, make sure it is valid
v = 0.0
if args.power_voltage:
v = float(args.power_voltage)
if v < 0.8 or v > 5.0:
meshtastic.util.our_exit("Voltage must be between 0.8 and 5.0")
if args.power_riden:
meter = RidenPowerSupply(args.power_riden)
elif args.power_ppk2_supply or args.power_ppk2_meter:
meter = PPK2PowerSupply()
assert v > 0, "Voltage must be specified for PPK2"
meter.v = v # PPK2 requires setting voltage before selecting supply mode
meter.setIsSupply(args.power_ppk2_supply)
elif args.power_sim:
meter = SimPowerSupply()
if meter and args.power_voltage:
v = float(args.power_voltage)
if v < 0.5 or v >5.0:
meshtastic.util.our_exit("Voltage must be between 1.0 and 5.0")
if meter and v:
logging.info(f"Setting power supply to {v} volts")
meter.v = v
meter.powerOn()
if args.power_wait:
input("Powered on, press enter to continue...")
else:
logging.info("Powered-on, waiting for device to boot")
time.sleep(5)
def common():
"""Shared code for all of our command line wrappers."""
@@ -1088,20 +1149,29 @@ def common():
print(f"Found: name='{x.name}' address='{x.address}'")
meshtastic.util.our_exit("BLE scan finished", 0)
elif args.ble:
client = BLEInterface(args.ble if args.ble != "any" else None, debugOut=logfile, noProto=args.noproto, noNodes=args.no_nodes)
client = BLEInterface(
args.ble if args.ble != "any" else None,
debugOut=logfile,
noProto=args.noproto,
noNodes=args.no_nodes,
)
elif args.host:
try:
client = meshtastic.tcp_interface.TCPInterface(
args.host, debugOut=logfile, noProto=args.noproto, noNodes=args.no_nodes
args.host,
debugOut=logfile,
noProto=args.noproto,
noNodes=args.no_nodes,
)
except Exception as ex:
meshtastic.util.our_exit(
f"Error connecting to {args.host}:{ex}", 1
)
meshtastic.util.our_exit(f"Error connecting to {args.host}:{ex}", 1)
else:
try:
client = meshtastic.serial_interface.SerialInterface(
args.port, debugOut=logfile, noProto=args.noproto, noNodes=args.no_nodes
args.port,
debugOut=logfile,
noProto=args.noproto,
noNodes=args.no_nodes,
)
except PermissionError as ex:
username = os.getlogin()
@@ -1116,7 +1186,10 @@ def common():
if client.devPath is None:
try:
client = meshtastic.tcp_interface.TCPInterface(
"localhost", debugOut=logfile, noProto=args.noproto, noNodes=args.no_nodes
"localhost",
debugOut=logfile,
noProto=args.noproto,
noNodes=args.no_nodes,
)
except Exception as ex:
meshtastic.util.our_exit(
@@ -1128,7 +1201,10 @@ def common():
have_tunnel = platform.system() == "Linux"
if (
args.noproto or args.reply or (have_tunnel and args.tunnel) or args.listen
args.noproto
or args.reply
or (have_tunnel and args.tunnel)
or args.listen
): # loop until someone presses ctrlc
try:
while True:
@@ -1139,13 +1215,19 @@ def common():
# don't call exit, background threads might be running still
# sys.exit(0)
def addConnectionArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentParser:
"""Add connection specifiation arguments"""
outer = parser.add_argument_group('Connection', 'Optional arguments that specify how to connect to a Meshtastic device.')
outer = parser.add_argument_group(
"Connection",
"Optional arguments that specify how to connect to a Meshtastic device.",
)
group = outer.add_mutually_exclusive_group()
group.add_argument(
"--port", "--serial", "-s",
"--port",
"--serial",
"-s",
help="The port of the device to connect to using serial, e.g. /dev/ttyUSB0. (defaults to trying to detect a port)",
nargs="?",
const=None,
@@ -1153,19 +1235,22 @@ def addConnectionArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentParse
)
group.add_argument(
"--host", "--tcp", "-t",
"--host",
"--tcp",
"-t",
help="Connect to a device using TCP, optionally passing hostname or IP address to use. (defaults to '%(const)s')",
nargs="?",
default=None,
const="localhost"
const="localhost",
)
group.add_argument(
"--ble", "-b",
"--ble",
"-b",
help="Connect to a BLE device, optionally specifying a device name (defaults to '%(const)s')",
nargs="?",
default=None,
const="any"
const="any",
)
return parser
@@ -1177,9 +1262,11 @@ def initParser():
args = mt_config.args
# The "Help" group includes the help option and other informational stuff about the CLI itself
outerHelpGroup = parser.add_argument_group('Help')
outerHelpGroup = parser.add_argument_group("Help")
helpGroup = outerHelpGroup.add_mutually_exclusive_group()
helpGroup.add_argument("-h", "--help", action="help", help="show this help message and exit")
helpGroup.add_argument(
"-h", "--help", action="help", help="show this help message and exit"
)
the_version = get_active_version()
helpGroup.add_argument("--version", action="version", version=f"{the_version}")
@@ -1216,9 +1303,9 @@ def initParser():
group.add_argument(
"--seriallog",
help="Log device serial output to either 'none' or a filename to append to. Defaults to 'stdout' if no filename specified.",
nargs='?',
nargs="?",
const="stdout",
default=None
default=None,
)
group.add_argument(
@@ -1474,7 +1561,7 @@ def initParser():
group.add_argument(
"--remove-node",
help="Tell the destination node to remove a specific node from its DB, by node number or ID"
help="Tell the destination node to remove a specific node from its DB, by node number or ID",
)
group.add_argument(
"--reset-nodedb",
@@ -1542,7 +1629,9 @@ def initParser():
action="store_true",
)
power_group = parser.add_argument_group('Power Testing', 'Options for power testing/logging.')
power_group = parser.add_argument_group(
"Power Testing", "Options for power testing/logging."
)
power_supply_group = power_group.add_mutually_exclusive_group()
@@ -1591,7 +1680,7 @@ def initParser():
help="Store structured-logs (slogs) for this run, optionally you can specifiy a destination directory",
nargs="?",
default=None,
const="default"
const="default",
)
group.add_argument(
@@ -1620,7 +1709,9 @@ def initParser():
action="store_true",
)
remoteHardwareArgs = parser.add_argument_group('Remote Hardware', 'Arguments related to the Remote Hardware module')
remoteHardwareArgs = parser.add_argument_group(
"Remote Hardware", "Arguments related to the Remote Hardware module"
)
remoteHardwareArgs.add_argument(
"--gpio-wrb", nargs=2, help="Set a particular GPIO # to 1 or 0", action="append"
@@ -1634,10 +1725,11 @@ def initParser():
"--gpio-watch", help="Start watching a GPIO mask for changes (ex: '0x10')"
)
have_tunnel = platform.system() == "Linux"
if have_tunnel:
tunnelArgs = parser.add_argument_group('Tunnel', 'Arguments related to establishing a tunnel device over the mesh.')
tunnelArgs = parser.add_argument_group(
"Tunnel", "Arguments related to establishing a tunnel device over the mesh."
)
tunnelArgs.add_argument(
"--tunnel",
action="store_true",
@@ -1652,7 +1744,6 @@ def initParser():
parser.set_defaults(deprecated=None)
args = parser.parse_args()
mt_config.args = args
mt_config.parser = parser
@@ -1663,7 +1754,8 @@ def main():
parser = argparse.ArgumentParser(
add_help=False,
epilog="If no connection arguments are specified, we search for a compatible serial device, "
"and if none is found, then attempt a TCP connection to localhost.")
"and if none is found, then attempt a TCP connection to localhost.",
)
mt_config.parser = parser
initParser()
common()

View File

@@ -0,0 +1,206 @@
"""Post-run analysis tools for meshtastic."""
import argparse
import logging
from typing import cast
import dash_bootstrap_components as dbc # type: ignore[import-untyped]
import numpy as np
import pandas as pd
import plotly.express as px # type: ignore[import-untyped]
import plotly.graph_objects as go # type: ignore[import-untyped]
import pyarrow as pa
from dash import Dash, dcc, html # type: ignore[import-untyped]
from pyarrow import feather
from .. import mesh_pb2, powermon_pb2
from ..slog import root_dir
# Configure panda options
pd.options.mode.copy_on_write = True
def to_pmon_names(arr) -> list[str]:
"""Convert the power monitor state numbers to their corresponding names.
arr (list): List of power monitor state numbers.
Returns the List of corresponding power monitor state names.
"""
def to_pmon_name(n):
try:
s = powermon_pb2.PowerMon.State.Name(int(n))
return s if s != "None" else None
except ValueError:
return None
return [to_pmon_name(x) for x in arr]
def read_pandas(filepath: str) -> pd.DataFrame:
"""Read a feather file and convert it to a pandas DataFrame.
filepath (str): Path to the feather file.
Returns the pandas DataFrame.
"""
# per https://arrow.apache.org/docs/python/pandas.html#reducing-memory-use-in-table-to-pandas
# use this to get nullable int fields treated as ints rather than floats in pandas
dtype_mapping = {
pa.int8(): pd.Int8Dtype(),
pa.int16(): pd.Int16Dtype(),
pa.int32(): pd.Int32Dtype(),
pa.int64(): pd.Int64Dtype(),
pa.uint8(): pd.UInt8Dtype(),
pa.uint16(): pd.UInt16Dtype(),
pa.uint32(): pd.UInt32Dtype(),
pa.uint64(): pd.UInt64Dtype(),
pa.bool_(): pd.BooleanDtype(),
pa.float32(): pd.Float32Dtype(),
pa.float64(): pd.Float64Dtype(),
pa.string(): pd.StringDtype(),
}
return cast(pd.DataFrame, feather.read_table(filepath).to_pandas(types_mapper=dtype_mapping.get)) # type: ignore[arg-type]
def get_pmon_raises(dslog: pd.DataFrame) -> pd.DataFrame:
"""Get the power monitor raises from the slog DataFrame.
dslog (pd.DataFrame): The slog DataFrame.
Returns the DataFrame containing the power monitor raises.
"""
pmon_events = dslog[dslog["pm_mask"].notnull()]
pm_masks = pd.Series(pmon_events["pm_mask"]).to_numpy()
# possible to do this with pandas rolling windows if I was smarter?
pm_changes = [
(pm_masks[i - 1] ^ x if i != 0 else x) for i, x in enumerate(pm_masks)
]
pm_raises = [(pm_masks[i] & x) for i, x in enumerate(pm_changes)]
pm_falls = [(~pm_masks[i] & x if i != 0 else 0) for i, x in enumerate(pm_changes)]
pmon_events["pm_raises"] = to_pmon_names(pm_raises)
pmon_events["pm_falls"] = to_pmon_names(pm_falls)
pmon_raises = pmon_events[pmon_events["pm_raises"].notnull()][["time", "pm_raises"]]
pmon_falls = pmon_events[pmon_events["pm_falls"].notnull()]
# pylint: disable=unused-variable
def get_endtime(row):
"""Find the corresponding fall event."""
following = pmon_falls[
(pmon_falls["pm_falls"] == row["pm_raises"])
& (pmon_falls["time"] > row["time"])
]
return following.iloc[0] if not following.empty else None
# HMM - setting end_time doesn't work yet - leave off for now
# pmon_raises['end_time'] = pmon_raises.apply(get_endtime, axis=1)
return pmon_raises
def get_board_info(dslog: pd.DataFrame) -> tuple:
"""Get the board information from the slog DataFrame.
dslog (pd.DataFrame): The slog DataFrame.
Returns a tuple containing the board ID and software version.
"""
board_info = dslog[dslog["sw_version"].notnull()]
sw_version = board_info.iloc[0]["sw_version"]
board_id = mesh_pb2.HardwareModel.Name(board_info.iloc[0]["board_id"])
return (board_id, sw_version)
def create_argparser() -> argparse.ArgumentParser:
"""Create the argument parser for the script."""
parser = argparse.ArgumentParser(description="Meshtastic power analysis tools")
group = parser
group.add_argument(
"--slog",
help="Specify the structured-logs directory (defaults to latest log directory)",
)
group.add_argument(
"--no-server",
action="store_true",
help="Exit immediately, without running the visualization web server",
)
return parser
def create_dash(slog_path: str) -> Dash:
"""Create a Dash application for visualizing power consumption data.
slog_path (str): Path to the slog directory.
Returns the Dash application.
"""
app = Dash(external_stylesheets=[dbc.themes.BOOTSTRAP])
dpwr = read_pandas(f"{slog_path}/power.feather")
dslog = read_pandas(f"{slog_path}/slog.feather")
pmon_raises = get_pmon_raises(dslog)
def set_legend(f, name):
f["data"][0]["showlegend"] = True
f["data"][0]["name"] = name
return f
avg_pwr_lines = px.line(dpwr, x="time", y="average_mW").update_traces(
line_color="red"
)
set_legend(avg_pwr_lines, "avg power")
max_pwr_points = px.scatter(dpwr, x="time", y="max_mW").update_traces(
marker_color="blue"
)
set_legend(max_pwr_points, "max power")
min_pwr_points = px.scatter(dpwr, x="time", y="min_mW").update_traces(
marker_color="green"
)
set_legend(min_pwr_points, "min power")
fake_y = np.full(len(pmon_raises), 10.0)
pmon_points = px.scatter(pmon_raises, x="time", y=fake_y, text="pm_raises")
fig = go.Figure(data=max_pwr_points.data + avg_pwr_lines.data + pmon_points.data)
fig.update_layout(
legend={"yanchor": "top", "y": 0.99, "xanchor": "left", "x": 0.01}
)
# App layout
app.layout = [
html.Div(children="Meshtastic power analysis tool testing..."),
dcc.Graph(figure=fig),
]
return app
def main():
"""Entry point of the script."""
parser = create_argparser()
args = parser.parse_args()
if not args.slog:
args.slog = f"{root_dir()}/latest"
app = create_dash(slog_path=args.slog)
port = 8051
logging.info(f"Running Dash visualization of {args.slog} (publicly accessible)")
if not args.no_server:
app.run_server(debug=True, host="0.0.0.0", port=port)
else:
logging.info("Exiting without running visualization server")
if __name__ == "__main__":
main()

View File

@@ -8,16 +8,14 @@ import time
from threading import Thread
from typing import List, Optional
import google.protobuf
from bleak import BleakClient, BleakScanner, BLEDevice
from bleak.exc import BleakDBusError, BleakError
import google.protobuf
from meshtastic.mesh_interface import MeshInterface
from .protobuf import (
mesh_pb2,
)
from .protobuf import mesh_pb2
SERVICE_UUID = "6ba1b218-15a8-461f-9fa8-5dcae273eafd"
TORADIO_UUID = "f75c76d2-129e-4dad-a1dd-7866124401e7"
FROMRADIO_UUID = "2c55e69e-4993-11ed-b878-0242ac120002"
@@ -53,16 +51,19 @@ class BLEInterface(MeshInterface):
self._receiveThread.start()
logging.debug("Threads running")
self.client: Optional[BLEClient] = None
try:
logging.debug(f"BLE connecting to: {address if address else 'any'}")
self.client: Optional[BLEClient] = self.connect(address)
self.client = self.connect(address)
logging.debug("BLE connected")
except BLEInterface.BLEError as e:
self.close()
raise e
if self.client.has_characteristic(LEGACY_LOGRADIO_UUID):
self.client.start_notify(LEGACY_LOGRADIO_UUID, self.legacy_log_radio_handler)
self.client.start_notify(
LEGACY_LOGRADIO_UUID, self.legacy_log_radio_handler
)
if self.client.has_characteristic(LOGRADIO_UUID):
self.client.start_notify(LOGRADIO_UUID, self.log_radio_handler)
@@ -93,11 +94,15 @@ class BLEInterface(MeshInterface):
log_record = mesh_pb2.LogRecord()
try:
log_record.ParseFromString(bytes(b))
except google.protobuf.message.DecodeError:
return
message = f'[{log_record.source}] {log_record.message}' if log_record.source else log_record.message
self._handleLogLine(message)
message = (
f"[{log_record.source}] {log_record.message}"
if log_record.source
else log_record.message
)
self._handleLogLine(message)
except google.protobuf.message.DecodeError:
logging.warning("Malformed LogRecord received. Skipping.")
async def legacy_log_radio_handler(self, _, b): # pylint: disable=C0116
log_radio = b.decode("utf-8").replace("\n", "")
@@ -191,7 +196,7 @@ class BLEInterface(MeshInterface):
def _sendToRadioImpl(self, toRadio):
b = toRadio.SerializeToString()
if b:
if b and self.client: # we silently ignore writes while we are shutting down
logging.debug(f"TORADIO write: {b.hex()}")
try:
self.client.write_gatt_char(
@@ -207,7 +212,6 @@ class BLEInterface(MeshInterface):
self.should_read = True
def close(self):
atexit.unregister(self._exit_handler)
try:
MeshInterface.close(self)
except Exception as e:
@@ -215,10 +219,14 @@ class BLEInterface(MeshInterface):
if self._want_receive:
self.want_receive = False # Tell the thread we want it to stop
self._receiveThread.join(timeout=2) # If bleak is hung, don't wait for the thread to exit (it is critical we disconnect)
self._receiveThread = None
if self._receiveThread:
self._receiveThread.join(
timeout=2
) # If bleak is hung, don't wait for the thread to exit (it is critical we disconnect)
self._receiveThread = None
if self.client:
atexit.unregister(self._exit_handler)
self.client.disconnect()
self.client.close()
self.client = None

View File

@@ -10,21 +10,14 @@ import threading
import time
from datetime import datetime
from decimal import Decimal
from typing import Any, Callable, Dict, List, Optional, Union
import google.protobuf.json_format
from pubsub import pub # type: ignore[import-untyped]
from tabulate import tabulate
import print_color # type: ignore[import-untyped]
from pubsub import pub # type: ignore[import-untyped]
from tabulate import tabulate
import meshtastic.node
from meshtastic.protobuf import (
mesh_pb2,
portnums_pb2,
telemetry_pb2,
)
from meshtastic import (
BROADCAST_ADDR,
BROADCAST_NUM,
@@ -32,18 +25,20 @@ from meshtastic import (
NODELESS_WANT_CONFIG_ID,
ResponseHandler,
protocols,
publishingThread
publishingThread,
)
from meshtastic.protobuf import mesh_pb2, portnums_pb2, telemetry_pb2
from meshtastic.util import (
Acknowledgment,
Timeout,
convert_mac_addr,
message_to_json,
our_exit,
remove_keys_from_dict,
stripnl,
message_to_json,
)
def _timeago(delta_secs: int) -> str:
"""Convert a number of seconds in the past into a short, friendly string
e.g. "now", "30 sec ago", "1 hour ago"
@@ -67,7 +62,7 @@ def _timeago(delta_secs: int) -> str:
return "now"
class MeshInterface: # pylint: disable=R0902
class MeshInterface: # pylint: disable=R0902
"""Interface class for meshtastic devices
Properties:
@@ -79,11 +74,14 @@ class MeshInterface: # pylint: disable=R0902
class MeshInterfaceError(Exception):
"""An exception class for general mesh interface errors"""
def __init__(self, message):
self.message = message
super().__init__(self.message)
def __init__(self, debugOut=None, noProto: bool=False, noNodes: bool=False) -> None:
def __init__(
self, debugOut=None, noProto: bool = False, noNodes: bool = False
) -> None:
"""Constructor
Keyword Arguments:
@@ -93,13 +91,21 @@ class MeshInterface: # pylint: disable=R0902
on startup, just other configuration information.
"""
self.debugOut = debugOut
self.nodes: Optional[Dict[str,Dict]] = None # FIXME
self.nodes: Optional[Dict[str, Dict]] = None # FIXME
self.isConnected: threading.Event = threading.Event()
self.noProto: bool = noProto
self.localNode: meshtastic.node.Node = meshtastic.node.Node(self, -1) # We fixup nodenum later
self.myInfo: Optional[mesh_pb2.MyNodeInfo] = None # We don't have device info yet
self.metadata: Optional[mesh_pb2.DeviceMetadata] = None # We don't have device metadata yet
self.responseHandlers: Dict[int,ResponseHandler] = {} # A map from request ID to the handler
self.localNode: meshtastic.node.Node = meshtastic.node.Node(
self, -1
) # We fixup nodenum later
self.myInfo: Optional[
mesh_pb2.MyNodeInfo
] = None # We don't have device info yet
self.metadata: Optional[
mesh_pb2.DeviceMetadata
] = None # We don't have device metadata yet
self.responseHandlers: Dict[
int, ResponseHandler
] = {} # A map from request ID to the handler
self.failure = (
None # If we've encountered a fatal exception it will be kept here
)
@@ -162,6 +168,12 @@ class MeshInterface: # pylint: disable=R0902
def _handleLogLine(self, line: str) -> None:
"""Handle a line of log output from the device."""
# Devices should _not_ be including a newline at the end of each log-line str (especially when
# encapsulated as a LogRecord). But to cope with old device loads, we check for that and fix it here:
if line.endswith("\n"):
line = line[:-1]
pub.sendMessage("meshtastic.log.line", line=line, interface=self)
def _handleLogRecord(self, record: mesh_pb2.LogRecord) -> None:
@@ -201,7 +213,9 @@ class MeshInterface: # pylint: disable=R0902
print(infos)
return infos
def showNodes(self, includeSelf: bool=True, file=sys.stdout) -> str: # pylint: disable=W0613
def showNodes(
self, includeSelf: bool = True
) -> str: # pylint: disable=W0613
"""Show table summary of nodes in mesh"""
def formatFloat(value, precision=2, unit="") -> Optional[str]:
@@ -232,7 +246,11 @@ class MeshInterface: # pylint: disable=R0902
continue
presumptive_id = f"!{node['num']:08x}"
row = {"N": 0, "User": f"Meshtastic {presumptive_id[-4:]}", "ID": presumptive_id}
row = {
"N": 0,
"User": f"Meshtastic {presumptive_id[-4:]}",
"ID": presumptive_id,
}
user = node.get("user")
if user:
@@ -241,7 +259,7 @@ class MeshInterface: # pylint: disable=R0902
"User": user.get("longName", "N/A"),
"AKA": user.get("shortName", "N/A"),
"ID": user["id"],
"Hardware": user.get("hwModel", "UNSET")
"Hardware": user.get("hwModel", "UNSET"),
}
)
@@ -295,7 +313,9 @@ class MeshInterface: # pylint: disable=R0902
print(table)
return table
def getNode(self, nodeId: str, requestChannels: bool=True) -> meshtastic.node.Node:
def getNode(
self, nodeId: str, requestChannels: bool = True
) -> meshtastic.node.Node:
"""Return a node object which contains device settings and channel info"""
if nodeId in (LOCAL_ADDR, BROADCAST_ADDR):
return self.localNode
@@ -312,11 +332,11 @@ class MeshInterface: # pylint: disable=R0902
def sendText(
self,
text: str,
destinationId: Union[int, str]=BROADCAST_ADDR,
wantAck: bool=False,
wantResponse: bool=False,
onResponse: Optional[Callable[[dict], Any]]=None,
channelIndex: int=0,
destinationId: Union[int, str] = BROADCAST_ADDR,
wantAck: bool = False,
wantResponse: bool = False,
onResponse: Optional[Callable[[dict], Any]] = None,
channelIndex: int = 0,
):
"""Send a utf8 string to some other node, if the node has a display it
will also be shown on the device.
@@ -419,14 +439,14 @@ class MeshInterface: # pylint: disable=R0902
def sendPosition(
self,
latitude: float=0.0,
longitude: float=0.0,
altitude: int=0,
timeSec: int=0,
destinationId: Union[int, str]=BROADCAST_ADDR,
wantAck: bool=False,
wantResponse: bool=False,
channelIndex: int=0,
latitude: float = 0.0,
longitude: float = 0.0,
altitude: int = 0,
timeSec: int = 0,
destinationId: Union[int, str] = BROADCAST_ADDR,
wantAck: bool = False,
wantResponse: bool = False,
channelIndex: int = 0,
):
"""
Send a position packet to some other node (normally a broadcast)
@@ -477,20 +497,22 @@ class MeshInterface: # pylint: disable=R0902
def onResponsePosition(self, p):
"""on response for position"""
if p["decoded"]["portnum"] == 'POSITION_APP':
if p["decoded"]["portnum"] == "POSITION_APP":
self._acknowledgment.receivedPosition = True
position = mesh_pb2.Position()
position.ParseFromString(p["decoded"]["payload"])
ret = "Position received: "
if position.latitude_i != 0 and position.longitude_i != 0:
ret += f"({position.latitude_i * 10**-7}, {position.longitude_i * 10**-7})"
ret += (
f"({position.latitude_i * 10**-7}, {position.longitude_i * 10**-7})"
)
else:
ret += "(unknown)"
if position.altitude != 0:
ret += f" {position.altitude}m"
if position.precision_bits not in [0,32]:
if position.precision_bits not in [0, 32]:
ret += f" precision:{position.precision_bits}"
elif position.precision_bits == 32:
ret += " full precision"
@@ -499,11 +521,15 @@ class MeshInterface: # pylint: disable=R0902
print(ret)
elif p["decoded"]["portnum"] == 'ROUTING_APP':
if p["decoded"]["routing"]["errorReason"] == 'NO_RESPONSE':
our_exit("No response from node. At least firmware 2.1.22 is required on the destination node.")
elif p["decoded"]["portnum"] == "ROUTING_APP":
if p["decoded"]["routing"]["errorReason"] == "NO_RESPONSE":
our_exit(
"No response from node. At least firmware 2.1.22 is required on the destination node."
)
def sendTraceRoute(self, dest: Union[int, str], hopLimit: int, channelIndex: int=0):
def sendTraceRoute(
self, dest: Union[int, str], hopLimit: int, channelIndex: int = 0
):
"""Send the trace route"""
r = mesh_pb2.RouteDiscovery()
self.sendData(
@@ -535,12 +561,19 @@ class MeshInterface: # pylint: disable=R0902
self._acknowledgment.receivedTraceRoute = True
def sendTelemetry(self, destinationId: Union[int,str]=BROADCAST_ADDR, wantResponse: bool=False, channelIndex: int=0):
def sendTelemetry(
self,
destinationId: Union[int, str] = BROADCAST_ADDR,
wantResponse: bool = False,
channelIndex: int = 0,
):
"""Send telemetry and optionally ask for a response"""
r = telemetry_pb2.Telemetry()
if self.nodes is not None:
node = next(n for n in self.nodes.values() if n["num"] == self.localNode.nodeNum)
node = next(
n for n in self.nodes.values() if n["num"] == self.localNode.nodeNum
)
if node is not None:
metrics = node.get("deviceMetrics")
if metrics:
@@ -578,7 +611,7 @@ class MeshInterface: # pylint: disable=R0902
def onResponseTelemetry(self, p: dict):
"""on response for telemetry"""
if p["decoded"]["portnum"] == 'TELEMETRY_APP':
if p["decoded"]["portnum"] == "TELEMETRY_APP":
self._acknowledgment.receivedTelemetry = True
telemetry = telemetry_pb2.Telemetry()
telemetry.ParseFromString(p["decoded"]["payload"])
@@ -593,16 +626,27 @@ class MeshInterface: # pylint: disable=R0902
f"Total channel utilization: {telemetry.device_metrics.channel_utilization:.2f}%"
)
if telemetry.device_metrics.air_util_tx is not None:
print(f"Transmit air utilization: {telemetry.device_metrics.air_util_tx:.2f}%")
print(
f"Transmit air utilization: {telemetry.device_metrics.air_util_tx:.2f}%"
)
if telemetry.device_metrics.uptime_seconds is not None:
print(f"Uptime: {telemetry.device_metrics.uptime_seconds} s")
elif p["decoded"]["portnum"] == 'ROUTING_APP':
if p["decoded"]["routing"]["errorReason"] == 'NO_RESPONSE':
our_exit("No response from node. At least firmware 2.1.22 is required on the destination node.")
elif p["decoded"]["portnum"] == "ROUTING_APP":
if p["decoded"]["routing"]["errorReason"] == "NO_RESPONSE":
our_exit(
"No response from node. At least firmware 2.1.22 is required on the destination node."
)
def _addResponseHandler(self, requestId: int, callback: Callable[[dict], Any], ackPermitted: bool=False):
self.responseHandlers[requestId] = ResponseHandler(callback=callback, ackPermitted=ackPermitted)
def _addResponseHandler(
self,
requestId: int,
callback: Callable[[dict], Any],
ackPermitted: bool = False,
):
self.responseHandlers[requestId] = ResponseHandler(
callback=callback, ackPermitted=ackPermitted
)
def _sendPacket(
self,
@@ -680,13 +724,17 @@ class MeshInterface: # pylint: disable=R0902
and self.localNode.waitForConfig()
)
if not success:
raise MeshInterface.MeshInterfaceError("Timed out waiting for interface config")
raise MeshInterface.MeshInterfaceError(
"Timed out waiting for interface config"
)
def waitForAckNak(self):
"""Wait for the ack/nak"""
success = self._timeout.waitForAckNak(self._acknowledgment)
if not success:
raise MeshInterface.MeshInterfaceError("Timed out waiting for an acknowledgment")
raise MeshInterface.MeshInterfaceError(
"Timed out waiting for an acknowledgment"
)
def waitForTraceRoute(self, waitFactor):
"""Wait for trace route"""
@@ -739,7 +787,9 @@ class MeshInterface: # pylint: disable=R0902
and raise an exception"""
if not self.noProto:
if not self.isConnected.wait(timeout): # timeout after x seconds
raise MeshInterface.MeshInterfaceError("Timed out waiting for connection completion")
raise MeshInterface.MeshInterfaceError(
"Timed out waiting for connection completion"
)
# If we failed while connecting, raise the connection to the client
if self.failure:
@@ -748,7 +798,9 @@ class MeshInterface: # pylint: disable=R0902
def _generatePacketId(self) -> int:
"""Get a new unique packet ID"""
if self.currentPacketId is None:
raise MeshInterface.MeshInterfaceError("Not connected yet, can not generate packet")
raise MeshInterface.MeshInterfaceError(
"Not connected yet, can not generate packet"
)
else:
self.currentPacketId = (self.currentPacketId + 1) & 0xFFFFFFFF
return self.currentPacketId
@@ -795,7 +847,9 @@ class MeshInterface: # pylint: disable=R0902
self.myInfo = None
self.nodes = {} # nodes keyed by ID
self.nodesByNum = {} # nodes keyed by nodenum
self._localChannels = [] # empty until we start getting channels pushed from the device (during config)
self._localChannels = (
[]
) # empty until we start getting channels pushed from the device (during config)
startConfig = mesh_pb2.ToRadio()
if self.configId is None or not self.noNodes:
@@ -944,7 +998,7 @@ class MeshInterface: # pylint: disable=R0902
logging.debug("Node without position")
# no longer necessary since we're mutating directly in nodesByNum via _getOrCreateByNum
#self.nodesByNum[node["num"]] = node
# self.nodesByNum[node["num"]] = node
if "user" in node: # Some nodes might not have user/ids assigned yet
if "id" in node["user"]:
self.nodes[node["user"]["id"]] = node
@@ -970,14 +1024,18 @@ class MeshInterface: # pylint: disable=R0902
elif fromRadio.HasField("mqttClientProxyMessage"):
publishingThread.queueWork(
lambda: pub.sendMessage(
"meshtastic.mqttclientproxymessage", proxymessage=fromRadio.mqttClientProxyMessage, interface=self
"meshtastic.mqttclientproxymessage",
proxymessage=fromRadio.mqttClientProxyMessage,
interface=self,
)
)
elif fromRadio.HasField("xmodemPacket"):
publishingThread.queueWork(
lambda: pub.sendMessage(
"meshtastic.xmodempacket", packet=fromRadio.xmodemPacket, interface=self
"meshtastic.xmodempacket",
packet=fromRadio.xmodemPacket,
interface=self,
)
)
@@ -1084,7 +1142,7 @@ class MeshInterface: # pylint: disable=R0902
return BROADCAST_ADDR
try:
return self.nodesByNum[num]["user"]["id"] #type: ignore[index]
return self.nodesByNum[num]["user"]["id"] # type: ignore[index]
except:
logging.debug(f"Node {num} not found for fromId")
return None
@@ -1092,7 +1150,9 @@ class MeshInterface: # pylint: disable=R0902
def _getOrCreateByNum(self, nodeNum):
"""Given a nodenum find the NodeInfo in the DB (or create if necessary)"""
if nodeNum == BROADCAST_NUM:
raise MeshInterface.MeshInterfaceError("Can not create/find nodenum by the broadcast num")
raise MeshInterface.MeshInterfaceError(
"Can not create/find nodenum by the broadcast num"
)
if nodeNum in self.nodesByNum:
return self.nodesByNum[nodeNum]
@@ -1104,9 +1164,9 @@ class MeshInterface: # pylint: disable=R0902
"id": presumptive_id,
"longName": f"Meshtastic {presumptive_id[-4:]}",
"shortName": f"{presumptive_id[-4:]}",
"hwModel": "UNSET"
}
} # Create a minimal node db entry
"hwModel": "UNSET",
},
} # Create a minimal node db entry
self.nodesByNum[nodeNum] = n
return n
@@ -1215,13 +1275,21 @@ class MeshInterface: # pylint: disable=R0902
# or the handler is set as ackPermitted, but send NAKs and
# other, data-containing responses to the handlers
routing = decoded.get("routing")
isAck = routing is not None and ("errorReason" not in routing or routing["errorReason"] == "NONE")
isAck = routing is not None and (
"errorReason" not in routing or routing["errorReason"] == "NONE"
)
# we keep the responseHandler in dict until we actually call it
handler = self.responseHandlers.get(requestId, None)
if handler is not None:
if (not isAck) or handler.callback.__name__ == "onAckNak" or handler.ackPermitted:
if (
(not isAck)
or handler.callback.__name__ == "onAckNak"
or handler.ackPermitted
):
handler = self.responseHandlers.pop(requestId, None)
logging.debug(f"Calling response handler for requestId {requestId}")
logging.debug(
f"Calling response handler for requestId {requestId}"
)
handler.callback(asDict)
logging.debug(f"Publishing {topic}: packet={stripnl(asDict)} ")

View File

@@ -9,6 +9,7 @@ from ppk2_api import ppk2_api # type: ignore[import-untyped]
from .power_supply import PowerError, PowerSupply
class PPK2PowerSupply(PowerSupply):
"""Interface for talking with the NRF PPK2 high-resolution micro-power supply.
Power Profiler Kit II is what you should google to find it for purchase.
@@ -35,12 +36,21 @@ class PPK2PowerSupply(PowerSupply):
self.current_min = 0
self.current_sum = 0
self.current_num_samples = 0
self.current_average = 0
# for tracking avera data read length (to determine if we are sleeping efficiently in measurement_loop)
self.total_data_len = 0
self.num_data_reads = 0
self.max_data_len = 0
# Normally we just sleep with a timeout on this condition (polling the power measurement data repeatedly)
# but any time our measurements have been fully consumed (via reset_measurements) we notify() this condition
# to trigger a new reading ASAP.
self._want_measurement = threading.Condition()
# To guard against a brief window while updating measured values
self._result_lock = threading.Condition()
self.r = r = ppk2_api.PPK2_API(
portName
) # serial port will be different for you
@@ -55,52 +65,63 @@ class PPK2PowerSupply(PowerSupply):
def measurement_loop(self):
"""Endless measurement loop will run in a thread."""
while self.measuring:
# always reads 4096 bytes, even if there is no new samples - or possibly the python single thread (because of global interpreter lock)
# is always behind and thefore we are inherently dropping samples semi randomly!!!
read_data = self.r.get_data()
if read_data != b"":
samples, _ = self.r.get_samples(read_data)
with self._want_measurement:
self._want_measurement.wait(
0.0001 if self.num_data_reads == 0 else 0.001
)
# normally we poll using this timeout, but sometimes
# reset_measurement() will notify us to read immediately
# update invariants
if len(samples) > 0:
if self.current_num_samples == 0:
self.current_min = samples[
0
] # we need at least one sample to get an initial min
self.current_max = max(self.current_max, max(samples))
self.current_min = min(self.current_min, min(samples))
self.current_sum += sum(samples)
self.current_num_samples += len(samples)
# logging.debug(f"PPK2 data_len={len(read_data)}, sample_len={len(samples)}")
# always reads 4096 bytes, even if there is no new samples - or possibly the python single thread (because of global interpreter lock)
# is always behind and thefore we are inherently dropping samples semi randomly!!!
read_data = self.r.get_data()
if read_data != b"":
samples, _ = self.r.get_samples(read_data)
self.num_data_reads += 1
self.total_data_len += len(read_data)
self.max_data_len = max(self.max_data_len, len(read_data))
# update invariants
if len(samples) > 0:
if self.current_num_samples == 0:
# First set of new reads, reset min/max
self.current_max = 0
self.current_min = samples[0]
# we need at least one sample to get an initial min
time.sleep(0.01) # FIXME figure out correct sleep duration
# The following operations could be expensive, so do outside of the lock
# FIXME - change all these lists into numpy arrays to use lots less CPU
self.current_max = max(self.current_max, max(samples))
self.current_min = min(self.current_min, min(samples))
latest_sum = sum(samples)
with self._result_lock:
self.current_sum += latest_sum
self.current_num_samples += len(samples)
# logging.debug(f"PPK2 data_len={len(read_data)}, sample_len={len(samples)}")
self.num_data_reads += 1
self.total_data_len += len(read_data)
self.max_data_len = max(self.max_data_len, len(read_data))
def get_min_current_mA(self):
"""Returns max current in mA (since last call to this method)."""
"""Return the min current in mA."""
return self.current_min / 1000
def get_max_current_mA(self):
"""Returns max current in mA (since last call to this method)."""
"""Return the max current in mA."""
return self.current_max / 1000
def get_average_current_mA(self):
"""Returns average current in mA (since last call to this method)."""
if self.current_num_samples == 0:
return 0
else:
return (
self.current_sum / self.current_num_samples / 1000
) # measurements are in microamperes, divide by 1000
"""Return the average current in mA."""
with self._result_lock:
if self.current_num_samples != 0:
# If we have new samples, calculate a new average
self.current_average = self.current_sum / self.current_num_samples
# Even if we don't have new samples, return the last calculated average
# measurements are in microamperes, divide by 1000
return self.current_average / 1000
def reset_measurements(self):
"""Reset current measurements."""
# Use the last reading as the new only reading (to ensure we always have a valid current reading)
self.current_max = 0
self.current_min = 0
self.current_sum = 0
self.current_num_samples = 0
@@ -111,6 +132,9 @@ class PPK2PowerSupply(PowerSupply):
self.total_data_len = 0
self.max_data_len = 0
with self._want_measurement:
self._want_measurement.notify() # notify the measurement loop to read immediately
def close(self) -> None:
"""Close the power meter."""
self.measuring = False
@@ -118,9 +142,11 @@ class PPK2PowerSupply(PowerSupply):
self.measurement_thread.join() # wait for our thread to finish
super().close()
def setIsSupply(self, s: bool):
def setIsSupply(self, is_supply: bool):
"""If in supply mode we will provide power ourself, otherwise we are just an amp meter."""
assert self.v > 0.8 # We must set a valid voltage before calling this method
self.r.set_source_voltage(
int(self.v * 1000)
) # set source voltage in mV BEFORE setting source mode
@@ -130,7 +156,7 @@ class PPK2PowerSupply(PowerSupply):
self.r.start_measuring() # send command to ppk2
if (
not s
not is_supply
): # min power outpuf of PPK2. If less than this assume we want just meter mode.
self.r.use_ampere_meter()
else:

View File

@@ -3,7 +3,7 @@
import logging
import time
from ..protobuf import ( portnums_pb2, powermon_pb2 )
from ..protobuf import portnums_pb2, powermon_pb2
def onPowerStressResponse(packet, interface):
@@ -53,6 +53,35 @@ class PowerStressClient:
onResponseAckPermitted=True,
)
def syncPowerStress(
self,
cmd: powermon_pb2.PowerStressMessage.Opcode.ValueType,
num_seconds: float = 0.0,
):
"""Send a power stress command and wait for the ack."""
gotAck = False
def onResponse(packet: dict): # pylint: disable=unused-argument
nonlocal gotAck
gotAck = True
logging.info(
f"Sending power stress command {powermon_pb2.PowerStressMessage.Opcode.Name(cmd)}"
)
self.sendPowerStress(cmd, onResponse=onResponse, num_seconds=num_seconds)
if num_seconds == 0.0:
# Wait for the response and then continue
while not gotAck:
time.sleep(0.1)
else:
# we wait a little bit longer than the time the UUT would be waiting (to make sure all of its messages are handled first)
time.sleep(
num_seconds + 0.2
) # completely block our thread for the duration of the test
if not gotAck:
logging.error("Did not receive ack for power stress command!")
class PowerStress:
"""Walk the UUT through a set of power states so we can capture repeatable power consumption measurements."""
@@ -62,20 +91,27 @@ class PowerStress:
def run(self):
"""Run the power stress test."""
# Send the power stress command
gotAck = False
try:
self.client.syncPowerStress(powermon_pb2.PowerStressMessage.PRINT_INFO)
def onResponse(packet: dict): # pylint: disable=unused-argument
nonlocal gotAck
gotAck = True
num_seconds = 5.0
states = [
powermon_pb2.PowerStressMessage.LED_ON,
powermon_pb2.PowerStressMessage.LED_OFF,
powermon_pb2.PowerStressMessage.BT_OFF,
powermon_pb2.PowerStressMessage.BT_ON,
powermon_pb2.PowerStressMessage.CPU_FULLON,
powermon_pb2.PowerStressMessage.CPU_IDLE,
# FIXME - can't test deepsleep yet because the ttyACM device disappears. Fix the python code to retry connections
# powermon_pb2.PowerStressMessage.CPU_DEEPSLEEP,
]
for s in states:
s_name = powermon_pb2.PowerStressMessage.Opcode.Name(s)
logging.info(
f"Running power stress test {s_name} for {num_seconds} seconds"
)
self.client.syncPowerStress(s, num_seconds)
logging.info("Starting power stress test, attempting to contact UUT...")
self.client.sendPowerStress(
powermon_pb2.PowerStressMessage.PRINT_INFO, onResponse=onResponse
)
# Wait for the response
while not gotAck:
time.sleep(0.1)
logging.info("Power stress test complete.")
logging.info("Power stress test complete.")
except KeyboardInterrupt as e:
logging.warning(f"Power stress interrupted: {e}")

View File

@@ -67,9 +67,10 @@ class SerialInterface(StreamInterface):
def close(self):
"""Close a connection to the device"""
self.stream.flush()
time.sleep(0.1)
self.stream.flush()
time.sleep(0.1)
if self.stream: # Stream can be null if we were already closed
self.stream.flush() # FIXME: why are there these two flushes with 100ms sleeps? This shouldn't be necessary
time.sleep(0.1)
self.stream.flush()
time.sleep(0.1)
logging.debug("Closing Serial stream")
StreamInterface.close(self)

View File

@@ -1,3 +1,3 @@
"""Structured logging framework (see dev docs for more info)."""
from .slog import LogSet
from .slog import LogSet, root_dir

View File

@@ -1,7 +1,9 @@
"""Utilities for Apache Arrow serialization."""
import logging
import threading
import os
from typing import Optional
import pyarrow as pa
from pyarrow import feather
@@ -19,34 +21,49 @@ class ArrowWriter:
"""
self.sink = pa.OSFile(file_name, "wb") # type: ignore
self.new_rows: list[dict] = []
self.schema: pa.Schema | None = None # haven't yet learned the schema
self.writer: pa.RecordBatchFileWriter | None = None
self.schema: Optional[pa.Schema] = None # haven't yet learned the schema
self.writer: Optional[pa.RecordBatchStreamWriter] = None
self._lock = threading.Condition() # Ensure only one thread writes at a time
def close(self):
"""Close the stream and writes the file as needed."""
self._write()
if self.writer:
self.writer.close()
self.sink.close()
with self._lock:
self._write()
if self.writer:
self.writer.close()
self.sink.close()
def set_schema(self, schema: pa.Schema):
"""Set the schema for the file.
Only needed for datasets where we can't learn it from the first record written.
schema (pa.Schema): The schema to use.
"""
with self._lock:
assert self.schema is None
self.schema = schema
self.writer = pa.ipc.new_stream(self.sink, schema)
def _write(self):
"""Write the new rows to the file."""
if len(self.new_rows) > 0:
if self.schema is None:
# only need to look at the first row to learn the schema
self.schema = pa.Table.from_pylist([self.new_rows[0]]).schema
self.writer = pa.ipc.new_stream(self.sink, self.schema)
self.set_schema(pa.Table.from_pylist([self.new_rows[0]]).schema)
self.writer.write_batch(pa.RecordBatch.from_pylist(self.new_rows))
self.writer.write_batch(
pa.RecordBatch.from_pylist(self.new_rows, schema=self.schema)
)
self.new_rows = []
def add_row(self, row_dict: dict):
"""Add a row to the arrow file.
We will automatically learn the schema from the first row. But all rows must use that schema.
"""
self.new_rows.append(row_dict)
if len(self.new_rows) >= chunk_size:
self._write()
with self._lock:
self.new_rows.append(row_dict)
if len(self.new_rows) >= chunk_size:
self._write()
class FeatherWriter(ArrowWriter):

View File

@@ -9,10 +9,12 @@ import threading
import time
from dataclasses import dataclass
from datetime import datetime
from functools import reduce
from typing import Optional
import parse # type: ignore[import-untyped]
import platformdirs
import pyarrow as pa
from pubsub import pub # type: ignore[import-untyped]
from meshtastic.mesh_interface import MeshInterface
@@ -21,29 +23,57 @@ from meshtastic.powermon import PowerMeter
from .arrow import FeatherWriter
def root_dir() -> str:
"""Return the root directory for slog files."""
app_name = "meshtastic"
app_author = "meshtastic"
app_dir = platformdirs.user_data_dir(app_name, app_author)
dir_name = f"{app_dir}/slogs"
os.makedirs(dir_name, exist_ok=True)
return dir_name
@dataclass(init=False)
class LogDef:
"""Log definition."""
code: str # i.e. PM or B or whatever... see meshtastic slog documentation
fields: list[tuple[str, pa.DataType]] # A list of field names and their arrow types
format: parse.Parser # A format string that can be used to parse the arguments
def __init__(self, code: str, fmt: str) -> None:
def __init__(self, code: str, fields: list[tuple[str, pa.DataType]]) -> None:
"""Initialize the LogDef object.
code (str): The code.
format (str): The format.
"""
self.code = code
self.format = parse.compile(fmt)
self.fields = fields
fmt = ""
for idx, f in enumerate(fields):
if idx != 0:
fmt += ","
# make the format string
suffix = (
"" if f[1] == pa.string() else ":d"
) # treat as a string or an int (the only types we have so far)
fmt += "{" + f[0] + suffix + "}"
self.format = parse.compile(
fmt
) # We include a catchall matcher at the end - to ignore stuff we don't understand
"""A dictionary mapping from logdef code to logdef"""
log_defs = {
d.code: d
for d in [
LogDef("B", "{boardid:d},{version}"),
LogDef("PM", "{bitmask:d},{reason}"),
LogDef("B", [("board_id", pa.uint32()), ("sw_version", pa.string())]),
LogDef("PM", [("pm_mask", pa.uint64()), ("pm_reason", pa.string())]),
LogDef("PS", [("ps_state", pa.uint32())]),
]
}
log_regex = re.compile(".*S:([0-9A-Za-z]+):(.*)")
@@ -52,7 +82,7 @@ log_regex = re.compile(".*S:([0-9A-Za-z]+):(.*)")
class PowerLogger:
"""Logs current watts reading periodically using PowerMeter and ArrowWriter."""
def __init__(self, pMeter: PowerMeter, file_path: str, interval=0.2) -> None:
def __init__(self, pMeter: PowerMeter, file_path: str, interval=0.002) -> None:
"""Initialize the PowerLogger object."""
self.pMeter = pMeter
self.writer = FeatherWriter(file_path)
@@ -63,17 +93,23 @@ class PowerLogger:
)
self.thread.start()
def store_current_reading(self, now: datetime | None = None) -> None:
"""Store current power measurement."""
if now is None:
now = datetime.now()
d = {
"time": now,
"average_mW": self.pMeter.get_average_current_mA(),
"max_mW": self.pMeter.get_max_current_mA(),
"min_mW": self.pMeter.get_min_current_mA(),
}
self.pMeter.reset_measurements()
self.writer.add_row(d)
def _logging_thread(self) -> None:
"""Background thread for logging the current watts reading."""
while self.is_logging:
d = {
"time": datetime.now(),
"average_mW": self.pMeter.get_average_current_mA(),
"max_mW": self.pMeter.get_max_current_mA(),
"min_mW": self.pMeter.get_min_current_mA(),
}
self.pMeter.reset_measurements()
self.writer.add_row(d)
self.store_current_reading()
time.sleep(self.interval)
def close(self) -> None:
@@ -93,13 +129,38 @@ class StructuredLogger:
"""Sniffs device logs for structured log messages, extracts those into apache arrow format.
Also writes the raw log messages to raw.txt"""
def __init__(self, client: MeshInterface, dir_path: str) -> None:
def __init__(
self,
client: MeshInterface,
dir_path: str,
power_logger: PowerLogger | None = None,
include_raw=True,
) -> None:
"""Initialize the StructuredLogger object.
client (MeshInterface): The MeshInterface object to monitor.
"""
self.client = client
self.power_logger = power_logger
# Setup the arrow writer (and its schema)
self.writer = FeatherWriter(f"{dir_path}/slog")
all_fields = reduce(
(lambda x, y: x + y), map(lambda x: x.fields, log_defs.values())
)
self.include_raw = include_raw
if self.include_raw:
all_fields.append(("raw", pa.string()))
# Use timestamp as the first column
all_fields.insert(0, ("time", pa.timestamp("us")))
# pass in our name->type tuples a pa.fields
self.writer.set_schema(
pa.schema(map(lambda x: pa.field(x[0], x[1]), all_fields))
)
self.raw_file: Optional[
io.TextIOWrapper
] = open( # pylint: disable=consider-using-with
@@ -110,11 +171,14 @@ class StructuredLogger:
def listen_glue(line, interface): # pylint: disable=unused-argument
self._onLogMessage(line)
self.listener = pub.subscribe(listen_glue, TOPIC_MESHTASTIC_LOG_LINE)
self._listen_glue = (
listen_glue # we must save this so it doesn't get garbage collected
)
self._listener = pub.subscribe(listen_glue, TOPIC_MESHTASTIC_LOG_LINE)
def close(self) -> None:
"""Stop logging."""
pub.unsubscribe(self.listener, TOPIC_MESHTASTIC_LOG_LINE)
pub.unsubscribe(self._listener, TOPIC_MESHTASTIC_LOG_LINE)
self.writer.close()
f = self.raw_file
self.raw_file = None # mark that we are shutting down
@@ -126,26 +190,51 @@ class StructuredLogger:
line (str): the line of log output
"""
di = {} # the dictionary of the fields we found to log
m = log_regex.match(line)
if m:
src = m.group(1)
args = m.group(2)
args += " " # append a space so that if the last arg is an empty str it will still be accepted as a match
logging.debug(f"SLog {src}, reason: {args}")
if src != "PM":
logging.warning(f"Not yet handling structured log {src} (FIXME)")
else:
d = log_defs.get(src)
if d:
r = d.format.parse(args) # get the values with the correct types
if r:
di = r.named
di["time"] = datetime.now()
self.writer.add_row(di)
else:
logging.warning(f"Failed to parse slog {line} with {d.format}")
logging.debug(f"SLog {src}, args: {args}")
d = log_defs.get(src)
if d:
last_field = d.fields[-1]
last_is_str = last_field[1] == pa.string()
if last_is_str:
args += " "
# append a space so that if the last arg is an empty str
# it will still be accepted as a match for a str
r = d.format.parse(args) # get the values with the correct types
if r:
di = r.named
if last_is_str:
di[last_field[0]] = di[
last_field[0]
].strip() # remove the trailing space we added
if di[last_field[0]] == "":
# If the last field is an empty string, remove it
del di[last_field[0]]
else:
logging.warning(f"Unknown Structured Log: {line}")
logging.warning(f"Failed to parse slog {line} with {d.format}")
else:
logging.warning(f"Unknown Structured Log: {line}")
# Store our structured log record
if di or self.include_raw:
now = datetime.now()
di["time"] = now
if self.include_raw:
di["raw"] = line
self.writer.add_row(di)
# If we have a sibling power logger, make sure we have a power measurement with the EXACT same timestamp
if self.power_logger:
self.power_logger.store_current_reading(now)
if self.raw_file:
self.raw_file.write(line + "\n") # Write the raw log
@@ -166,31 +255,30 @@ class LogSet:
"""
if not dir_name:
app_name = "meshtastic"
app_author = "meshtastic"
app_dir = platformdirs.user_data_dir(app_name, app_author)
dir_name = f"{app_dir}/slogs/{datetime.now().strftime('%Y%m%d-%H%M%S')}"
app_dir = root_dir()
dir_name = f"{app_dir}/{datetime.now().strftime('%Y%m%d-%H%M%S')}"
os.makedirs(dir_name, exist_ok=True)
# Also make a 'latest' directory that always points to the most recent logs
# symlink might fail on some platforms, if it does fail silently
if os.path.exists(f"{app_dir}/slogs/latest"):
os.unlink(f"{app_dir}/slogs/latest")
os.symlink(dir_name, f"{app_dir}/slogs/latest", target_is_directory=True)
if os.path.exists(f"{app_dir}/latest"):
os.unlink(f"{app_dir}/latest")
os.symlink(dir_name, f"{app_dir}/latest", target_is_directory=True)
self.dir_name = dir_name
logging.info(f"Writing slogs to {dir_name}")
self.slog_logger: Optional[StructuredLogger] = StructuredLogger(
client, self.dir_name
)
self.power_logger: Optional[PowerLogger] = (
None
if not power_meter
else PowerLogger(power_meter, f"{self.dir_name}/power")
)
self.slog_logger: Optional[StructuredLogger] = StructuredLogger(
client, self.dir_name, power_logger=self.power_logger
)
# Store a lambda so we can find it again to unregister
self.atexit_handler = lambda: self.close() # pylint: disable=unnecessary-lambda

View File

Binary file not shown.

View File

@@ -0,0 +1,349 @@
[RadioIf] getFromRadio=STATE_SEND_PACKETS
[RadioIf] Can not send yet, busyRx
Telling client we have new packets 3
BLE notify fromNum
[Blink] S:PM:0x000009c8,
[Blink] S:PM:0x00000948,
toRadioWriteCb data 0x2001ffea, len 26
PACKET FROM PHONE (id=0x8f26f64c fr=0x00 to=0x46, WantAck=1, HopLim=3 Ch=0x0 Portnum=74 WANTRESP)
[RadioIf] Ignore false preamble detection.
[RadioIf] S:PM:0x00000940,
[RadioIf] Starting low level send (id=0x8f26f64b fr=0x46 to=0xff, WantAck=0, HopLim=3 Ch=0x8 encrypted hopStart=3 priority=64)
[RadioIf] S:PM:0x00000950,
[RadioIf] (bw=250, sf=11, cr=4/5) packet symLen=8 ms, payloadSize=25, time 419 ms
[RadioIf] AirTime - Packet transmitted : 419ms
Telling client we have new packets 4
BLE notify fromNum
[Router] Add packet record (id=0x8f26f64c fr=0x00 to=0x46, WantAck=1, HopLim=3 Ch=0x0 Portnum=74 WANTRESP rxtime=1720725846)
[Router] handleReceived(REMOTE) (id=0x8f26f64c fr=0x00 to=0x46, WantAck=1, HopLim=3 Ch=0x0 Portnum=74 WANTRESP rxtime=1720725846)
[Router] Module 'powerstress' wantsPacket=1
[Router] Received powerstress from=0x0, id=0x8f26f64c, portnum=74, payloadlen=2
[Router] Received PowerStress cmd=1
[Router] S:B:9,2.3.15.177d19ac
[Router] Asked module 'powerstress' to send a response
[Router] Module 'powerstress' handled and skipped other processing
[Router] No one responded, send a nak
[Router] Alloc an err=8,to=0x67f63246,idFrom=0x8f26f64c,id=0x5fa26660
[Router] Enqueued local (id=0x5fa26660 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f64c rxtime=1720725847 priority=120)
[Router] Rx someone rebroadcasting for us (id=0x5fa26660 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f64c rxtime=1720725847 priority=120)
[Router] didn't find pending packet
[Router] Add packet record (id=0x5fa26660 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f64c rxtime=1720725847 priority=120)
[Router] handleReceived(REMOTE) (id=0x5fa26660 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f64c rxtime=1720725847 priority=120)
[Router] Module 'canned' wantsPacket=1
[Router] showing standard frames
[Router] Showing 0 module frames
[Router] Total frame count: 103
[Router] Added modules. numframes: 0
[Router] Finished building frames. numframes: 7
[Router] Module 'canned' considered
[Router] Module 'routing' wantsPacket=1
[Router] Received routing from=0x67f63246, id=0x5fa26660, portnum=5, payloadlen=2
[Router] Routing sniffing (id=0x5fa26660 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f64c rxtime=1720725847 priority=120)
[Router] Received a nak for 0x8f26f64c, stopping retransmissions
[Router] Delivering rx packet (id=0x5fa26660 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f64c rxtime=1720725847 priority=120)
[Router] Update DB node 0x67f63246, rx_time=1720725847
[Router] Forwarding to phone (id=0x5fa26660 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f64c rxtime=1720725847 priority=120)
[Router] Module 'routing' considered
[RadioIf] Completed sending (id=0x8f26f64b fr=0x46 to=0xff, WantAck=0, HopLim=3 Ch=0x8 encrypted hopStart=3 priority=64)
[RadioIf] S:PM:0x00000940,
[RadioIf] S:PM:0x00000948,
Telling client we have new packets 5
BLE notify fromNum
[RadioIf] S:PM:0x00000940,
[RadioIf] Starting low level send (id=0x5fa2665f fr=0x46 to=0xff, WantAck=0, HopLim=3 Ch=0x8 encrypted hopStart=3 priority=10)
[RadioIf] S:PM:0x00000950,
[RadioIf] (bw=250, sf=11, cr=4/5) packet symLen=8 ms, payloadSize=66, time 722 ms
[RadioIf] AirTime - Packet transmitted : 722ms
[Blink] S:PM:0x000009d0,
[Blink] S:PM:0x00000950,
getFromRadio=STATE_SEND_PACKETS
getFromRadio=STATE_SEND_PACKETS
phone downloaded packet (id=0x5fa26660 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f64c rxtime=1720725847 priority=120)
toRadioWriteCb data 0x2001ffea, len 31
PACKET FROM PHONE (id=0x8f26f64d fr=0x00 to=0x46, WantAck=1, HopLim=3 Ch=0x0 Portnum=74 WANTRESP)
Enqueued local (id=0x8f26f64d fr=0x00 to=0x46, WantAck=1, HopLim=3 Ch=0x0 Portnum=74 WANTRESP rxtime=1720725848)
Telling client we have new packets 6
BLE notify fromNum
[Router] Add packet record (id=0x8f26f64d fr=0x00 to=0x46, WantAck=1, HopLim=3 Ch=0x0 Portnum=74 WANTRESP rxtime=1720725848)
[Router] handleReceived(REMOTE) (id=0x8f26f64d fr=0x00 to=0x46, WantAck=1, HopLim=3 Ch=0x0 Portnum=74 WANTRESP rxtime=1720725848)
[Router] Module 'powerstress' wantsPacket=1
[Router] Received powerstress from=0x0, id=0x8f26f64d, portnum=74, payloadlen=7
[Router] Received PowerStress cmd=48
[Router] Asked module 'powerstress' to send a response
[Router] Module 'powerstress' handled and skipped other processing
[Router] No one responded, send a nak
[Router] Alloc an err=8,to=0x67f63246,idFrom=0x8f26f64d,id=0x5fa26661
[Router] Enqueued local (id=0x5fa26661 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f64d rxtime=1720725848 priority=120)
[Router] Rx someone rebroadcasting for us (id=0x5fa26661 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f64d rxtime=1720725848 priority=120)
[Router] didn't find pending packet
[Router] Add packet record (id=0x5fa26661 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f64d rxtime=1720725848 priority=120)
[Router] handleReceived(REMOTE) (id=0x5fa26661 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f64d rxtime=1720725848 priority=120)
[Router] Module 'canned' wantsPacket=1
[Router] Showing 0 module frames
[Router] Total frame count: 103
[Router] Added modules. numframes: 0
[Router] Finished building frames. numframes: 7
[Router] Module 'canned' considered
[Router] Module 'routing' wantsPacket=1
[Router] Received routing from=0x67f63246, id=0x5fa26661, portnum=5, payloadlen=2
[Router] Routing sniffing (id=0x5fa26661 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f64d rxtime=1720725848 priority=120)
[Router] Received a nak for 0x8f26f64d, stopping retransmissions
[Router] Delivering rx packet (id=0x5fa26661 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f64d rxtime=1720725848 priority=120)
[Router] Update DB node 0x67f63246, rx_time=1720725848
[Router] Forwarding to phone (id=0x5fa26661 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f64d rxtime=1720725848 priority=120)
[Router] Module 'routing' considered
[PowerStressModule] S:PS:48
[PowerStressModule] S:PM:0x000009d0,
[RadioIf] Completed sending (id=0x5fa2665f fr=0x46 to=0xff, WantAck=0, HopLim=3 Ch=0x8 encrypted hopStart=3 priority=10)
[RadioIf] S:PM:0x000009c0,
[RadioIf] S:PM:0x000009c8,
Telling client we have new packets 7
BLE notify fromNum
getFromRadio=STATE_SEND_PACKETS
phone downloaded packet (id=0x5fa26661 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f64d rxtime=1720725848 priority=120)
toRadioWriteCb data 0x2001ffea, len 31
PACKET FROM PHONE (id=0x8f26f64e fr=0x00 to=0x46, WantAck=1, HopLim=3 Ch=0x0 Portnum=74 WANTRESP)
Enqueued local (id=0x8f26f64e fr=0x00 to=0x46, WantAck=1, HopLim=3 Ch=0x0 Portnum=74 WANTRESP rxtime=1720725853)
Telling client we have new packets 8
[Router] Add packet record (id=0x8f26f64e fr=0x00 to=0x46, WantAck=1, HopLim=3 Ch=0x0 Portnum=74 WANTRESP rxtime=1720725853)
[Router] handleReceived(REMOTE) (id=0x8f26f64e fr=0x00 to=0x46, WantAck=1, HopLim=3 Ch=0x0 Portnum=74 WANTRESP rxtime=1720725853)
[Router] Module 'powerstress' wantsPacket=1
[Router] Received powerstress from=0x0, id=0x8f26f64e, portnum=74, payloadlen=7
[Router] Received PowerStress cmd=49
[Router] PowerStress operation 48 already in progress! Can't start new command
[Router] Asked module 'powerstress' to send a response
[Router] Module 'powerstress' handled and skipped other processing
[Router] No one responded, send a nak
[Router] Alloc an err=8,to=0x67f63246,idFrom=0x8f26f64e,id=0x5fa26662
[Router] Enqueued local (id=0x5fa26662 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f64e rxtime=1720725853 priority=120)
[Router] Rx someone rebroadcasting for us (id=0x5fa26662 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f64e rxtime=1720725853 priority=120)
[Router] didn't find pending packet
[Router] Add packet record (id=0x5fa26662 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f64e rxtime=1720725853 priority=120)
[Router] handleReceived(REMOTE) (id=0x5fa26662 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f64e rxtime=1720725853 priority=120)
[Router] Module 'canned' wantsPacket=1
[Router] showing standard frames
[Router] Showing 0 module frames
[Router] Total frame count: 103
[Router] Added modules. numframes: 0
[Router] Finished building frames. numframes: 7
[Router] Module 'canned' considered
[Router] Module 'routing' wantsPacket=1
[Router] Received routing from=0x67f63246, id=0x5fa26662, portnum=5, payloadlen=2
[Router] Routing sniffing (id=0x5fa26662 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f64e rxtime=1720725853 priority=120)
[Router] Received a nak for 0x8f26f64e, stopping retransmissions
[Router] Delivering rx packet (id=0x5fa26662 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f64e rxtime=1720725853 priority=120)
[Router] Update DB node 0x67f63246, rx_time=1720725853
[Router] Forwarding to phone (id=0x5fa26662 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f64e rxtime=1720725853 priority=120)
[Router] Module 'routing' considered
[PowerStressModule] S:PS:0
Telling client we have new packets 9
BLE notify fromNum
[Power] Battery: usbPower=0, isCharging=0, batMv=3191, batPct=4
getFromRadio=STATE_SEND_PACKETS
phone downloaded packet (id=0x5fa26662 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f64e rxtime=1720725853 priority=120)
toRadioWriteCb data 0x2001ffea, len 31
PACKET FROM PHONE (id=0x8f26f64f fr=0x00 to=0x46, WantAck=1, HopLim=3 Ch=0x0 Portnum=74 WANTRESP)
Enqueued local (id=0x8f26f64f fr=0x00 to=0x46, WantAck=1, HopLim=3 Ch=0x0 Portnum=74 WANTRESP rxtime=1720725858)
Telling client we have new packets 10
BLE notify fromNum
[Router] Add packet record (id=0x8f26f64f fr=0x00 to=0x46, WantAck=1, HopLim=3 Ch=0x0 Portnum=74 WANTRESP rxtime=1720725858)
[Router] handleReceived(REMOTE) (id=0x8f26f64f fr=0x00 to=0x46, WantAck=1, HopLim=3 Ch=0x0 Portnum=74 WANTRESP rxtime=1720725858)
[Router] Module 'powerstress' wantsPacket=1
[Router] Received powerstress from=0x0, id=0x8f26f64f, portnum=74, payloadlen=7
[Router] Received PowerStress cmd=80
[Router] Asked module 'powerstress' to send a response
[Router] Module 'powerstress' handled and skipped other processing
[Router] No one responded, send a nak
[Router] Alloc an err=8,to=0x67f63246,idFrom=0x8f26f64f,id=0x5fa26663
[Router] Enqueued local (id=0x5fa26663 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f64f rxtime=1720725858 priority=120)
[Router] Rx someone rebroadcasting for us (id=0x5fa26663 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f64f rxtime=1720725858 priority=120)
[Router] didn't find pending packet
[Router] Add packet record (id=0x5fa26663 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f64f rxtime=1720725858 priority=120)
[Router] handleReceived(REMOTE) (id=0x5fa26663 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f64f rxtime=1720725858 priority=120)
[Router] Module 'canned' wantsPacket=1
[Router] showing standard frames
[Router] Showing 0 module frames
[Router] Added modules. numframes: 0
[Router] Finished building frames. numframes: 7
[Router] Module 'canned' considered
[Router] Module 'routing' wantsPacket=1
[Router] Received routing from=0x67f63246, id=0x5fa26663, portnum=5, payloadlen=2
[Router] Routing sniffing (id=0x5fa26663 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f64f rxtime=1720725858 priority=120)
[Router] Received a nak for 0x8f26f64f, stopping retransmissions
[Router] Delivering rx packet (id=0x5fa26663 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f64f rxtime=1720725858 priority=120)
[Router] Update DB node 0x67f63246, rx_time=1720725858
[Router] Forwarding to phone (id=0x5fa26663 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f64f rxtime=1720725858 priority=120)
[Router] Module 'routing' considered
[PowerStressModule] S:PS:80
[PowerStressModule] S:PM:0x00000988,
[PowerStressModule] Disable NRF52 bluetooth
Telling client we have new packets 11
BLE notify fromNum
[DeviceTelemetryModule] (Sending): air_util_tx=0.031694, channel_utilization=1.901667, battery_level=4, voltage=3.191000, uptime=50
[DeviceTelemetryModule] updateTelemetry LOCAL
[DeviceTelemetryModule] Node status update: 1 online, 82 total
[DeviceTelemetryModule] Sending packet to mesh
[DeviceTelemetryModule] Update DB node 0x67f63246, rx_time=1720725859
[DeviceTelemetryModule] handleReceived(LOCAL) (id=0x5fa26664 fr=0x46 to=0xff, WantAck=0, HopLim=3 Ch=0x0 Portnum=67 rxtime=1720725859 priority=10)
[DeviceTelemetryModule] No modules interested in portnum=67, src=LOCAL
[DeviceTelemetryModule] localSend to channel 0
[DeviceTelemetryModule] Add packet record (id=0x5fa26664 fr=0x46 to=0xff, WantAck=0, HopLim=3 Ch=0x0 Portnum=67 rxtime=1720725859 priority=10)
[DeviceTelemetryModule] Expanding short PSK #1
[DeviceTelemetryModule] Using AES128 key!
[DeviceTelemetryModule] nRF52 encrypt fr=67f63246, num=5fa26664, numBytes=30!
[DeviceTelemetryModule] enqueuing for send (id=0x5fa26664 fr=0x46 to=0xff, WantAck=0, HopLim=3 Ch=0x8 encrypted rxtime=1720725859 hopStart=3 priority=10)
[DeviceTelemetryModule] txGood=2,rxGood=0,rxBad=0
[DeviceTelemetryModule] Using channel 0 (hash 0x8)
[DeviceTelemetryModule] Expanding short PSK #1
[DeviceTelemetryModule] Using AES128 key!
[DeviceTelemetryModule] nRF52 encrypt fr=67f63246, num=5fa26664, numBytes=30!
[DeviceTelemetryModule] decoded message (id=0x5fa26664 fr=0x46 to=0xff, WantAck=0, HopLim=3 Ch=0x0 Portnum=67 rxtime=1720725859 hopStart=3 priority=10)
Telling client we have new packets 13
BLE notify fromNum
[RadioIf] Can not send yet, busyRx
[RadioIf] Can not send yet, busyRx
[RadioIf] Can not send yet, busyRx
[RadioIf] Can not send yet, busyRx
getFromRadio=STATE_SEND_PACKETS
[RadioIf] Can not send yet, busyRx
[RadioIf] Can not send yet, busyRx
[RadioIf] Can not send yet, busyRx
[RadioIf] Can not send yet, busyRx
[RadioIf] Can not send yet, busyRx
getFromRadio=STATE_SEND_PACKETS
phone downloaded packet (id=0x5fa26663 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f64f rxtime=1720725858 priority=120)
[RadioIf] Ignore false preamble detection.
[RadioIf] S:PM:0x00000980,
[RadioIf] Starting low level send (id=0x5fa26664 fr=0x46 to=0xff, WantAck=0, HopLim=3 Ch=0x8 encrypted rxtime=1720725859 hopStart=3 priority=10)
[RadioIf] S:PM:0x00000990,
[RadioIf] (bw=250, sf=11, cr=4/5) packet symLen=8 ms, payloadSize=46, time 575 ms
[RadioIf] AirTime - Packet transmitted : 575ms
getFromRadio=STATE_SEND_PACKETS
phone downloaded packet (id=0x5fa26664 fr=0x46 to=0xff, WantAck=0, HopLim=3 Ch=0x0 Portnum=67 rxtime=1720725859 hopStart=3 priority=10)
[RadioIf] Completed sending (id=0x5fa26664 fr=0x46 to=0xff, WantAck=0, HopLim=3 Ch=0x8 encrypted rxtime=1720725859 hopStart=3 priority=10)
[RadioIf] S:PM:0x00000980,
[RadioIf] S:PM:0x00000988,
toRadioWriteCb data 0x2001ffea, len 31
PACKET FROM PHONE (id=0x8f26f650 fr=0x00 to=0x46, WantAck=1, HopLim=3 Ch=0x0 Portnum=74 WANTRESP)
Enqueued local (id=0x8f26f650 fr=0x00 to=0x46, WantAck=1, HopLim=3 Ch=0x0 Portnum=74 WANTRESP rxtime=1720725864)
Telling client we have new packets 14
BLE notify fromNum
[Router] Add packet record (id=0x8f26f650 fr=0x00 to=0x46, WantAck=1, HopLim=3 Ch=0x0 Portnum=74 WANTRESP rxtime=1720725864)
[Router] handleReceived(REMOTE) (id=0x8f26f650 fr=0x00 to=0x46, WantAck=1, HopLim=3 Ch=0x0 Portnum=74 WANTRESP rxtime=1720725864)
[Router] Module 'powerstress' wantsPacket=1
[Router] Received powerstress from=0x0, id=0x8f26f650, portnum=74, payloadlen=7
[Router] Received PowerStress cmd=81
[Router] PowerStress operation 80 already in progress! Can't start new command
[Router] Asked module 'powerstress' to send a response
[Router] Module 'powerstress' handled and skipped other processing
[Router] No one responded, send a nak
[Router] Alloc an err=8,to=0x67f63246,idFrom=0x8f26f650,id=0x5fa26665
[Router] Enqueued local (id=0x5fa26665 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f650 rxtime=1720725864 priority=120)
[Router] Rx someone rebroadcasting for us (id=0x5fa26665 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f650 rxtime=1720725864 priority=120)
[Router] didn't find pending packet
[Router] Add packet record (id=0x5fa26665 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f650 rxtime=1720725864 priority=120)
[Router] handleReceived(REMOTE) (id=0x5fa26665 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f650 rxtime=1720725864 priority=120)
[Router] Module 'canned' wantsPacket=1
[Router] showing standard frames
[Router] Showing 0 module frames
[Router] Total frame count: 103
[Router] Added modules. numframes: 0
[Router] Finished building frames. numframes: 7
[Router] Module 'canned' considered
[Router] Module 'routing' wantsPacket=1
[Router] Received routing from=0x67f63246, id=0x5fa26665, portnum=5, payloadlen=2
[Router] Routing sniffing (id=0x5fa26665 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f650 rxtime=1720725864 priority=120)
[Router] Received a nak for 0x8f26f650, stopping retransmissions
[Router] Delivering rx packet (id=0x5fa26665 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f650 rxtime=1720725864 priority=120)
[Router] Update DB node 0x67f63246, rx_time=1720725864
[Router] Forwarding to phone (id=0x5fa26665 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f650 rxtime=1720725864 priority=120)
[Router] Module 'routing' considered
Telling client we have new packets 15
BLE notify fromNum
getFromRadio=STATE_SEND_PACKETS
[PowerStressModule] S:PS:0
getFromRadio=STATE_SEND_PACKETS
phone downloaded packet (id=0x5fa26665 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f650 rxtime=1720725864 priority=120)
toRadioWriteCb data 0x2001ffea, len 31
PACKET FROM PHONE (id=0x8f26f651 fr=0x00 to=0x46, WantAck=1, HopLim=3 Ch=0x0 Portnum=74 WANTRESP)
Enqueued local (id=0x8f26f651 fr=0x00 to=0x46, WantAck=1, HopLim=3 Ch=0x0 Portnum=74 WANTRESP rxtime=1720725869)
Telling client we have new packets 16
BLE notify fromNum
[Router] Add packet record (id=0x8f26f651 fr=0x00 to=0x46, WantAck=1, HopLim=3 Ch=0x0 Portnum=74 WANTRESP rxtime=1720725869)
[Router] handleReceived(REMOTE) (id=0x8f26f651 fr=0x00 to=0x46, WantAck=1, HopLim=3 Ch=0x0 Portnum=74 WANTRESP rxtime=1720725869)
[Router] Module 'powerstress' wantsPacket=1
[Router] Received powerstress from=0x0, id=0x8f26f651, portnum=74, payloadlen=7
[Router] Received PowerStress cmd=34
[Router] Asked module 'powerstress' to send a response
[Router] Module 'powerstress' handled and skipped other processing
[Router] No one responded, send a nak
[Router] Alloc an err=8,to=0x67f63246,idFrom=0x8f26f651,id=0x5fa26666
[Router] Enqueued local (id=0x5fa26666 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f651 rxtime=1720725869 priority=120)
[Router] Rx someone rebroadcasting for us (id=0x5fa26666 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f651 rxtime=1720725869 priority=120)
[Router] didn't find pending packet
[Router] Add packet record (id=0x5fa26666 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f651 rxtime=1720725869 priority=120)
[Router] handleReceived(REMOTE) (id=0x5fa26666 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f651 rxtime=1720725869 priority=120)
[Router] Module 'canned' wantsPacket=1
[Router] showing standard frames
[Router] Showing 0 module frames
[Router] Total frame count: 103
[Router] Added modules. numframes: 0
[Router] Finished building frames. numframes: 7
[Router] Module 'canned' considered
[Router] Module 'routing' wantsPacket=1
[Router] Received routing from=0x67f63246, id=0x5fa26666, portnum=5, payloadlen=2
[Router] Routing sniffing (id=0x5fa26666 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f651 rxtime=1720725869 priority=120)
[Router] Received a nak for 0x8f26f651, stopping retransmissions
[Router] Delivering rx packet (id=0x5fa26666 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f651 rxtime=1720725869 priority=120)
[Router] Update DB node 0x67f63246, rx_time=1720725869
[Router] Forwarding to phone (id=0x5fa26666 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f651 rxtime=1720725869 priority=120)
[Router] Module 'routing' considered
[PowerStressModule] S:PS:34
[PowerStressModule] getFromRadio=STATE_SEND_PACKETS
[PowerStressModule] getFromRadio=STATE_SEND_PACKETS
[PowerStressModule] phone downloaded packet (id=0x5fa26666 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f651 rxtime=1720725869 priority=120)
Telling client we have new packets 17
BLE notify fromNum
[Power] Battery: usbPower=0, isCharging=0, batMv=3202, batPct=5
[PowerStressModule] S:PS:0
toRadioWriteCb data 0x2001ffea, len 31
PACKET FROM PHONE (id=0x8f26f652 fr=0x00 to=0x46, WantAck=1, HopLim=3 Ch=0x0 Portnum=74 WANTRESP)
Enqueued local (id=0x8f26f652 fr=0x00 to=0x46, WantAck=1, HopLim=3 Ch=0x0 Portnum=74 WANTRESP rxtime=1720725874)
Telling client we have new packets 18
[Router] Add packet record (id=0x8f26f652 fr=0x00 to=0x46, WantAck=1, HopLim=3 Ch=0x0 Portnum=74 WANTRESP rxtime=1720725874)
[Router] handleReceived(REMOTE) (id=0x8f26f652 fr=0x00 to=0x46, WantAck=1, HopLim=3 Ch=0x0 Portnum=74 WANTRESP rxtime=1720725875)
[Router] Module 'powerstress' wantsPacket=1
[Router] Received powerstress from=0x0, id=0x8f26f652, portnum=74, payloadlen=7
[Router] Received PowerStress cmd=32
[Router] Asked module 'powerstress' to send a response
[Router] Module 'powerstress' handled and skipped other processing
[Router] No one responded, send a nak
[Router] Alloc an err=8,to=0x67f63246,idFrom=0x8f26f652,id=0x5fa26667
[Router] Enqueued local (id=0x5fa26667 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f652 rxtime=1720725875 priority=120)
[Router] Rx someone rebroadcasting for us (id=0x5fa26667 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f652 rxtime=1720725875 priority=120)
[Router] didn't find pending packet
[Router] Add packet record (id=0x5fa26667 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f652 rxtime=1720725875 priority=120)
[Router] handleReceived(REMOTE) (id=0x5fa26667 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f652 rxtime=1720725875 priority=120)
[Router] Module 'canned' wantsPacket=1
[Router] showing standard frames
[Router] Showing 0 module frames
[Router] Total frame count: 103
[Router] Added modules. numframes: 0
[Router] Finished building frames. numframes: 7
[Router] Module 'canned' considered
[Router] Module 'routing' wantsPacket=1
[Router] Received routing from=0x67f63246, id=0x5fa26667, portnum=5, payloadlen=2
[Router] Routing sniffing (id=0x5fa26667 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f652 rxtime=1720725875 priority=120)
[Router] Received a nak for 0x8f26f652, stopping retransmissions
[Router] Delivering rx packet (id=0x5fa26667 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f652 rxtime=1720725875 priority=120)
[Router] Update DB node 0x67f63246, rx_time=1720725875
[Router] Forwarding to phone (id=0x5fa26667 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f652 rxtime=1720725875 priority=120)
[Router] Module 'routing' considered
[PowerStressModule] S:PS:32
Telling client we have new packets 19
BLE notify fromNum
getFromRadio=STATE_SEND_PACKETS
phone downloaded packet (id=0x5fa26667 fr=0x46 to=0x46, WantAck=0, HopLim=3 Ch=0x0 Portnum=5 requestId=8f26f652 rxtime=1720725875 priority=120)
toRadioWriteCb data 0x2001ffea, len 2
Disconnecting from phone
[PowerStressModule] S:PS:0

View File

Binary file not shown.

View File

@@ -0,0 +1,25 @@
"""Test analysis processing."""
import logging
import os
import sys
import pytest
from meshtastic.analysis.__main__ import main
@pytest.mark.unit
def test_analysis(caplog):
"""Test analysis processing"""
cur_dir = os.path.dirname(os.path.abspath(__file__))
slog_input_dir = os.path.join(cur_dir, "slog-test-input")
sys.argv = ["fakescriptname", "--no-server", "--slog", slog_input_dir]
with caplog.at_level(logging.DEBUG):
logging.getLogger().propagate = True # Let our testing framework see our logs
main()
assert "Exiting without running visualization server" in caplog.text

View File

@@ -11,6 +11,8 @@ from ..protobuf import mesh_pb2, config_pb2
from .. import BROADCAST_ADDR, LOCAL_ADDR
from ..mesh_interface import MeshInterface, _timeago
from ..node import Node
from ..slog import LogSet
from ..powermon import SimPowerSupply
# TODO
# from ..config import Config
@@ -47,11 +49,15 @@ def test_MeshInterface(capsys):
iface.localNode.localConfig.lora.CopyFrom(config_pb2.Config.LoRaConfig())
# Also get some coverage of the structured logging/power meter stuff by turning it on as well
log_set = LogSet(iface, None, SimPowerSupply())
iface.showInfo()
iface.localNode.showInfo()
iface.showNodes()
iface.sendText("hello")
iface.close()
log_set.close()
out, err = capsys.readouterr()
assert re.search(r"Owner: None \(None\)", out, re.MULTILINE)
assert re.search(r"Nodes", out, re.MULTILINE)

862
poetry.lock generated
View File

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ license = "GPL-3.0-only"
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.9,<3.13" # 3.9 is needed for pandas, bleak requires a max of 3.13 for some reason
python = "^3.9,<3.13" # 3.9 is needed for pandas, bleak requires a max of 3.13 for some reason
pyserial = "^3.5"
protobuf = ">=5.26.0"
dotmap = "^1.3.30"
@@ -27,6 +27,10 @@ ppk2-api = "^0.9.2"
pyarrow = "^16.1.0"
platformdirs = "^4.2.2"
print-color = "^0.4.6"
dash = { version = "^2.17.1", optional = true }
pytap2 = { version = "^2.3.0", optional = true }
dash-bootstrap-components = { version = "^1.6.0", optional = true }
pandas = { version = "^2.2.2", optional = true }
[tool.poetry.group.dev.dependencies]
hypothesis = "^6.103.2"
@@ -35,7 +39,6 @@ pytest-cov = "^5.0.0"
pdoc3 = "^0.10.0"
autopep8 = "^2.1.0"
pylint = "^3.2.3"
pytap2 = "^2.3.0"
pyinstaller = "^6.8.0"
mypy = "^1.10.0"
mypy-protobuf = "^3.6.0"
@@ -45,32 +48,27 @@ types-requests = "^2.31.0.20240406"
types-setuptools = "^69.5.0.20240423"
types-pyyaml = "^6.0.12.20240311"
pyarrow-stubs = "^10.0.1.7"
pandas-stubs = "^2.2.2.240603"
# If you are doing power analysis you probably want these extra devtools
# If you are doing power analysis you might want these extra devtools
[tool.poetry.group.analysis]
optional = true
[tool.poetry.group.analysis.dependencies]
jupyterlab = "^4.2.2"
mypy = "^1.10.0"
mypy-protobuf = "^3.6.0"
types-protobuf = "^5.26.0.20240422"
types-tabulate = "^0.9.0.20240106"
types-requests = "^2.31.0.20240406"
types-setuptools = "^69.5.0.20240423"
types-pyyaml = "^6.0.12.20240311"
matplotlib = "^3.9.0"
ipympl = "^0.9.4"
ipywidgets = "^8.1.3"
jupyterlab-widgets = "^3.0.11"
dash = "^2.17.1"
[tool.poetry.extras]
tunnel = ["pytap2"]
analysis = ["dash", "dash-bootstrap-components", "pandas", "pandas-stubs"]
[tool.poetry.scripts]
meshtastic = "meshtastic.__main__:main"
mesh-tunnel = "meshtastic.__main__:tunnelMain [tunnel]"
mesh-analysis = "meshtastic.analysis.__main__:main [analysis]"
# "Poe the poet" (optional) provides an easy way of running non python tools inside the poetry virtualenv
# if you would like to use it run "pipx install poe"