99import time
1010from dataclasses import dataclass
1111from datetime import datetime
12+ from functools import reduce
1213from typing import Optional
1314
1415import parse # type: ignore[import-untyped]
1516import platformdirs
17+ import pyarrow as pa
1618from pubsub import pub # type: ignore[import-untyped]
1719
1820from 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"""
4258log_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}
4966log_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