diff --git a/cmd/gotelem/cli/server.go b/cmd/gotelem/cli/server.go index 4204e38..904481c 100644 --- a/cmd/gotelem/cli/server.go +++ b/cmd/gotelem/cli/server.go @@ -1,13 +1,18 @@ package cli import ( + "encoding/binary" "fmt" "net" + "os" + "strings" "time" "github.com/kschamplin/gotelem" "github.com/kschamplin/gotelem/socketcan" + "github.com/kschamplin/gotelem/xbee" "github.com/urfave/cli/v2" + "golang.org/x/exp/slog" ) var serveCmd = &cli.Command{ @@ -15,49 +20,116 @@ var serveCmd = &cli.Command{ Aliases: []string{"server", "s"}, Usage: "Start a telemetry server", Flags: []cli.Flag{ - &cli.BoolFlag{Name: "xbee", Aliases: []string{"x"}, Usage: "Find and connect to an XBee"}, - }, - Action: func(ctx *cli.Context) error { - serve(ctx.Bool("xbee")) - return nil + &cli.BoolFlag{Name: "test", Usage: "use vcan0 test"}, + &cli.StringFlag{ + Name: "device", + Aliases: []string{"d"}, + Usage: "The XBee to connect to", + EnvVars: []string{"XBEE_DEVICE"}, + }, + &cli.StringFlag{ + Name: "can", + Aliases: []string{"c"}, + Usage: "CAN device string", + EnvVars: []string{"CAN_DEVICE"}, + }, + &cli.StringFlag{ + Name: "logfile", + Aliases: []string{"l"}, + Value: "log.txt", + Usage: "file to store log to", + }, }, + Action: serve, } -func serve(useXbee bool) { +func serve(cCtx *cli.Context) error { + // TODO: output both to stderr and a file. + logger := slog.New(slog.NewTextHandler(os.Stderr)) + slog.SetDefault(logger) broker := gotelem.NewBroker(3) + + done := make(chan struct{}) // start the can listener - go vcanTest() - go canHandler(broker) + // can logger. + go CanDump(broker, logger.WithGroup("candump"), done) + + + if cCtx.String("device") != "" { + logger.Info("using xbee device") + transport, err := xbee.ParseDeviceString(cCtx.String("device")) + if err != nil { + logger.Error("failed to open device string", "err", err) + os.Exit(1) + } + go XBeeSend(broker, logger.WithGroup("xbee"), done, transport) + } + + if cCtx.String("can") != "" { + logger.Info("using can device") + go canHandler(broker, logger.With("device", cCtx.String("can")), done, cCtx.String("can")) + + if strings.HasPrefix(cCtx.String("can"), "v") { + go vcanTest(cCtx.String("can")) + } + } + go broker.Start() + + // tcp listener server. ln, err := net.Listen("tcp", ":8082") if err != nil { fmt.Printf("Error listening: %v\n", err) } - fmt.Printf("Listening on :8082\n") + logger.Info("TCP listener started", "addr", ln.Addr().String()) for { conn, err := ln.Accept() if err != nil { fmt.Printf("error accepting: %v\n", err) } - go handleCon(conn, broker) + go handleCon(conn, broker, logger.WithGroup("tcp"), done) } } -func handleCon(conn net.Conn, broker *gotelem.Broker) { +func handleCon(conn net.Conn, broker *gotelem.Broker, l *slog.Logger, done <-chan struct{}) { // reader := msgp.NewReader(conn) - conn.Close() -} + subname := fmt.Sprint("hi", conn.RemoteAddr().String()) -func xbeeSvc(b *gotelem.Broker) { + rxCh := broker.Subscribe(subname) + defer broker.Unsubscribe(subname) + defer conn.Close() + + for { + select { + case msg := <-rxCh: + // FIXME: poorly optimized + buf := make([]byte, 0, 8) + binary.LittleEndian.AppendUint32(buf, msg.Id) + buf = append(buf, msg.Data...) + + _, err := conn.Write(buf) + if err != nil { + l.Warn("error writing tcp packet", "err", err) + } + case <-done: + return + + } + } } + // this spins up a new can socket on vcan0 and broadcasts a packet every second. for testing. -func vcanTest() { - sock, _ := socketcan.NewCanSocket("vcan0") +func vcanTest(devname string) { + sock, err := socketcan.NewCanSocket(devname) + if err != nil { + slog.Error("error opening socket", "err", err) + return + } testFrame := &gotelem.Frame{ Id: 0x234, Kind: gotelem.CanSFFFrame, @@ -65,31 +137,106 @@ func vcanTest() { } for { - fmt.Printf("sending test packet\n") + slog.Info("sending test packet") sock.Send(testFrame) time.Sleep(1 * time.Second) } } -func canHandler(broker *gotelem.Broker) { +// connects the broker to a socket can +func canHandler(broker *gotelem.Broker, l *slog.Logger, done <-chan struct{}, devname string) { rxCh := broker.Subscribe("socketcan") - sock, _ := socketcan.NewCanSocket("vcan0") + sock, err := socketcan.NewCanSocket(devname) + + if err != nil { + l.Error("error opening socket", "err", err) + return + } // start a simple dispatcher that just relays can frames. rxCan := make(chan gotelem.Frame) go func() { for { - pkt, _ := sock.Recv() + pkt, err := sock.Recv() + if err != nil { + l.Warn("error reading SocketCAN", "err", err) + return + } rxCan <- *pkt } }() for { select { case msg := <-rxCh: + l.Info("Sending a CAN bus message", "id", msg.Id, "data", msg.Data) sock.Send(&msg) case msg := <-rxCan: - fmt.Printf("got a packet from the can %v\n", msg) + l.Info("Got a CAN bus message", "id", msg.Id, "data", msg.Data) broker.Publish("socketcan", msg) + case <-done: + sock.Close() + return } } } + +func CanDump(broker *gotelem.Broker, l *slog.Logger, done <-chan struct{}) { + rxCh := broker.Subscribe("candump") + t := time.Now() + fname := fmt.Sprintf("candump_%d-%02d-%02dT%02d.%02d.%02d.txt", + t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second()) + + cw, err := gotelem.OpenCanWriter(fname) + if err != nil { + slog.Error("error opening file", "err", err) + } + + for { + select { + case msg := <-rxCh: + + cw.Send(&msg) + case <-done: + cw.Close() + return + } + } +} + + +func XBeeSend(broker *gotelem.Broker, l *slog.Logger, done <-chan struct{}, trspt *xbee.Transport) { + rxCh := broker.Subscribe("xbee") + l.Info("starting xbee send routine") + + xb, err := xbee.NewSession(trspt, l.With("device", trspt.Type())) + + if err != nil { + l.Error("failed to start xbee session", "err", err) + return + } + + l.Info("connected to local xbee", "addr", xb.LocalAddr()) + + for { + select { + case <-done: + xb.Close() + return + case msg := <-rxCh: + // TODO: take can message and send it over CAN. + l.Info("got msg", "msg", msg) + buf := make([]byte, 0) + + binary.LittleEndian.AppendUint32(buf, msg.Id) + buf = append(buf, msg.Data...) + + _, err := xb.Write(buf) + if err != nil { + l.Warn("error writing to xbee", "err", err) + } + + } + } + + +} diff --git a/cmd/gotelem/cli/xbee.go b/cmd/gotelem/cli/xbee.go index b15ee34..87c38f9 100644 --- a/cmd/gotelem/cli/xbee.go +++ b/cmd/gotelem/cli/xbee.go @@ -22,6 +22,15 @@ const ( keyIODevice ctxKey = iota ) +var xbeeDeviceFlag = &cli.StringFlag{ + Name: "device", + Aliases: []string{"d"}, + Usage: "The XBee to connect to", + Required: true, + EnvVars: []string{"XBEE_DEVICE"}, + } + + var xbeeCmd = &cli.Command{ Name: "xbee", Aliases: []string{"x"}, @@ -40,13 +49,7 @@ TCP/UDP connections require a port and will fail if one is not provided. `, Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "device", - Aliases: []string{"d"}, - Usage: "The XBee to connect to", - Required: true, - EnvVars: []string{"XBEE_DEVICE"}, - }, + xbeeDeviceFlag, }, // this parses the device string and creates the io device. // TODO: should we create the session here instead? @@ -97,7 +100,7 @@ func xbeeInfo(ctx *cli.Context) error { return cli.Exit(err, 1) } - b, err := xb.ATCommand([2]rune{'I', 'D'}, nil, false) + b, err := xb.ATCommand([2]byte{'I', 'D'}, nil, false) if err != nil { return cli.Exit(err, 1) } diff --git a/gen_skylab.go b/gen_skylab.go deleted file mode 100644 index d313eff..0000000 --- a/gen_skylab.go +++ /dev/null @@ -1,91 +0,0 @@ -//go:build ignore - -// this file is a generator for skylab code. -package main - - -import ( - "gopkg.in/yaml.v3" -) - -type Field interface { - Name() string - - Size() int // the size of the data. - - // returns something like - // AuxVoltage uint16 - // used inside the packet struct - Embed() string - - // returns - Marshal() string - Decode() string -} - -// this is a standard field, not a bitfield. -type DataField struct { - Name string - Type string - Units string // mostly for documentation - Conversion float32 -} - - -// a PacketDef is a full can packet. -type PacketDef struct { - Name string - Description string - Id uint32 - BigEndian bool - data: []Field -} - -// we need to generate bitfield types. -// packet structs per each packet -// constancts for packet IDs or a map. - - -/* - - -example for a simple packet type -it also needs a json marshalling. - - type BMSMeasurement struct { - BatteryVoltage uint16 - AuxVoltage uint16 - Current float32 - } - - func (b *BMSMeasurement)MarshalPacket() ([]byte, error) { - pkt := make([]byte, b.Size()) - binary.LittleEndian.PutUint16(pkt[0:], b.BatteryVoltage * 0.01) - binary.LittleEndian.PutUint16(pkt[2:],b.AuxVoltage * 0.001) - binary.LittleEndian.PutFloat32(b.Current) // TODO: make float function - } - - func (b *BMSMeasurement)UnmarshalPacket(p []byte) error { - - } - - func (b *BMSMeasurement) Id() uint32 { - return 0x010 - } - - func (b *BMSMeasurement) Size() int { - return 8 - } - - func (b *BMSMeasurement) String() string { - return "blah blah" - } - -we also need some kind of mechanism to lookup data type. - - func getPkt (id uint32, data []byte) (Packet, error) { - - // insert really massive switch case statement here. - } - -*/ diff --git a/go.mod b/go.mod index 5568f3c..0cd6391 100644 --- a/go.mod +++ b/go.mod @@ -19,4 +19,5 @@ require ( github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/stretchr/testify v1.8.0 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/skylab/gen_skylab.go b/skylab/gen_skylab.go new file mode 100644 index 0000000..0b753a3 --- /dev/null +++ b/skylab/gen_skylab.go @@ -0,0 +1,263 @@ +//go:build ignore +// +build ignore + +// this file is a generator for skylab code. +package main + +import ( + "encoding/binary" + "fmt" + "os" + "strings" + "text/template" + + "golang.org/x/exp/slog" + "gopkg.in/yaml.v3" +) + +// data field. +type DataField struct { + Name string + Type string + Units string // mostly for documentation + Conversion float32 + Bits []struct { + Name string + } +} + +// a PacketDef is a full can packet. +type PacketDef struct { + Name string + Description string + Id uint32 + BigEndian bool + Repeat int + Offset int + Data []DataField +} + +// we need to generate bitfield types. +// packet structs per each packet +// constancts for packet IDs or a map. + +/* + + +example for a simple packet type +it also needs a json marshalling. + + type BMSMeasurement struct { + BatteryVoltage uint16 + AuxVoltage uint16 + Current float32 + } + + func (b *BMSMeasurement)MarshalPacket() ([]byte, error) { + pkt := make([]byte, b.Size()) + binary.LittleEndian.PutUint16(pkt[0:], b.BatteryVoltage * 0.01) + binary.LittleEndian.PutUint16(pkt[2:],b.AuxVoltage * 0.001) + binary.LittleEndian.PutFloat32(b.Current) // TODO: make float function + } + + func (b *BMSMeasurement)UnmarshalPacket(p []byte) error { + // the opposite of above. + + } + + func (b *BMSMeasurement) Id() uint32 { + return 0x010 + } + + func (b *BMSMeasurement) Size() int { + return 8 + } + + func (b *BMSMeasurement) String() string { + return "blah blah" + } + +we also need some kind of mechanism to lookup data type. + + func getPkt (id uint32, data []byte) (Packet, error) { + + // insert really massive switch case statement here. + } + +*/ + +var test = ` +packets: + - name: dashboard_pedal_percentages + description: ADC values from the brake and accelerator pedals. + id: 0x290 + endian: little + frequency: 10 + data: + - name: accel_pedal_value + type: uint8_t + - name: brake_pedal_value + type: uint8_t +` + +type SkylabFile struct { + Packets []PacketDef +} + +var typeMap = map[string]string{ + "uint16_t": "uint16", + "uint32_t": "uint32", + "uint64_t": "uint64", + "uint8_t": "uint8", + "float": "float32", + + "int16_t": "int16", + "int32_t": "int32", + "int64_t": "int64", + "int8_t": "int8", +} + +var typeSizeMap = map[string]uint{ + "uint16_t": 2, + "uint32_t": 4, + "uint64_t": 8, + "uint8_t": 1, + "float": 4, + + "int16_t": 2, + "int32_t": 4, + "int64_t": 8, + "int8_t": 1, + "bitfield": 1, +} + +func (d *DataField) ToStructMember() string { + if d.Type != "bitfield" { + return toCamelInitCase(d.Name, true) + " " + typeMap[d.Type] + } + // it's a bitfield, things are more complicated. + slog.Warn("bitfields are skipped for now") + return "" +} + + + +func (p PacketDef) Size() int { + // makes a function that returns the size of the code. + + var size int = 0 + for _, val := range p.Data { + size += int(typeSizeMap[val.Type]) + } + + return size +} + + +func (p PacketDef) MakeMarshal() string { + var buf strings.Builder + + var offset int = 0 + // we have a b []byte as the correct-size byte array to store in. + // and the packet itself is represented as `p` + for _, val := range p.Data { + if val.Type == "uint8_t" || val.Type == "int8_t" { + buf.WriteString(fmt.Sprintf("b[%d] = p.%s\n", offset, toCamelInitCase(val.Name, true))) + } else if val.Type == "bitfield" { + + } else if val.Type == "float" { + + } else if name,ok := typeMap[val.Type]; ok { + + } + + + offset += int(typeSizeMap[val.Type]) + } + + return "" +} + +var templ = ` +// go code generated! don't touch! +{{ $structName := camelCase .Name true}} +// {{$structName}} is {{.Description}} +type {{$structName}} struct { +{{- range .Data}} + {{.ToStructMember}} +{{- end}} +} + +func (p *{{$structName}}) Id() uint32 { + return {{.Id}} +} + +func (p *{{$structName}}) Size() int { + return {{.Size}} +} +` + + +// stolen camelCaser code. initCase = true means CamelCase, false means camelCase +func toCamelInitCase(s string, initCase bool) string { + s = strings.TrimSpace(s) + if s == "" { + return s + } + + n := strings.Builder{} + n.Grow(len(s)) + capNext := initCase + for i, v := range []byte(s) { + vIsCap := v >= 'A' && v <= 'Z' + vIsLow := v >= 'a' && v <= 'z' + if capNext { + if vIsLow { + v += 'A' + v -= 'a' + } + } else if i == 0 { + if vIsCap { + v += 'a' + v -= 'A' + } + } + if vIsCap || vIsLow { + n.WriteByte(v) + capNext = false + } else if vIsNum := v >= '0' && v <= '9'; vIsNum { + n.WriteByte(v) + capNext = true + } else { + capNext = v == '_' || v == ' ' || v == '-' || v == '.' + } + } + return n.String() +} + +func main() { + v := &SkylabFile{} + + err := yaml.Unmarshal([]byte(test), v) + if err != nil { + fmt.Printf("err %v", err) + } + + fmt.Printf("%#v\n", v.Packets) + + fnMap := template.FuncMap{ + "camelCase": toCamelInitCase, + } + tmpl, err := template.New("packet").Funcs(fnMap).Parse(templ) + + if err != nil { + panic(err) + } + + err = tmpl.Execute(os.Stdout, v.Packets[0]) + + if err != nil { + panic(err) + } + +} diff --git a/xbee/at.go b/xbee/at.go index dc000eb..e2c5e23 100644 --- a/xbee/at.go +++ b/xbee/at.go @@ -35,7 +35,7 @@ func (b RawATCmd) Bytes() []byte { // EncodeATCommand takes an AT command and encodes it in the payload format. // it takes the frame index (which can be zero) as well as if it should be queued or // not. It encodes the AT command to be framed and sent over the wire and returns the packet -func encodeATCommand(cmd [2]rune, p []byte, idx uint8, queued bool) RawATCmd { +func encodeATCommand(cmd [2]byte, p []byte, idx uint8, queued bool) RawATCmd { // we encode a new byte slice that contains the cmd + payload concatenated correclty. // this is then used to make the command frame, which contains ID/Type/Queued or not. // the ATCmdFrame can be converted to bytes to be sent over the wire once framed. diff --git a/xbee/at_test.go b/xbee/at_test.go index 611e41c..2389a53 100644 --- a/xbee/at_test.go +++ b/xbee/at_test.go @@ -90,7 +90,7 @@ func Test_encodeRemoteATCommand(t *testing.T) { func Test_encodeATCommand(t *testing.T) { type args struct { - cmd [2]rune + cmd [2]byte p []byte idx uint8 queued bool @@ -104,7 +104,7 @@ func Test_encodeATCommand(t *testing.T) { { name: "Setting AT Command", args: args{ - cmd: [2]rune{'N', 'I'}, + cmd: [2]byte{'N', 'I'}, idx: 0xA1, p: []byte{0x45, 0x6E, 0x64, 0x20, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65}, queued: false, @@ -114,7 +114,7 @@ func Test_encodeATCommand(t *testing.T) { { name: "Query AT Command", args: args{ - cmd: [2]rune{'T', 'P'}, + cmd: [2]byte{'T', 'P'}, idx: 0x17, p: nil, queued: false, @@ -124,7 +124,7 @@ func Test_encodeATCommand(t *testing.T) { { name: "Queue Local AT Command", args: args{ - cmd: [2]rune{'B', 'D'}, + cmd: [2]byte{'B', 'D'}, idx: 0x53, p: []byte{0x07}, queued: true, @@ -134,7 +134,7 @@ func Test_encodeATCommand(t *testing.T) { { name: "Queue Query AT Command", args: args{ - cmd: [2]rune{'T', 'P'}, + cmd: [2]byte{'T', 'P'}, idx: 0x17, p: nil, queued: true, diff --git a/xbee/session.go b/xbee/session.go index fcea905..b5b2551 100644 --- a/xbee/session.go +++ b/xbee/session.go @@ -55,6 +55,9 @@ type Session struct { // can only be one direct connection to a device. This is pretty reasonable IMO. // but needs to be documented very clearly. conns map[uint64]*Conn + + // local address + lAddr XBeeAddr } // NewSession takes an IO device and a logger and returns a new XBee session. @@ -76,6 +79,21 @@ func NewSession(dev io.ReadWriteCloser, baseLog *slog.Logger) (*Session, error) go sess.rxHandler() + + // now we should get the local address cached so LocalAddr is fast. + sh, err := sess.ATCommand([2]byte{'S', 'H'}, nil, false) + if err != nil { + return sess, errors.New("error getting SH") + } + sl, err := sess.ATCommand([2]byte{'S', 'L'}, nil, false) + if err != nil { + return sess, errors.New("error getting SL") + } + + addr := append(sh, sl...) + + sess.lAddr = XBeeAddr(binary.BigEndian.Uint64(addr)) + return sess, nil } @@ -90,10 +108,13 @@ func (sess *Session) rxHandler() { scan := bufio.NewScanner(sess.ioDev) scan.Split(xbeeFrameSplit) + sess.log.Debug("starting rx handler", "device", sess.ioDev) + // scan.Scan() will return false when there's EOF, i.e the io device is closed. // this is activated by sess.Close() for scan.Scan() { data, err := parseFrame(scan.Bytes()) + sess.log.Debug("got an api frame", "data", data) if err != nil { sess.log.Warn("error parsing frame", "error", err, "data", data) continue @@ -207,7 +228,7 @@ func (sess *Session) writeAddr(p []byte, dest uint64) (n int, err error) { // instead, an AC command must be set to apply the queued changes. `queued` does not // affect query-type commands, which always return right away. // the AT command is an interface. -func (sess *Session) ATCommand(cmd [2]rune, data []byte, queued bool) (payload []byte, err error) { +func (sess *Session) ATCommand(cmd [2]byte, data []byte, queued bool) (payload []byte, err error) { // we must encode the command, and then create the actual packet. // then we send the packet, and wait for the response // TODO: how to handle multiple-response-packet AT commands? @@ -262,12 +283,7 @@ func (sess *Session) Close() error { } func (sess *Session) LocalAddr() XBeeAddr { - // TODO: should we get this once at the start? and then just store it? - sh, _ := sess.ATCommand([2]rune{'S', 'H'}, nil, false) - sl, _ := sess.ATCommand([2]rune{'S', 'L'}, nil, false) - - addr := uint64(binary.BigEndian.Uint32(sh)) << 32 & uint64(binary.BigEndian.Uint32(sl)) - return XBeeAddr(addr) + return sess.lAddr } func (sess *Session) RemoteAddr() XBeeAddr { diff --git a/xbee/session_test.go b/xbee/session_test.go index 176fbde..a7cdf0c 100644 --- a/xbee/session_test.go +++ b/xbee/session_test.go @@ -47,9 +47,9 @@ func TestXBeeHardware(t *testing.T) { // now we should test sending a packet. and getting a response. t.Run("Get Network ID", func(t *testing.T) { - b, err := sess.ATCommand([2]rune{'I', 'D'}, nil, false) + b, err := sess.ATCommand([2]byte{'I', 'D'}, nil, false) if err != nil { - t.Errorf("ATCommand() error = %v", err) + t.Fatalf("ATCommand() error = %v", err) } if len(b) != 2 { t.Errorf("reponse length mismatch: expected 2 got %d", len(b)) @@ -57,10 +57,10 @@ func TestXBeeHardware(t *testing.T) { }) t.Run("Check NP", func(t *testing.T) { - b, err := sess.ATCommand([2]rune{'N', 'P'}, nil, false) + b, err := sess.ATCommand([2]byte{'N', 'P'}, nil, false) if err != nil { - t.Errorf("ATCommand() error = %v", err) + t.Fatalf("ATCommand() error = %v", err) } val := binary.BigEndian.Uint16(b) @@ -69,6 +69,27 @@ func TestXBeeHardware(t *testing.T) { } }) + + t.Run("check source address", func(t *testing.T) { + a := sess.LocalAddr() + + t.Logf("local device address is %v", a) + + }) + + + t.Run("Check device name", func(t *testing.T) { + a, err := sess.ATCommand([2]byte{'N', 'I'}, nil, false) + + if err != nil { + t.Fatalf("Could not run NI: %v", err) + } + + name := string(a) + t.Logf("Device Name: %s", name) + }) + + } func TestParseDeviceString(t *testing.T) {