Skip to content

Commit 39e03db

Browse files
committed
add beginnings of analysis viewer (and fix poetry extras usage for tunnel)
1 parent 4dbf9b9 commit 39e03db

4 files changed

Lines changed: 220 additions & 72 deletions

File tree

.vscode/launch.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,14 @@
3636
"justMyCode": true,
3737
"args": ["--tunnel", "--debug"]
3838
},
39+
{
40+
"name": "meshtastic analysis",
41+
"type": "debugpy",
42+
"request": "launch",
43+
"module": "meshtastic.analysis",
44+
"justMyCode": true,
45+
"args": [""]
46+
},
3947
{
4048
"name": "meshtastic set chan",
4149
"type": "debugpy",

meshtastic/analysis/__main__.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
"""Post-run analysis tools for meshtastic."""
2+
3+
import logging
4+
import numpy as np
5+
import pandas as pd
6+
import plotly.express as px
7+
import plotly.graph_objects as go
8+
import pyarrow as pa
9+
import pyarrow.feather as feather
10+
from dash import Dash, Input, Output, callback, dash_table, dcc, html
11+
12+
from .. import mesh_pb2, powermon_pb2
13+
14+
# per https://arrow.apache.org/docs/python/pandas.html#reducing-memory-use-in-table-to-pandas
15+
# use this to get nullable int fields treated as ints rather than floats in pandas
16+
dtype_mapping = {
17+
pa.int8(): pd.Int8Dtype(),
18+
pa.int16(): pd.Int16Dtype(),
19+
pa.int32(): pd.Int32Dtype(),
20+
pa.int64(): pd.Int64Dtype(),
21+
pa.uint8(): pd.UInt8Dtype(),
22+
pa.uint16(): pd.UInt16Dtype(),
23+
pa.uint32(): pd.UInt32Dtype(),
24+
pa.uint64(): pd.UInt64Dtype(),
25+
pa.bool_(): pd.BooleanDtype(),
26+
pa.float32(): pd.Float32Dtype(),
27+
pa.float64(): pd.Float64Dtype(),
28+
pa.string(): pd.StringDtype(),
29+
}
30+
31+
# sdir = '/home/kevinh/.local/share/meshtastic/slogs/20240626-152804'
32+
sdir = "/home/kevinh/.local/share/meshtastic/slogs/latest"
33+
dpwr = feather.read_table(f"{sdir}/power.feather").to_pandas(
34+
types_mapper=dtype_mapping.get
35+
)
36+
dslog = feather.read_table(f"{sdir}/slog.feather").to_pandas(
37+
types_mapper=dtype_mapping.get
38+
)
39+
40+
41+
def get_board_info():
42+
"""Get the board information from the slog dataframe.
43+
44+
tuple: A tuple containing the board ID and software version.
45+
"""
46+
board_info = dslog[dslog["sw_version"].notnull()]
47+
sw_version = board_info.iloc[0]["sw_version"]
48+
board_id = mesh_pb2.HardwareModel.Name(board_info.iloc[0]["board_id"])
49+
return (board_id, sw_version)
50+
51+
52+
pmon_events = dslog[dslog["pm_mask"].notnull()]
53+
54+
55+
pm_masks = pd.Series(pmon_events["pm_mask"]).to_numpy()
56+
57+
# possible to do this with pandas rolling windows if I was smarter?
58+
pm_changes = [(pm_masks[i - 1] ^ x if i != 0 else x) for i, x in enumerate(pm_masks)]
59+
pm_raises = [(pm_masks[i] & x) for i, x in enumerate(pm_changes)]
60+
pm_falls = [(~pm_masks[i] & x if i != 0 else 0) for i, x in enumerate(pm_changes)]
61+
62+
63+
def to_pmon_names(arr) -> list[str]:
64+
"""Convert the power monitor state numbers to their corresponding names.
65+
"""
66+
67+
def to_pmon_name(n):
68+
try:
69+
s = powermon_pb2.PowerMon.State.Name(int(n))
70+
return s if s != "None" else None
71+
except ValueError:
72+
return None
73+
74+
return [to_pmon_name(x) for x in arr]
75+
76+
77+
pd.options.mode.copy_on_write = True
78+
pmon_events["pm_raises"] = to_pmon_names(pm_raises)
79+
pmon_events["pm_falls"] = to_pmon_names(pm_falls)
80+
81+
pmon_raises = pmon_events[pmon_events["pm_raises"].notnull()]
82+
83+
84+
def create_dash():
85+
"""Create a Dash application for visualizing power consumption data."""
86+
app = Dash()
87+
88+
def set_legend(f, name):
89+
f["data"][0]["showlegend"] = True
90+
f["data"][0]["name"] = name
91+
return f
92+
93+
df = dpwr
94+
avg_pwr_lines = px.line(df, x="time", y="average_mW").update_traces(
95+
line_color="red"
96+
)
97+
set_legend(avg_pwr_lines, "avg power")
98+
max_pwr_points = px.scatter(df, x="time", y="max_mW").update_traces(
99+
marker_color="blue"
100+
)
101+
set_legend(max_pwr_points, "max power")
102+
min_pwr_points = px.scatter(df, x="time", y="min_mW").update_traces(
103+
marker_color="green"
104+
)
105+
set_legend(min_pwr_points, "min power")
106+
107+
pmon = pmon_raises
108+
fake_y = np.full(len(pmon), 10.0)
109+
pmon_points = px.scatter(pmon, x="time", y=fake_y, text="pm_raises")
110+
111+
# fig = avg_pwr_lines
112+
# fig.add_trace(max_pwr_points)
113+
# don't show minpower because not that interesting: min_pwr_points.data
114+
fig = go.Figure(data=max_pwr_points.data + avg_pwr_lines.data + pmon_points.data)
115+
116+
fig.update_layout(legend=dict(yanchor="top", y=0.99, xanchor="left", x=0.01))
117+
118+
# App layout
119+
app.layout = [
120+
html.Div(children="Early Meshtastic power analysis tool testing..."),
121+
# dash_table.DataTable(data=df.to_dict('records'), page_size=10),
122+
dcc.Graph(figure=fig),
123+
]
124+
125+
return app
126+
127+
128+
def main():
129+
"""Entry point of the script."""
130+
app = create_dash()
131+
port = 8051
132+
logging.info(f"Running Dash visualization webapp on port {port} (publicly accessible)")
133+
app.run_server(debug=True, host='0.0.0.0', port=port)
134+
135+
136+
if __name__ == "__main__":
137+
main()

0 commit comments

Comments
 (0)