package logparsers import ( "encoding/hex" "fmt" "regexp" "strconv" "strings" "time" "github.com/kschamplin/gotelem/internal/can" "github.com/kschamplin/gotelem/skylab" ) // A FormatError is an error when parsing a format. Typically we simply ignore // these and move on, but they can optionally wrap another error that is fatal. type FormatError struct { msg string err error } func (e *FormatError) Error() string { if e.err != nil { return fmt.Sprintf("%s:%s", e.msg, e.err.Error()) } return fmt.Sprintf("%s", e.msg) } func (e *FormatError) Unwrap() error { return e.err } func NewFormatError(msg string, err error) error { return &FormatError{msg: msg, err: err} } // type LineParserFunc is a function that takes a string // and returns a can frame. This is useful for common // can dump formats. type LineParserFunc func(string) (can.Frame, time.Time, error) var candumpRegex = regexp.MustCompile(`^\((\d+)\.(\d{6}) \w+ (\w+)#(\w+)$`) func parseCanDumpLine(dumpLine string) (frame can.Frame, ts time.Time, err error) { frame = can.Frame{} ts = time.Unix(0,0) // dumpline looks like this: // (1684538768.521889) can0 200#8D643546 // remove trailing newline dumpLine = strings.TrimSpace(dumpLine) segments := strings.Split(dumpLine, " ") if len(segments) != 3 { err = NewFormatError("failed to split line", err) return } var unixSeconds, unixMicros int64 fmt.Sscanf(segments[0], "(%d.%d)", &unixSeconds, &unixMicros) // now we extract the remaining data: hexes := strings.Split(segments[2], "#") // first portion is id, second is data id, err := strconv.ParseUint(hexes[0], 16, 64) if err != nil { err = NewFormatError("failed to parse id", err) return } if (len(hexes[1]) % 2) != 0 { err = NewFormatError("odd number of hex characters", nil) return } rawData, err := hex.DecodeString(hexes[1]) if err != nil { err = NewFormatError("failed to decode hex data", err) return } frame.Id = can.CanID{Id: uint32(id), Extended: false} frame.Data = rawData frame.Kind = can.CanDataFrame ts = time.Unix(unixSeconds, unixMicros) return } // data is of the form // 1698180835.318 0619D80564080EBE241 // the second part there is 3 nibbles (12 bits, 3 hex chars) for can ID, // the rest is data. // this regex does the processing. we precompile for speed. var telemRegex = regexp.MustCompile(`^(\d+)\.(\d{3}) (\w{3})(\w+)$`) func parseTelemLogLine(line string) (frame can.Frame, ts time.Time, err error) { frame = can.Frame{} ts = time.Unix(0,0) // strip trailng newline since we rely on it being gone line = strings.TrimSpace(line) // these files tend to get corrupted. // there are all kinds of nasties that can happen. // defense against random panics defer func() { if r := recover(); r != nil { err = NewFormatError("caught panic", nil) } }() a := telemRegex.FindStringSubmatch(line) if a == nil || len(a) != 5 { err = NewFormatError("no regex match", nil) return } var unixSeconds, unixMillis int64 // note that a contains 5 elements, the first being the full match. // so we start from the second element unixSeconds, err = strconv.ParseInt(a[1], 10, 0) if err != nil { err = NewFormatError("failed to parse unix seconds", err) return } unixMillis, err = strconv.ParseInt(a[2], 10, 0) if err != nil { err = NewFormatError("failed to parse unix millis", err) return } ts = time.Unix(unixSeconds, unixMillis*int64(time.Millisecond)) // VALIDATION STEP: sometimes the data gets really whack. // We check that the time is between 2017 and 2032. // Realistically we will not be using this software then. // TODO: add this id, err := strconv.ParseUint(a[3], 16, 16) if err != nil { err = NewFormatError("failed to parse id", err) return } if len(a[4])%2 != 0 { // odd hex chars, protect against a panic err = NewFormatError("wrong amount of hex chars", nil) return } rawData, err := hex.DecodeString(a[4]) if err != nil { err = NewFormatError("failed to parse hex data", err) return } frame = can.Frame{ Id: can.CanID{Id: uint32(id), Extended: false}, Data: rawData, Kind: can.CanDataFrame, } return frame, ts, nil } // this is how we adapt a can frame source into one that produces // skylab busevents type BusParserFunc func(string) (skylab.BusEvent, error) func parserBusEventMapper(f LineParserFunc) BusParserFunc { return func(s string) (skylab.BusEvent, error) { var b = skylab.BusEvent{} f, ts, err := f(s) if err != nil { return b, err } b.Timestamp = ts b.Data, err = skylab.FromCanFrame(f) if err != nil { return b, err } b.Name = b.Data.String() return b, nil } } var ParsersMap = map[string]BusParserFunc{ "telem": parserBusEventMapper(parseTelemLogLine), "candump": parserBusEventMapper(parseCanDumpLine), }