gotelem/internal/logparsers/parsers.go
2024-02-13 10:03:39 -06:00

168 lines
4.2 KiB
Go

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}
}
// A Parser takes a string containing one line of a particular log file
// and returns an associated skylab.BusEvent representing the packet.
// if no packet is found, an error is returned instead.
type ParserFunc func(string) (skylab.BusEvent, error)
func parseCanDumpLine(dumpLine string) (b skylab.BusEvent, err error) {
b = skylab.BusEvent{}
// dumpline looks like this:
// (1684538768.521889) can0 200#8D643546
// remove trailing newline
dumpLine = strings.TrimSpace(dumpLine)
segments := strings.Split(dumpLine, " ")
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 := can.Frame{
// TODO: fix extended ids. we assume not extended for now.
Id: can.CanID{Id: uint32(id), Extended: false},
Data: rawData,
Kind: can.CanDataFrame,
}
b.Timestamp = time.Unix(unixSeconds, unixMicros)
b.Data, err = skylab.FromCanFrame(frame)
if err != nil {
err = NewFormatError("failed to parse can frame", err)
return
}
// set the name
b.Name = b.Data.String()
return
}
func parseTelemLogLine(line string) (b skylab.BusEvent, err error) {
b = skylab.BusEvent{}
// strip trailng newline since we rely on it being gone
line = strings.TrimSpace(line)
// 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.
r := regexp.MustCompile(`^(\d+).(\d{3}) (\w{3})(\w+)$`)
// 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 := r.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*1e6)
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)
}
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,
}
b.Timestamp = ts
b.Data, err = skylab.FromCanFrame(frame)
if err != nil {
err = NewFormatError("failed to parse can frame", err)
return
}
b.Name = b.Data.String()
return
}
var ParsersMap = map[string]ParserFunc{
"telem": parseTelemLogLine,
"candump": parseCanDumpLine,
}