195 lines
5.1 KiB
Go
195 lines
5.1 KiB
Go
package logparsers
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"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 e.msg
|
|
|
|
}
|
|
func (e *FormatError) Unwrap() error {
|
|
return e.err
|
|
}
|
|
|
|
// NewFormatError constructs a new format error.
|
|
func NewFormatError(msg string, err error) error {
|
|
return &FormatError{msg: msg, err: err}
|
|
}
|
|
|
|
// type CanFrameParser is a function that takes a string
|
|
// and returns a can frame. This is useful for common
|
|
// can dump formats.
|
|
type CanFrameParser 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/whitespaces
|
|
dumpLine = strings.TrimSpace(dumpLine)
|
|
m := candumpRegex.FindStringSubmatch(dumpLine)
|
|
if m == nil || len(m) != 5 {
|
|
err = NewFormatError("no regex match", nil)
|
|
return
|
|
}
|
|
|
|
var unixSeconds, unixMicros int64
|
|
|
|
unixSeconds, err = strconv.ParseInt(m[1], 10, 0)
|
|
if err != nil {
|
|
err = NewFormatError("failed to parse unix seconds", err)
|
|
return
|
|
}
|
|
unixMicros, err = strconv.ParseInt(m[2], 10, 0)
|
|
if err != nil {
|
|
err = NewFormatError("failed to parse unix micros", err)
|
|
return
|
|
}
|
|
|
|
id, err := strconv.ParseUint(m[3], 16, 64)
|
|
if err != nil {
|
|
err = NewFormatError("failed to parse id", err)
|
|
return
|
|
}
|
|
if (len(m[4]) % 2) != 0 {
|
|
err = NewFormatError("odd number of hex characters", nil)
|
|
return
|
|
}
|
|
rawData, err := hex.DecodeString(m[4])
|
|
if err != nil {
|
|
err = NewFormatError("failed to decode hex data", err)
|
|
return
|
|
}
|
|
|
|
// TODO: add extended id support, need an example log and a test.
|
|
frame.Id = can.CanID{Id: uint32(id), Extended: false}
|
|
frame.Data = rawData
|
|
frame.Kind = can.CanDataFrame
|
|
|
|
ts = time.Unix(unixSeconds, unixMicros*int64(time.Microsecond))
|
|
|
|
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)
|
|
|
|
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, but remains valid.
|
|
// 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
|
|
|
|
}
|
|
|
|
// BusEventParser is a function that takes a string and returns a busevent.
|
|
type BusEventParser func(string) (skylab.BusEvent, error)
|
|
|
|
// skylabify JSON parser.
|
|
func parseSkylabifyLogLine(input string) (skylab.BusEvent, error) {
|
|
var b = skylab.BusEvent{}
|
|
err := json.Unmarshal([]byte(input), &b)
|
|
return b, err
|
|
}
|
|
|
|
// frameParseToBusEvent takes a line parser (that returns a can frame)
|
|
// and makes it return a busEvent instead.
|
|
func frameParseToBusEvent(fun CanFrameParser) BusEventParser {
|
|
return func(s string) (skylab.BusEvent, error) {
|
|
var b = skylab.BusEvent{}
|
|
frame, ts, err := fun(s)
|
|
if err != nil {
|
|
return b, err
|
|
}
|
|
b.Timestamp = ts
|
|
b.Data, err = skylab.FromCanFrame(frame)
|
|
if err != nil {
|
|
return b, err
|
|
}
|
|
b.Name = b.Data.String()
|
|
return b, nil
|
|
}
|
|
}
|
|
|
|
var ParsersMap = map[string]BusEventParser{
|
|
"telem": frameParseToBusEvent(parseTelemLogLine),
|
|
"candump": frameParseToBusEvent(parseCanDumpLine),
|
|
"json": parseSkylabifyLogLine,
|
|
}
|