11"""Post-run analysis tools for meshtastic."""
22
3+ import argparse
34import logging
5+
6+ import dash_bootstrap_components as dbc
47import numpy as np
58import pandas as pd
69import plotly .express as px
710import plotly .graph_objects as go
811import pyarrow as pa
912import pyarrow .feather as feather
1013from dash import Dash , Input , Output , callback , dash_table , dcc , html
11- import dash_bootstrap_components as dbc
1214
1315from .. 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
3319pd .options .mode .copy_on_write = True
3420
21+
3522def 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+
5140def 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+
6066def 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+
93104def 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+
105128def 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+
154181def 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
161191if __name__ == "__main__" :
162192 main ()
0 commit comments