Skip to content

Commit a471517

Browse files
committed
Add basic arg parsing to the meshtastic analysis stuff
1 parent c8eb202 commit a471517

4 files changed

Lines changed: 78 additions & 39 deletions

File tree

.vscode/launch.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@
4141
"type": "debugpy",
4242
"request": "launch",
4343
"module": "meshtastic.analysis",
44-
"justMyCode": true,
45-
"args": [""]
44+
"justMyCode": false,
45+
"args": []
4646
},
4747
{
4848
"name": "meshtastic set chan",

meshtastic/analysis/__main__.py

Lines changed: 59 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,32 @@
11
"""Post-run analysis tools for meshtastic."""
22

3+
import argparse
34
import logging
5+
6+
import dash_bootstrap_components as dbc
47
import numpy as np
58
import pandas as pd
69
import plotly.express as px
710
import plotly.graph_objects as go
811
import pyarrow as pa
912
import pyarrow.feather as feather
1013
from dash import Dash, Input, Output, callback, dash_table, dcc, html
11-
import dash_bootstrap_components as dbc
1214

1315
from .. import mesh_pb2, powermon_pb2
14-
15-
# per https://arrow.apache.org/docs/python/pandas.html#reducing-memory-use-in-table-to-pandas
16-
# use this to get nullable int fields treated as ints rather than floats in pandas
17-
dtype_mapping = {
18-
pa.int8(): pd.Int8Dtype(),
19-
pa.int16(): pd.Int16Dtype(),
20-
pa.int32(): pd.Int32Dtype(),
21-
pa.int64(): pd.Int64Dtype(),
22-
pa.uint8(): pd.UInt8Dtype(),
23-
pa.uint16(): pd.UInt16Dtype(),
24-
pa.uint32(): pd.UInt32Dtype(),
25-
pa.uint64(): pd.UInt64Dtype(),
26-
pa.bool_(): pd.BooleanDtype(),
27-
pa.float32(): pd.Float32Dtype(),
28-
pa.float64(): pd.Float64Dtype(),
29-
pa.string(): pd.StringDtype(),
30-
}
16+
from ..slog import root_dir
3117

3218
# Configure panda options
3319
pd.options.mode.copy_on_write = True
3420

21+
3522
def to_pmon_names(arr) -> list[str]:
3623
"""Convert the power monitor state numbers to their corresponding names.
3724
3825
arr (list): List of power monitor state numbers.
3926
4027
Returns the List of corresponding power monitor state names.
4128
"""
29+
4230
def to_pmon_name(n):
4331
try:
4432
s = powermon_pb2.PowerMon.State.Name(int(n))
@@ -48,15 +36,33 @@ def to_pmon_name(n):
4836

4937
return [to_pmon_name(x) for x in arr]
5038

39+
5140
def read_pandas(filepath: str) -> pd.DataFrame:
5241
"""Read a feather file and convert it to a pandas DataFrame.
5342
5443
filepath (str): Path to the feather file.
5544
5645
Returns the pandas DataFrame.
5746
"""
47+
# per https://arrow.apache.org/docs/python/pandas.html#reducing-memory-use-in-table-to-pandas
48+
# use this to get nullable int fields treated as ints rather than floats in pandas
49+
dtype_mapping = {
50+
pa.int8(): pd.Int8Dtype(),
51+
pa.int16(): pd.Int16Dtype(),
52+
pa.int32(): pd.Int32Dtype(),
53+
pa.int64(): pd.Int64Dtype(),
54+
pa.uint8(): pd.UInt8Dtype(),
55+
pa.uint16(): pd.UInt16Dtype(),
56+
pa.uint32(): pd.UInt32Dtype(),
57+
pa.uint64(): pd.UInt64Dtype(),
58+
pa.bool_(): pd.BooleanDtype(),
59+
pa.float32(): pd.Float32Dtype(),
60+
pa.float64(): pd.Float64Dtype(),
61+
pa.string(): pd.StringDtype(),
62+
}
5863
return feather.read_table(filepath).to_pandas(types_mapper=dtype_mapping.get)
5964

65+
6066
def get_pmon_raises(dslog: pd.DataFrame) -> pd.DataFrame:
6167
"""Get the power monitor raises from the slog DataFrame.
6268
@@ -69,7 +75,9 @@ def get_pmon_raises(dslog: pd.DataFrame) -> pd.DataFrame:
6975
pm_masks = pd.Series(pmon_events["pm_mask"]).to_numpy()
7076

7177
# possible to do this with pandas rolling windows if I was smarter?
72-
pm_changes = [(pm_masks[i - 1] ^ x if i != 0 else x) for i, x in enumerate(pm_masks)]
78+
pm_changes = [
79+
(pm_masks[i - 1] ^ x if i != 0 else x) for i, x in enumerate(pm_masks)
80+
]
7381
pm_raises = [(pm_masks[i] & x) for i, x in enumerate(pm_changes)]
7482
pm_falls = [(~pm_masks[i] & x if i != 0 else 0) for i, x in enumerate(pm_changes)]
7583

@@ -81,15 +89,18 @@ def get_pmon_raises(dslog: pd.DataFrame) -> pd.DataFrame:
8189

8290
def get_endtime(row):
8391
"""Find the corresponding fall event."""
84-
following = pmon_falls[(pmon_falls["pm_falls"] == row["pm_raises"]) &
85-
(pmon_falls["time"] > row["time"])]
92+
following = pmon_falls[
93+
(pmon_falls["pm_falls"] == row["pm_raises"])
94+
& (pmon_falls["time"] > row["time"])
95+
]
8696
return following.iloc[0] if not following.empty else None
8797

8898
# HMM - setting end_time doesn't work yet - leave off for now
8999
# pmon_raises['end_time'] = pmon_raises.apply(get_endtime, axis=1)
90100

91101
return pmon_raises
92102

103+
93104
def get_board_info(dslog: pd.DataFrame) -> tuple:
94105
"""Get the board information from the slog DataFrame.
95106
@@ -102,19 +113,34 @@ def get_board_info(dslog: pd.DataFrame) -> tuple:
102113
board_id = mesh_pb2.HardwareModel.Name(board_info.iloc[0]["board_id"])
103114
return (board_id, sw_version)
104115

116+
117+
def create_argparser() -> argparse.ArgumentParser:
118+
"""Create the argument parser for the script."""
119+
parser = argparse.ArgumentParser(description="Meshtastic power analysis tools")
120+
group = parser
121+
group.add_argument(
122+
"--slog",
123+
help="Specify the structured-logs directory (defaults to latest log directory)",
124+
)
125+
return parser
126+
127+
105128
def create_dash(slog_path: str) -> Dash:
106129
"""Create a Dash application for visualizing power consumption data.
107130
108131
slog_path (str): Path to the slog directory.
109132
110133
Returns the Dash application.
111134
"""
112-
app = Dash(
113-
external_stylesheets=[dbc.themes.BOOTSTRAP]
114-
)
135+
app = Dash(external_stylesheets=[dbc.themes.BOOTSTRAP])
115136

116-
dpwr = read_pandas(f"{slog_path}/power.feather")
117-
dslog = read_pandas(f"{slog_path}/slog.feather")
137+
parser = create_argparser()
138+
args = parser.parse_args()
139+
if not args.slog:
140+
args.slog = f"{root_dir()}/latest"
141+
142+
dpwr = read_pandas(f"{args.slog}/power.feather")
143+
dslog = read_pandas(f"{args.slog}/slog.feather")
118144

119145
pmon_raises = get_pmon_raises(dslog)
120146

@@ -145,18 +171,22 @@ def set_legend(f, name):
145171

146172
# App layout
147173
app.layout = [
148-
html.Div(children="Early Meshtastic power analysis tool testing..."),
174+
html.Div(children="Meshtastic power analysis tool testing..."),
149175
dcc.Graph(figure=fig),
150176
]
151177

152178
return app
153179

180+
154181
def main():
155182
"""Entry point of the script."""
156183
app = create_dash(slog_path="/home/kevinh/.local/share/meshtastic/slogs/latest")
157184
port = 8051
158-
logging.info(f"Running Dash visualization webapp on port {port} (publicly accessible)")
159-
app.run_server(debug=True, host='0.0.0.0', port=port)
185+
logging.info(
186+
f"Running Dash visualization webapp on port {port} (publicly accessible)"
187+
)
188+
app.run_server(debug=True, host="0.0.0.0", port=port)
189+
160190

161191
if __name__ == "__main__":
162192
main()

meshtastic/slog/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""Structured logging framework (see dev docs for more info)."""
22

3-
from .slog import LogSet
3+
from .slog import LogSet, root_dir

meshtastic/slog/slog.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,17 @@
2323
from .arrow import FeatherWriter
2424

2525

26+
def root_dir() -> str:
27+
"""Return the root directory for slog files."""
28+
29+
app_name = "meshtastic"
30+
app_author = "meshtastic"
31+
app_dir = platformdirs.user_data_dir(app_name, app_author)
32+
dir_name = f"{app_dir}/slogs"
33+
os.makedirs(dir_name, exist_ok=True)
34+
return dir_name
35+
36+
2637
@dataclass(init=False)
2738
class LogDef:
2839
"""Log definition."""
@@ -244,17 +255,15 @@ def __init__(
244255
"""
245256

246257
if not dir_name:
247-
app_name = "meshtastic"
248-
app_author = "meshtastic"
249-
app_dir = platformdirs.user_data_dir(app_name, app_author)
250-
dir_name = f"{app_dir}/slogs/{datetime.now().strftime('%Y%m%d-%H%M%S')}"
258+
app_dir = root_dir()
259+
dir_name = f"{app_dir}/{datetime.now().strftime('%Y%m%d-%H%M%S')}"
251260
os.makedirs(dir_name, exist_ok=True)
252261

253262
# Also make a 'latest' directory that always points to the most recent logs
254263
# symlink might fail on some platforms, if it does fail silently
255-
if os.path.exists(f"{app_dir}/slogs/latest"):
256-
os.unlink(f"{app_dir}/slogs/latest")
257-
os.symlink(dir_name, f"{app_dir}/slogs/latest", target_is_directory=True)
264+
if os.path.exists(f"{app_dir}/latest"):
265+
os.unlink(f"{app_dir}/latest")
266+
os.symlink(dir_name, f"{app_dir}/latest", target_is_directory=True)
258267

259268
self.dir_name = dir_name
260269

0 commit comments

Comments
 (0)