|
| 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