2023-05-20 19:53:56 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bufio"
|
|
|
|
"encoding/hex"
|
|
|
|
"encoding/json"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"os"
|
2024-02-12 15:45:23 +00:00
|
|
|
"regexp"
|
2023-05-20 19:53:56 +00:00
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
"syscall"
|
2023-07-03 14:44:32 +00:00
|
|
|
"time"
|
2023-05-20 19:53:56 +00:00
|
|
|
|
2024-02-12 15:45:23 +00:00
|
|
|
"github.com/kschamplin/gotelem/internal/can"
|
2023-05-20 19:53:56 +00:00
|
|
|
"github.com/kschamplin/gotelem/skylab"
|
|
|
|
"github.com/urfave/cli/v2"
|
2023-06-28 01:39:57 +00:00
|
|
|
"golang.org/x/exp/slog"
|
2023-05-20 19:53:56 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// this command can be used to decode candump logs and dump json output.
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
|
|
|
|
app := cli.NewApp()
|
|
|
|
app.Name = "skylabify"
|
|
|
|
app.Usage = "decode skylab packets"
|
|
|
|
app.ArgsUsage = "<input file>"
|
|
|
|
app.Commands = nil
|
|
|
|
app.Description = `skylabify can read in candump logs and output newline-delimited JSON.
|
|
|
|
It is designed to make reading candumps fast and easy.
|
|
|
|
|
|
|
|
skylabify can be combined with jq and candump to allow for advanced queries.
|
|
|
|
|
|
|
|
Examples:
|
|
|
|
skylabify candump.txt
|
|
|
|
|
|
|
|
candump -L can0 | skylabify -
|
|
|
|
|
|
|
|
skylabify previous_candump.txt | jq <some json query>
|
|
|
|
|
|
|
|
I highly suggest reading the manpages for candump and jq. The -L option is
|
|
|
|
required for piping candump into skylabify. Likewise, data should be stored with
|
|
|
|
-l.
|
|
|
|
|
|
|
|
`
|
|
|
|
|
|
|
|
app.Flags = []cli.Flag{
|
|
|
|
&cli.BoolFlag{
|
|
|
|
Name: "verbose",
|
|
|
|
Aliases: []string{"v"},
|
|
|
|
},
|
2024-02-12 15:45:23 +00:00
|
|
|
&cli.StringFlag{
|
|
|
|
Name: "format",
|
|
|
|
Aliases: []string{"f"},
|
|
|
|
Usage: "the format of the incoming data. One of 'telem', 'candump'",
|
|
|
|
},
|
2023-05-20 19:53:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
app.Action = run
|
|
|
|
if err := app.Run(os.Args); err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-12 15:45:23 +00:00
|
|
|
// 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 {
|
|
|
|
return fmt.Sprintf("%s:%s", e.msg, e.err.Error())
|
|
|
|
}
|
|
|
|
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) {
|
|
|
|
// 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)
|
|
|
|
b.Timestamp = time.Unix(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.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) {
|
|
|
|
// 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 {
|
|
|
|
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 parseMap = map[string]ParserFunc{
|
|
|
|
"telem": parseTelemLogLine,
|
|
|
|
"candump": parseCanDumpLine,
|
|
|
|
}
|
|
|
|
|
2023-05-20 19:53:56 +00:00
|
|
|
func run(ctx *cli.Context) (err error) {
|
|
|
|
path := ctx.Args().Get(0)
|
|
|
|
if path == "" {
|
2023-07-07 19:03:53 +00:00
|
|
|
fmt.Println("missing input file")
|
2023-05-20 19:53:56 +00:00
|
|
|
cli.ShowAppHelpAndExit(ctx, int(syscall.EINVAL))
|
|
|
|
}
|
|
|
|
|
|
|
|
var istream *os.File
|
|
|
|
if path == "-" {
|
|
|
|
istream = os.Stdin
|
|
|
|
} else {
|
|
|
|
istream, err = os.Open(path)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-12 15:45:23 +00:00
|
|
|
fileReader := bufio.NewReader(istream)
|
|
|
|
|
|
|
|
var pfun ParserFunc
|
|
|
|
|
|
|
|
pfun, ok := parseMap[ctx.String("format")]
|
|
|
|
if !ok {
|
|
|
|
fmt.Println("invalid format!")
|
|
|
|
cli.ShowAppHelpAndExit(ctx, int(syscall.EINVAL))
|
|
|
|
}
|
|
|
|
|
|
|
|
n_err := 0
|
|
|
|
unknown_packets := 0
|
2023-05-20 19:53:56 +00:00
|
|
|
|
|
|
|
for {
|
2024-02-12 15:45:23 +00:00
|
|
|
line, err := fileReader.ReadString('\n')
|
2023-05-20 19:53:56 +00:00
|
|
|
if err != nil {
|
|
|
|
if errors.Is(err, io.EOF) {
|
|
|
|
return nil
|
|
|
|
}
|
2024-02-12 15:45:23 +00:00
|
|
|
return err // i/o failures are fatal
|
2023-05-20 19:53:56 +00:00
|
|
|
}
|
2024-02-12 15:45:23 +00:00
|
|
|
f, err := pfun(line)
|
2023-06-28 01:39:57 +00:00
|
|
|
var idErr *skylab.UnknownIdError
|
|
|
|
if errors.As(err, &idErr) {
|
|
|
|
// unknown id
|
|
|
|
slog.Info("unknown id", "err", err)
|
2024-02-12 15:45:23 +00:00
|
|
|
unknown_packets++
|
2023-07-01 03:08:06 +00:00
|
|
|
continue
|
2023-06-28 01:39:57 +00:00
|
|
|
} else if err != nil {
|
2024-02-12 15:45:23 +00:00
|
|
|
// TODO: we should consider absorbing all errors.
|
|
|
|
fmt.Printf("got an error %v\n", err)
|
|
|
|
n_err++
|
|
|
|
continue
|
2023-05-20 19:53:56 +00:00
|
|
|
}
|
|
|
|
|
2023-05-22 13:50:07 +00:00
|
|
|
// format and print out the JSON.
|
2024-02-12 15:45:23 +00:00
|
|
|
out, _ := json.Marshal(&f)
|
2023-05-22 13:50:07 +00:00
|
|
|
fmt.Println(string(out))
|
2023-05-20 19:53:56 +00:00
|
|
|
|
|
|
|
}
|
|
|
|
}
|