Skip to content

Commit 462d9a8

Browse files
committed
Automatically extract and store all known structured-logs
1 parent 4c02114 commit 462d9a8

5 files changed

Lines changed: 60 additions & 23 deletions

File tree

.vscode/launch.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@
204204
"request": "launch",
205205
"module": "meshtastic",
206206
"justMyCode": false,
207-
"args": ["--slog", "--power-ppk2-meter", "--power-stress", "--power-voltage", "3.3"]
207+
"args": ["--slog", "--power-ppk2-meter", "--power-stress", "--power-voltage", "3.3", "--seriallog"]
208208
},
209209
{
210210
"name": "meshtastic test",

meshtastic/__main__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -897,8 +897,10 @@ def setSimpleConfig(modem_preset):
897897
# if the user didn't ask for serial debugging output, we might want to exit after we've done our operation
898898
if (not args.seriallog) and closeNow:
899899
interface.close() # after running command then exit
900-
if log_set:
901-
log_set.close()
900+
901+
# Close any structured logs after we've done all of our API operations
902+
if log_set:
903+
log_set.close()
902904

903905
except Exception as ex:
904906
print(f"Aborting due to: {ex}")

meshtastic/powermon/stress.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,13 @@ def run(self):
9595
num_seconds = 5.0
9696
states = [
9797
powermon_pb2.PowerStressMessage.LED_ON,
98+
powermon_pb2.PowerStressMessage.LED_OFF,
9899
powermon_pb2.PowerStressMessage.BT_OFF,
99100
powermon_pb2.PowerStressMessage.BT_ON,
100101
powermon_pb2.PowerStressMessage.CPU_FULLON,
101102
powermon_pb2.PowerStressMessage.CPU_IDLE,
102-
powermon_pb2.PowerStressMessage.CPU_DEEPSLEEP,
103+
# FIXME - can't test deepsleep yet because the ttyACM device disappears. Fix the python code to retry connections
104+
# powermon_pb2.PowerStressMessage.CPU_DEEPSLEEP,
103105
]
104106
for s in states:
105107
s_name = powermon_pb2.PowerStressMessage.Opcode.Name(s)

meshtastic/slog/arrow.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,22 @@ def close(self):
2929
self.writer.close()
3030
self.sink.close()
3131

32+
def set_schema(self, schema: pa.Schema):
33+
"""Set the schema for the file.
34+
Only needed for datasets where we can't learn it from the first record written.
35+
36+
schema (pa.Schema): The schema to use.
37+
"""
38+
assert self.schema is None
39+
self.schema = schema
40+
self.writer = pa.ipc.new_stream(self.sink, schema)
41+
3242
def _write(self):
3343
"""Write the new rows to the file."""
3444
if len(self.new_rows) > 0:
3545
if self.schema is None:
3646
# only need to look at the first row to learn the schema
37-
self.schema = pa.Table.from_pylist([self.new_rows[0]]).schema
38-
self.writer = pa.ipc.new_stream(self.sink, self.schema)
47+
self.set_schema(pa.Table.from_pylist([self.new_rows[0]]).schema)
3948

4049
self.writer.write_batch(pa.RecordBatch.from_pylist(self.new_rows))
4150
self.new_rows = []

meshtastic/slog/slog.py

Lines changed: 41 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@
99
import time
1010
from dataclasses import dataclass
1111
from datetime import datetime
12+
from functools import reduce
1213
from typing import Optional
1314

1415
import parse # type: ignore[import-untyped]
1516
import platformdirs
17+
import pyarrow as pa
1618
from pubsub import pub # type: ignore[import-untyped]
1719

1820
from meshtastic.mesh_interface import MeshInterface
@@ -26,24 +28,39 @@ class LogDef:
2628
"""Log definition."""
2729

2830
code: str # i.e. PM or B or whatever... see meshtastic slog documentation
31+
fields: list[tuple[str, pa.DataType]] # A list of field names and their arrow types
2932
format: parse.Parser # A format string that can be used to parse the arguments
3033

31-
def __init__(self, code: str, fmt: str) -> None:
34+
def __init__(self, code: str, fields: list[tuple[str, pa.DataType]]) -> None:
3235
"""Initialize the LogDef object.
3336
3437
code (str): The code.
3538
format (str): The format.
39+
3640
"""
3741
self.code = code
42+
self.fields = fields
43+
44+
fmt = ""
45+
for idx, f in enumerate(fields):
46+
if idx != 0:
47+
fmt += ","
48+
49+
# make the format string
50+
suffix = (
51+
"" if f[1] == pa.string() else ":d"
52+
) # treat as a string or an int (the only types we have so far)
53+
fmt += "{" + f[0] + suffix + "}"
3854
self.format = parse.compile(fmt)
3955

4056

4157
"""A dictionary mapping from logdef code to logdef"""
4258
log_defs = {
4359
d.code: d
4460
for d in [
45-
LogDef("B", "{boardid:d},{version}"),
46-
LogDef("PM", "{bitmask:d},{reason}"),
61+
LogDef("B", [("board_id", pa.uint32()), ("sw_version", pa.string())]),
62+
LogDef("PM", [("pm_mask", pa.uint64()), ("pm_reason", pa.string())]),
63+
LogDef("PS", [("ps_state", pa.uint64())]),
4764
]
4865
}
4966
log_regex = re.compile(".*S:([0-9A-Za-z]+):(.*)")
@@ -99,7 +116,15 @@ def __init__(self, client: MeshInterface, dir_path: str) -> None:
99116
client (MeshInterface): The MeshInterface object to monitor.
100117
"""
101118
self.client = client
119+
120+
# Setup the arrow writer (and its schema)
102121
self.writer = FeatherWriter(f"{dir_path}/slog")
122+
all_fields = reduce(
123+
(lambda x, y: x + y), map(lambda x: x.fields, log_defs.values())
124+
)
125+
126+
self.writer.set_schema(pa.schema(all_fields))
127+
103128
self.raw_file: Optional[
104129
io.TextIOWrapper
105130
] = open( # pylint: disable=consider-using-with
@@ -131,21 +156,20 @@ def _onLogMessage(self, line: str) -> None:
131156
src = m.group(1)
132157
args = m.group(2)
133158
args += " " # append a space so that if the last arg is an empty str it will still be accepted as a match
134-
logging.debug(f"SLog {src}, reason: {args}")
135-
if src != "PM":
136-
logging.warning(f"Not yet handling structured log {src} (FIXME)")
137-
else:
138-
d = log_defs.get(src)
139-
if d:
140-
r = d.format.parse(args) # get the values with the correct types
141-
if r:
142-
di = r.named
143-
di["time"] = datetime.now()
144-
self.writer.add_row(di)
145-
else:
146-
logging.warning(f"Failed to parse slog {line} with {d.format}")
159+
logging.debug(f"SLog {src}, args: {args}")
160+
161+
d = log_defs.get(src)
162+
if d:
163+
r = d.format.parse(args) # get the values with the correct types
164+
if r:
165+
di = r.named
166+
di["time"] = datetime.now()
167+
self.writer.add_row(di)
147168
else:
148-
logging.warning(f"Unknown Structured Log: {line}")
169+
logging.warning(f"Failed to parse slog {line} with {d.format}")
170+
else:
171+
logging.warning(f"Unknown Structured Log: {line}")
172+
149173
if self.raw_file:
150174
self.raw_file.write(line + "\n") # Write the raw log
151175

0 commit comments

Comments
 (0)