|
29 | 29 | pa.string(): pd.StringDtype(), |
30 | 30 | } |
31 | 31 |
|
32 | | -# sdir = '/home/kevinh/.local/share/meshtastic/slogs/20240626-152804' |
33 | | -sdir = "/home/kevinh/.local/share/meshtastic/slogs/latest" |
34 | | -dpwr = feather.read_table(f"{sdir}/power.feather").to_pandas( |
35 | | - types_mapper=dtype_mapping.get |
36 | | -) |
37 | | -dslog = feather.read_table(f"{sdir}/slog.feather").to_pandas( |
38 | | - types_mapper=dtype_mapping.get |
39 | | -) |
| 32 | +# Configure panda options |
| 33 | +pd.options.mode.copy_on_write = True |
40 | 34 |
|
| 35 | +def to_pmon_names(arr) -> list[str]: |
| 36 | + """Convert the power monitor state numbers to their corresponding names. |
41 | 37 |
|
42 | | -def get_board_info(): |
43 | | - """Get the board information from the slog dataframe. |
| 38 | + arr (list): List of power monitor state numbers. |
44 | 39 |
|
45 | | - tuple: A tuple containing the board ID and software version. |
| 40 | + Returns the List of corresponding power monitor state names. |
46 | 41 | """ |
47 | | - board_info = dslog[dslog["sw_version"].notnull()] |
48 | | - sw_version = board_info.iloc[0]["sw_version"] |
49 | | - board_id = mesh_pb2.HardwareModel.Name(board_info.iloc[0]["board_id"]) |
50 | | - return (board_id, sw_version) |
| 42 | + def to_pmon_name(n): |
| 43 | + try: |
| 44 | + s = powermon_pb2.PowerMon.State.Name(int(n)) |
| 45 | + return s if s != "None" else None |
| 46 | + except ValueError: |
| 47 | + return None |
51 | 48 |
|
| 49 | + return [to_pmon_name(x) for x in arr] |
52 | 50 |
|
53 | | -pmon_events = dslog[dslog["pm_mask"].notnull()] |
| 51 | +def read_pandas(filepath: str) -> pd.DataFrame: |
| 52 | + """Read a feather file and convert it to a pandas DataFrame. |
54 | 53 |
|
| 54 | + filepath (str): Path to the feather file. |
55 | 55 |
|
56 | | -pm_masks = pd.Series(pmon_events["pm_mask"]).to_numpy() |
| 56 | + Returns the pandas DataFrame. |
| 57 | + """ |
| 58 | + return feather.read_table(filepath).to_pandas(types_mapper=dtype_mapping.get) |
57 | 59 |
|
58 | | -# possible to do this with pandas rolling windows if I was smarter? |
59 | | -pm_changes = [(pm_masks[i - 1] ^ x if i != 0 else x) for i, x in enumerate(pm_masks)] |
60 | | -pm_raises = [(pm_masks[i] & x) for i, x in enumerate(pm_changes)] |
61 | | -pm_falls = [(~pm_masks[i] & x if i != 0 else 0) for i, x in enumerate(pm_changes)] |
| 60 | +def get_pmon_raises(dslog: pd.DataFrame) -> pd.DataFrame: |
| 61 | + """Get the power monitor raises from the slog DataFrame. |
62 | 62 |
|
| 63 | + dslog (pd.DataFrame): The slog DataFrame. |
63 | 64 |
|
64 | | -def to_pmon_names(arr) -> list[str]: |
65 | | - """Convert the power monitor state numbers to their corresponding names. |
| 65 | + Returns the DataFrame containing the power monitor raises. |
66 | 66 | """ |
| 67 | + pmon_events = dslog[dslog["pm_mask"].notnull()] |
67 | 68 |
|
68 | | - def to_pmon_name(n): |
69 | | - try: |
70 | | - s = powermon_pb2.PowerMon.State.Name(int(n)) |
71 | | - return s if s != "None" else None |
72 | | - except ValueError: |
73 | | - return None |
| 69 | + pm_masks = pd.Series(pmon_events["pm_mask"]).to_numpy() |
74 | 70 |
|
75 | | - return [to_pmon_name(x) for x in arr] |
| 71 | + # 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)] |
| 73 | + pm_raises = [(pm_masks[i] & x) for i, x in enumerate(pm_changes)] |
| 74 | + pm_falls = [(~pm_masks[i] & x if i != 0 else 0) for i, x in enumerate(pm_changes)] |
76 | 75 |
|
| 76 | + pmon_events["pm_raises"] = to_pmon_names(pm_raises) |
| 77 | + pmon_events["pm_falls"] = to_pmon_names(pm_falls) |
77 | 78 |
|
78 | | -pd.options.mode.copy_on_write = True |
79 | | -pmon_events["pm_raises"] = to_pmon_names(pm_raises) |
80 | | -pmon_events["pm_falls"] = to_pmon_names(pm_falls) |
| 79 | + pmon_raises = pmon_events[pmon_events["pm_raises"].notnull()][["time", "pm_raises"]] |
| 80 | + pmon_falls = pmon_events[pmon_events["pm_falls"].notnull()] |
| 81 | + |
| 82 | + def get_endtime(row): |
| 83 | + """Find the corresponding fall event.""" |
| 84 | + following = pmon_falls[(pmon_falls["pm_falls"] == row["pm_raises"]) & |
| 85 | + (pmon_falls["time"] > row["time"])] |
| 86 | + return following.iloc[0] if not following.empty else None |
| 87 | + |
| 88 | + # HMM - setting end_time doesn't work yet - leave off for now |
| 89 | + # pmon_raises['end_time'] = pmon_raises.apply(get_endtime, axis=1) |
| 90 | + |
| 91 | + return pmon_raises |
| 92 | + |
| 93 | +def get_board_info(dslog: pd.DataFrame) -> tuple: |
| 94 | + """Get the board information from the slog DataFrame. |
| 95 | +
|
| 96 | + dslog (pd.DataFrame): The slog DataFrame. |
| 97 | +
|
| 98 | + Returns a tuple containing the board ID and software version. |
| 99 | + """ |
| 100 | + board_info = dslog[dslog["sw_version"].notnull()] |
| 101 | + sw_version = board_info.iloc[0]["sw_version"] |
| 102 | + board_id = mesh_pb2.HardwareModel.Name(board_info.iloc[0]["board_id"]) |
| 103 | + return (board_id, sw_version) |
81 | 104 |
|
82 | | -pmon_raises = pmon_events[pmon_events["pm_raises"].notnull()] |
| 105 | +def create_dash(slog_path: str) -> Dash: |
| 106 | + """Create a Dash application for visualizing power consumption data. |
83 | 107 |
|
| 108 | + slog_path (str): Path to the slog directory. |
84 | 109 |
|
85 | | -def create_dash(): |
86 | | - """Create a Dash application for visualizing power consumption data.""" |
| 110 | + Returns the Dash application. |
| 111 | + """ |
87 | 112 | app = Dash( |
88 | 113 | external_stylesheets=[dbc.themes.BOOTSTRAP] |
89 | 114 | ) |
90 | 115 |
|
| 116 | + dpwr = read_pandas(f"{slog_path}/power.feather") |
| 117 | + dslog = read_pandas(f"{slog_path}/slog.feather") |
| 118 | + |
| 119 | + pmon_raises = get_pmon_raises(dslog) |
| 120 | + |
91 | 121 | def set_legend(f, name): |
92 | 122 | f["data"][0]["showlegend"] = True |
93 | 123 | f["data"][0]["name"] = name |
94 | 124 | return f |
95 | 125 |
|
96 | | - df = dpwr |
97 | | - avg_pwr_lines = px.line(df, x="time", y="average_mW").update_traces( |
| 126 | + avg_pwr_lines = px.line(dpwr, x="time", y="average_mW").update_traces( |
98 | 127 | line_color="red" |
99 | 128 | ) |
100 | 129 | set_legend(avg_pwr_lines, "avg power") |
101 | | - max_pwr_points = px.scatter(df, x="time", y="max_mW").update_traces( |
| 130 | + max_pwr_points = px.scatter(dpwr, x="time", y="max_mW").update_traces( |
102 | 131 | marker_color="blue" |
103 | 132 | ) |
104 | 133 | set_legend(max_pwr_points, "max power") |
105 | | - min_pwr_points = px.scatter(df, x="time", y="min_mW").update_traces( |
| 134 | + min_pwr_points = px.scatter(dpwr, x="time", y="min_mW").update_traces( |
106 | 135 | marker_color="green" |
107 | 136 | ) |
108 | 137 | set_legend(min_pwr_points, "min power") |
109 | 138 |
|
110 | | - pmon = pmon_raises |
111 | | - fake_y = np.full(len(pmon), 10.0) |
112 | | - pmon_points = px.scatter(pmon, x="time", y=fake_y, text="pm_raises") |
| 139 | + fake_y = np.full(len(pmon_raises), 10.0) |
| 140 | + pmon_points = px.scatter(pmon_raises, x="time", y=fake_y, text="pm_raises") |
113 | 141 |
|
114 | | - # fig = avg_pwr_lines |
115 | | - # fig.add_trace(max_pwr_points) |
116 | | - # don't show minpower because not that interesting: min_pwr_points.data |
117 | 142 | fig = go.Figure(data=max_pwr_points.data + avg_pwr_lines.data + pmon_points.data) |
118 | 143 |
|
119 | 144 | fig.update_layout(legend=dict(yanchor="top", y=0.99, xanchor="left", x=0.01)) |
120 | 145 |
|
121 | 146 | # App layout |
122 | 147 | app.layout = [ |
123 | 148 | html.Div(children="Early Meshtastic power analysis tool testing..."), |
124 | | - # dash_table.DataTable(data=df.to_dict('records'), page_size=10), |
125 | 149 | dcc.Graph(figure=fig), |
126 | 150 | ] |
127 | 151 |
|
128 | 152 | return app |
129 | 153 |
|
130 | | - |
131 | 154 | def main(): |
132 | 155 | """Entry point of the script.""" |
133 | | - app = create_dash() |
| 156 | + app = create_dash(slog_path="/home/kevinh/.local/share/meshtastic/slogs/latest") |
134 | 157 | port = 8051 |
135 | 158 | logging.info(f"Running Dash visualization webapp on port {port} (publicly accessible)") |
136 | 159 | app.run_server(debug=True, host='0.0.0.0', port=port) |
137 | 160 |
|
138 | | - |
139 | 161 | if __name__ == "__main__": |
140 | 162 | main() |
0 commit comments