From 268e784cb5f3a952102323c93c0ec4d0672e2db6 Mon Sep 17 00:00:00 2001 From: Terekhin Alexandr Date: Mon, 12 Dec 2022 12:57:02 +0300 Subject: [PATCH] Initial commit --- .gitignore | 4 + .idea/.gitignore | 8 ++ can/can.go | 141 ++++++++++++++++++++++++ can/input.go | 96 ++++++++++++++++ cli-mon.go | 44 ++++++++ go.mod | 14 +++ go.sum | 10 ++ ui/helper.go | 43 ++++++++ ui/model.go | 81 ++++++++++++++ ui/ui.go | 88 +++++++++++++++ ui/view.go | 224 +++++++++++++++++++++++++++++++++++++ yabl/protocol.go | 280 +++++++++++++++++++++++++++++++++++++++++++++++ yabl/strings.go | 129 ++++++++++++++++++++++ 13 files changed, 1162 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 can/can.go create mode 100644 can/input.go create mode 100644 cli-mon.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 ui/helper.go create mode 100644 ui/model.go create mode 100644 ui/ui.go create mode 100644 ui/view.go create mode 100644 yabl/protocol.go create mode 100644 yabl/strings.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4124709 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/cli-mon +/.idea/cli-mon.iml +/.idea/modules.xml +/.idea/vcs.xml diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/can/can.go b/can/can.go new file mode 100644 index 0000000..f441dc4 --- /dev/null +++ b/can/can.go @@ -0,0 +1,141 @@ +package can + +import ( + "encoding/binary" + "errors" + "io" + "net" + "time" + "unsafe" + + "golang.org/x/sys/unix" +) + +const CAN_ID_071 = 0x071 +const CAN_ID_073 = 0x073 +const CAN_ID_021 = 0x021 +const CAN_ID_022 = 0x022 + +type CanFrame struct { + CanId uint32 + Payload []uint8 + Date string +} + +// NewCan New returns a new CAN bus socket. +func NewCan() (*Socket, error) { + fd, err := unix.Socket(unix.AF_CAN, unix.SOCK_RAW, unix.CAN_RAW) + if err != nil { + return nil, err + } + + return &Socket{dev: device{fd}}, nil +} + +// Socket is a high-level representation of a CANBus socket. +type Socket struct { + iface *net.Interface + addr *unix.SockaddrCAN + dev device +} + +// Close closes the CAN bus socket. +func (sck *Socket) Close() error { + return unix.Close(sck.dev.fd) +} + +func (sck *Socket) Bind(addr string, filter []unix.CanFilter) error { + iface, err := net.InterfaceByName(addr) + if err != nil { + return err + } + + sck.iface = iface + sck.addr = &unix.SockaddrCAN{Ifindex: sck.iface.Index} + + //add filter + if filter != nil { + unix.SetsockoptCanRawFilter(sck.dev.fd, unix.SOL_CAN_RAW, unix.CAN_RAW_FILTER, filter) + } + + return unix.Bind(sck.dev.fd, sck.addr) +} + +// Recv receives data from the CAN socket. +// id is the CAN_frame id the data was originated from. +func (sck *Socket) Recv() (id uint32, data []byte, err error) { + var buf [frameSize]byte + n, err := io.ReadFull(sck.dev, buf[:]) + if err != nil { + return id, data, err + } + + if n != len(buf) { + return id, data, io.ErrUnexpectedEOF + } + + id = binary.LittleEndian.Uint32(buf[:4]) + //TODO make correct switch betwin EFF and SFF + id &= unix.CAN_EFF_MASK + data = make([]byte, buf[4]) + copy(data, buf[8:]) + return id, data, nil +} + +type device struct { + fd int +} + +func (d device) Read(data []byte) (int, error) { + return unix.Read(d.fd, data) +} + +func (d device) Write(data []byte) (int, error) { + return unix.Write(d.fd, data) +} + +const frameSize = unsafe.Sizeof(canframe{}) + +// frame is a can_frame. +type canframe struct { + ID uint32 + Len byte + _ [3]byte + Data [8]byte +} + +func StartCan(canbus string) (<-chan *CanFrame, error) { + + var filter []unix.CanFilter = []unix.CanFilter{ + {Id: CAN_ID_071, Mask: 0xFFF}, + {Id: CAN_ID_073, Mask: 0xFFF}, + {Id: CAN_ID_021, Mask: 0xFFF}, + {Id: CAN_ID_022, Mask: 0xFFF}, + } + + if socket, err := NewCan(); err == nil { + + if cerr := socket.Bind(canbus, filter); cerr == nil { + + canevents := make(chan *CanFrame) + + go func() { + defer close(canevents) + defer socket.Close() + + for { //TODO implement stop + if id, data, serr := socket.Recv(); serr == nil { + canevents <- &CanFrame{ + Date: time.Now().String(), + CanId: id, + Payload: data, + } + } + } + }() + + return canevents, nil + } + } + return nil, errors.New("can't bind can socket") +} diff --git a/can/input.go b/can/input.go new file mode 100644 index 0000000..732332e --- /dev/null +++ b/can/input.go @@ -0,0 +1,96 @@ +package can + +import ( + "bufio" + "fmt" + "os" + "regexp" + "strconv" +) + +// (2022-07-08 16:54:15.587099) can0 100 [8] 00 00 00 00 00 00 64 00 +// (1670578900.771868) can0 00004021#00000000FCFFFFFF +const EXPR_HR = "\\((.*)\\)\\s+can\\d+\\s+(\\S+)\\s+\\[\\d\\]\\s+([A-F0-9]+)\\s+([A-F0-9]+)\\s+([A-F0-9]+)\\s+([A-F0-9]+)\\s+([A-F0-9]+)\\s+([A-F0-9]+)\\s+([A-F0-9]+)\\s+([A-F0-9]+)" +const EXPR_M = "\\((.*)\\)\\s+can\\d+\\s+(\\S+)#([A-F0-9]{2})([A-F0-9]{2})([A-F0-9]{2})([A-F0-9]{2})([A-F0-9]{2})([A-F0-9]{2})([A-F0-9]{2})([A-F0-9]{2})" +const length = 11 + +var patternHr = regexp.MustCompile(EXPR_HR) +var patternM = regexp.MustCompile(EXPR_M) + +func Readfile(filename *string) <-chan *CanFrame { + c := make(chan *CanFrame) + + go func() { + defer close(c) + + file, err := os.Open(*filename) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + defer file.Close() + + scanner := bufio.NewScanner(file) + process(scanner, c) + }() + + return c +} + +func process(scanner *bufio.Scanner, ch chan<- *CanFrame) { + for scanner.Scan() { + text := scanner.Text() + if frame := fromString(&text); frame != nil { + ch <- frame + } + } + + if err := scanner.Err(); err != nil { + fmt.Println(err) + } +} + +func ReadStdin() <-chan *CanFrame { + c := make(chan *CanFrame) + + go func() { + scanner := bufio.NewScanner(os.Stdin) + process(scanner, c) + }() + return c +} + +func fromString(text *string) *CanFrame { + var submatch []string + if submatch = patternHr.FindStringSubmatch(*text); submatch == nil { + if submatch = patternM.FindStringSubmatch(*text); submatch == nil { + return nil + } + } + + if len(submatch) == length { + canid, err := strconv.ParseUint(submatch[2], 16, 0) + if err != nil { + return nil + } + + payload := make([]uint8, 8) + for idx, octet := range submatch[3:] { + val, err := strconv.ParseUint(octet, 16, 0) + if err != nil { + fmt.Println(err) + continue + } + + payload[idx] = uint8(val) + } + + return &CanFrame{ + Date: submatch[1], + CanId: uint32(canid), + Payload: payload, + } + } + + return nil +} diff --git a/cli-mon.go b/cli-mon.go new file mode 100644 index 0000000..262890d --- /dev/null +++ b/cli-mon.go @@ -0,0 +1,44 @@ +package main + +import ( + "cli-mon/can" + "cli-mon/ui" + "cli-mon/yabl" + "flag" + "fmt" + "os" +) + +func main() { + filename := flag.String("f", "", "Candump filename") + canbus := flag.String("i", "", "CAN bus interface") + stdin := flag.Bool("s", false, "Read from stdin") + flag.Parse() + + var frames <-chan *can.CanFrame + + switch { + case *stdin: + frames = can.ReadStdin() + case len(*filename) > 0: + frames = can.Readfile(filename) + case len(*canbus) > 0: + var err error + frames, err = can.StartCan(*canbus) + if err != nil { + fmt.Println(err) + return + } + default: + flag.Usage() + } + + if frames == nil { + fmt.Println("Get no data") + os.Exit(0) + } + + var messages = yabl.StartProtocolParsing(frames) + + ui.InitCliApp(messages) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..5950aad --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module cli-mon + +go 1.19 + +require ( + github.com/gizak/termui/v3 v3.1.0 + golang.org/x/sys v0.3.0 +) + +require ( + github.com/mattn/go-runewidth v0.0.2 // indirect + github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect + github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..155a7a8 --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/gizak/termui/v3 v3.1.0 h1:ZZmVDgwHl7gR7elfKf1xc4IudXZ5qqfDh4wExk4Iajc= +github.com/gizak/termui/v3 v3.1.0/go.mod h1:bXQEBkJpzxUAKf0+xq9MSWAvWZlE7c+aidmyFlkYTrY= +github.com/mattn/go-runewidth v0.0.2 h1:UnlwIPBGaTZfPQ6T1IGzPI0EkYAQmT9fAEJ/poFC63o= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM= +github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d h1:x3S6kxmy49zXVVyhcnrFqxvNVCBPb2KZ9hV2RBdS840= +github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ= +golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/ui/helper.go b/ui/helper.go new file mode 100644 index 0000000..0ab17a9 --- /dev/null +++ b/ui/helper.go @@ -0,0 +1,43 @@ +package ui + +import "cli-mon/yabl" + +var fNewCIS = func(packet *yabl.ContactorInternalState) *contactorType { + return &contactorType{state: packet} +} +var fGetCIS = func(c *contactorType) *yabl.ContactorInternalState { + return c.state +} +var fSetCIS = func(c *contactorType, packet *yabl.ContactorInternalState) { + c.state = packet +} + +var fNewCES = func(packet *yabl.ContactorInternalErrors) *contactorType { + return &contactorType{errors: packet} +} +var fGetCES = func(c *contactorType) *yabl.ContactorInternalErrors { + return c.errors +} +var fSetCES = func(c *contactorType, packet *yabl.ContactorInternalErrors) { + c.errors = packet +} + +var fNewPPE = func(packet *yabl.PuPresentEnergy) *connectorType { + return &connectorType{energy: packet} +} +var fGetPPE = func(c *connectorType) *yabl.PuPresentEnergy { + return c.energy +} +var fSetPPE = func(c *connectorType, packet *yabl.PuPresentEnergy) { + c.energy = packet +} + +var fNewPP = func(packet *yabl.PuPeriphery) *connectorType { + return &connectorType{state: packet} +} +var fGetPP = func(c *connectorType) *yabl.PuPeriphery { + return c.state +} +var fSetPP = func(c *connectorType, packet *yabl.PuPeriphery) { + c.state = packet +} diff --git a/ui/model.go b/ui/model.go new file mode 100644 index 0000000..7622406 --- /dev/null +++ b/ui/model.go @@ -0,0 +1,81 @@ +package ui + +import ( + "cli-mon/yabl" + "github.com/gizak/termui/v3/widgets" + "reflect" +) + +type connectorType struct { + energy *yabl.PuPresentEnergy + state *yabl.PuPeriphery +} + +type contactorType struct { + state *yabl.ContactorInternalState + errors *yabl.ContactorInternalErrors +} + +type modelTypes interface { + *connectorType | *contactorType +} + +type dataTypes interface { + *yabl.PuPresentEnergy | *yabl.PuPeriphery | *yabl.ContactorInternalState | *yabl.ContactorInternalErrors +} + +type fNewType[T modelTypes, V dataTypes] func(e V) T +type fGetType[T modelTypes, V dataTypes] func(m T) V +type fSetType[T modelTypes, V dataTypes] func(m T, e V) + +var connectors = make([]*connectorType, connectorsCount+1) +var contactors = make([]*contactorType, contactorsCount+1) + +var connUI [connectorsCount + 1]*widgets.Paragraph +var contUI [contactorsCount + 1]*widgets.Paragraph +var contactorsErr *widgets.Paragraph + +func process(message yabl.Packet) { + if message == nil { + return + } + + unitId := message.GetUnitId() + + switch msg := message.(type) { + case *yabl.ContactorInternalState: + if updateOrNew(contactors, unitId, msg, fNewCIS, fGetCIS, fSetCIS) { + updateContactorsView(unitId) + } + + case *yabl.ContactorInternalErrors: + if updateOrNew(contactors, unitId, msg, fNewCES, fGetCES, fSetCES) { + updateContactorsStateView() + } + + case *yabl.PuPeriphery: + if updateOrNew(connectors, unitId, msg, fNewPP, fGetPP, fSetPP) { + updateConnectorsView(unitId) + } + + case *yabl.PuPresentEnergy: + if updateOrNew(connectors, unitId, msg, fNewPPE, fGetPPE, fSetPPE) { + updateConnectorsView(unitId) + } + + } +} + +func updateOrNew[T modelTypes, V dataTypes](array []T, id uint, msg V, fNew fNewType[T, V], fGet fGetType[T, V], fSet fSetType[T, V]) bool { + if model := array[id]; model != nil { + if element := fGet(model); !reflect.DeepEqual(element, msg) { + fSet(model, msg) + return true + } + } else { + model = fNew(msg) + array[id] = model + return true + } + return false +} diff --git a/ui/ui.go b/ui/ui.go new file mode 100644 index 0000000..6ae7959 --- /dev/null +++ b/ui/ui.go @@ -0,0 +1,88 @@ +package ui + +import ( + "cli-mon/yabl" + ui "github.com/gizak/termui/v3" + "github.com/gizak/termui/v3/widgets" + "log" + "time" +) + +const headerHeight int = 4 +const contactorsCount = 18 +const connectorsCount = 6 + +func InitCliApp(events <-chan yabl.Packet) { + if err := ui.Init(); err != nil { + log.Fatalf("failed to initialize termui: %v", err) + } + defer ui.Close() + + var connectorsTab *ui.Grid + //var contactorsErr *widgets.Paragraph + var contactorsStateTab *ui.Grid + var header *widgets.Paragraph + + tabpane := widgets.NewTabPane("Connectors", "Contactors States", "Contactors Errors") + tabpane.SetRect(0, 1, 150, 4) + tabpane.Border = true + + header = newHeader() + connectorsTab = newConnectorsView() + contactorsStateTab = newContactorsView() + contactorsErr = newContactorsErrView() + + renderTab := func() { + switch tabpane.ActiveTabIndex { + case 0: + termWidth, termHeight := ui.TerminalDimensions() + connectorsTab.SetRect(0, headerHeight, termWidth, termHeight) + ui.Render(connectorsTab) + case 1: + termWidth, termHeight := ui.TerminalDimensions() + contactorsStateTab.SetRect(0, headerHeight, termWidth, termHeight) + ui.Render(contactorsStateTab) + case 2: + ui.Render(contactorsErr) + } + } + + ui.Render(header, tabpane, connectorsTab) + + uiEvents := ui.PollEvents() + ticker := time.NewTicker(2 * time.Second).C + for { + select { + case e := <-uiEvents: + switch e.ID { // event string/identifier + case "q", "": // press 'q' or 'C-c' to quit + return + case "": + //payload := e.Payload.(ui.Mouse) + //x, y := payload.X, payload.Y + //p.Text = fmt.Sprintf("X = %d, Y = %d", x, y) + case "": + tabpane.FocusLeft() + ui.Clear() + ui.Render(header, tabpane) + renderTab() + case "": + tabpane.FocusRight() + ui.Clear() + ui.Render(header, tabpane) + renderTab() + } + + switch e.Type { + case ui.KeyboardEvent: // handle all key presses + //eventID := e.ID // keypress string + //p.Text = eventID + } + // use Go's built-in tickers for updating and drawing data + case <-ticker: + renderTab() + case message := <-events: + process(message) + } + } +} diff --git a/ui/view.go b/ui/view.go new file mode 100644 index 0000000..5397408 --- /dev/null +++ b/ui/view.go @@ -0,0 +1,224 @@ +package ui + +import ( + "cli-mon/yabl" + "fmt" + ui "github.com/gizak/termui/v3" + "github.com/gizak/termui/v3/widgets" +) + +func newHeader() *widgets.Paragraph { + widget := widgets.NewParagraph() + widget.Text = "Press q to quit, Press or to switch tabs" + widget.SetRect(0, 0, 50, 1) + widget.Border = false + widget.TextStyle.Bg = ui.ColorBlue + return widget +} + +func newConnectorsView() *ui.Grid { + grid := ui.NewGrid() + + termWidth, termHeight := ui.TerminalDimensions() + grid.SetRect(0, headerHeight, termWidth, termHeight) + + var conn [connectorsCount + 1]*widgets.Paragraph //ui.GridItem + + for i := 0; i <= connectorsCount; i++ { + p := widgets.NewParagraph() + p.Text = fmt.Sprintf("No data: #%2.d", i) + p.Title = fmt.Sprintf("Connector: #%2.d", i) + //p.SetRect(0, 0, 50, 30) + p.TextStyle.Bg = ui.ColorBlue + p.TextStyle.Fg = ui.ColorWhite + p.BorderStyle.Fg = ui.ColorYellow + p.Border = true + + connUI[i] = p + conn[i] = p //ui.NewCol(1.0/6, p) + } + + grid.Set( + ui.NewRow( + 1.0/2, + ui.NewCol(1.0/4, conn[1]), + ui.NewCol(1.0/4, conn[2]), + ui.NewCol(1.0/4, conn[3]), + ui.NewCol(1.0/4, conn[4]), + ), + ui.NewRow( + 1.0/2, + ui.NewCol(1.0/4, conn[5]), + ui.NewCol(1.0/4, conn[6]), + ui.NewCol(1.0/4, conn[0]), + ), + ) + + return grid +} + +func newContactorsView() *ui.Grid { + grid := ui.NewGrid() + + termWidth, termHeight := ui.TerminalDimensions() + grid.SetRect(0, headerHeight, termWidth, termHeight) + + var cont [contactorsCount + 1]ui.GridItem + + //var contactors = make([]ui.GridItem, contactorsCount) + for i := 1; i <= contactorsCount; i++ { + p := widgets.NewParagraph() + p.Text = fmt.Sprintf("No data: #%2.d", i) + p.Title = fmt.Sprintf("Contactor: #%2.d", i) + p.Border = false + p.TextStyle.Bg = ui.ColorBlue + p.TextStyle.Fg = ui.ColorWhite + p.BorderStyle.Fg = ui.ColorYellow + p.Border = true + + contUI[i] = p + cont[i] = ui.NewCol(1.0/6, p) + } + + grid.Set( + ui.NewRow( + 1.0/3, + cont[1], + cont[2], + cont[3], + cont[4], + cont[5], + cont[6], + ), + ui.NewRow( + 1.0/3, + cont[7], + cont[8], + cont[9], + cont[10], + cont[11], + cont[12], + ), + ui.NewRow( + 1.0/3, + cont[13], + cont[14], + cont[15], + cont[16], + cont[17], + cont[18], + )) + + //ui.NewRow(1, ui.InterfaceSlice(contactors)...) + grid.Set() + + return grid +} + +func newContactorsErrView() *widgets.Paragraph { + p := widgets.NewParagraph() + p.Text = "No data\n" + p.Title = "Contactors Errors" + p.SetRect(5, 5, 55, 15) + p.BorderStyle.Fg = ui.ColorYellow + + return p +} + +func updateContactorsView(unitId uint) { + p := contUI[unitId] + s := contactors[unitId].state + + if s == nil || p == nil { + return + } + + p.Text = fmt.Sprintf( + "BoardReady: %s\nContactorOn: %s\nUnexpectedState: %s\nIsolated: %s\nDebug: %s", + s.ContactorReady, + s.ContactorOn, + s.UnexpectedState, + s.Isolated, + s.DebugEnabled) + + if s.ContactorReady == yabl.BOARD_READY_OK { + p.TextStyle.Bg = ui.ColorClear + } else { + p.TextStyle.Bg = ui.ColorRed + } + + if s.ContactorOn == yabl.ON { + p.TitleStyle.Bg = ui.ColorBlue + } else { + p.TitleStyle.Bg = ui.ColorClear + } +} + +func updateConnectorsView(unitId uint) { + p := connUI[unitId] + s := connectors[unitId].state + e := connectors[unitId].energy + + if s == nil || e == nil { + return + } + + fstring := "ConnectorInsert %s\n" + + "ContactorOn %s\n" + + "ConnectorLocked %s\n" + + "LineLevel %s\n" + + "IsolationState %s\n" + + "ChargingAllowed %s\n" + + "PwmEnabled %s\n" + + "CpLineVoltage %s\n" + + "V2GMode %s\n" + + "VoltageBefore %s\n" + + "VoltageAfter %s\n" + + "PresentCurrent %s" + + p.Text = fmt.Sprintf( + fstring, + s.ConnectorInsert, + s.ContactorOn, + s.ConnectorLocked, + s.CpLineLevel, + s.IsolationState, + s.ChargingAllowed, + s.PwmEnabled, + s.CpLineVoltage, + e.V2GMode, + e.VoltageBefore, + e.VoltageAfter, + e.PresentCurrent) +} + +func updateContactorsStateView() { + p := contactorsErr + + if contactors[1] == nil { + return + } + + e := contactors[1].errors + + if e == nil { + return + } + + p.Text = fmt.Sprintf( + "BoardReady: %s\nOtherError: %s\nGroupChanged: %s\nUnexpectedFormation: %s\nCpuNotReady: %s\nPuNotReady: %s\nDebug: %s", + e.BoardReady, + e.OtherError, + e.ContactorGroupChanged, + e.UnexpectedFormation, + e.CpuNotReady, + e.PuNotReady, + e.Debug, + ) + + if e.BoardReady == yabl.BOARD_READY_OK { + p.TextStyle.Bg = ui.ColorClear + } else { + p.TextStyle.Bg = ui.ColorRed + } +} diff --git a/yabl/protocol.go b/yabl/protocol.go new file mode 100644 index 0000000..6b8b908 --- /dev/null +++ b/yabl/protocol.go @@ -0,0 +1,280 @@ +package yabl + +import ( + can "cli-mon/can" + "encoding/binary" +) + +const ( + CONTACTOR_MAX = 18 + 1 + CONNECTOR_MAX = 6 + 1 + ALL_BITS = 0xFFFFFFFFFFFFFFFF + UNIT_ID_OFFSET = 12 + UNIT_ID_MASK = 0b111111111111 << UNIT_ID_OFFSET + ACTION_ID_MASK = 0b111111111111 + BOARD_READY_OK BoardReadyType = 0 + BOARD_READY_INFO BoardReadyType = 1 + BOARD_READY_WARNING BoardReadyType = 2 + BOARD_READY_DEBUG BoardReadyType = 3 + BOARD_READY_DEP_ERROR BoardReadyType = 4 + BOARD_READY_ERROR BoardReadyType = 5 + BOARD_READY_CRITICAL BoardReadyType = 6 + OFF BooleanType = 0 + ON BooleanType = 1 + NO_ERROR ErrorType = 0 + ERROR ErrorType = 1 + OTHER_NO_ERROR ContactorInternalOtherErrorType = 0 + NO_PENDING_CHANGES ContactorGroupChangedType = 0 + INITIALIZED_CHANGE ContactorGroupChangedType = 1 + CHANGE_IN_PROGRESS ContactorGroupChangedType = 2 + V2G_MODE_G2V V2GModeType = 0 + V2G_MODE_V2G V2GModeType = 1 + V2G_MODE_INV V2GModeType = 2 + NOT_ENABLE CpLineLevelType = 0 + TWELVE CpLineLevelType = 1 + NINE CpLineLevelType = 2 + SIX CpLineLevelType = 3 + THREE CpLineLevelType = 4 + MINUS_TWELVE CpLineLevelType = 5 + UNKNOWN IsolationStateType = 0 + ONGOING IsolationStateType = 1 + PASSED IsolationStateType = 2 + WARNING IsolationStateType = 3 + FAILED IsolationStateType = 4 +) + +type Packet interface { + GetUnitId() uint +} + +type ContactorInternalState struct { + UnitId uint + ContactorReady BoardReadyType + ContactorOn BooleanType + UnexpectedState ErrorType + Isolated ErrorType + DebugEnabled BooleanType +} + +type ContactorInternalErrors struct { + UnitId uint + BoardReady BoardReadyType + OtherError ContactorInternalOtherErrorType + ContactorGroupChanged ContactorGroupChangedType + UnexpectedFormation ContactorInternalOtherErrorType + CpuNotReady ErrorType + PuNotReady ErrorType + Debug BooleanType +} + +type PuPresentEnergy struct { + UnitId uint + V2GMode V2GModeType + VoltageBefore Voltage11BitType + VoltageAfter Voltage11BitType + PresentCurrent Current10BitType +} + +type PuPeriphery struct { + UnitId uint + ConnectorInsert BooleanType + ContactorOn BooleanType + ConnectorLocked BooleanType + CpLineLevel CpLineLevelType + IsolationState IsolationStateType + ChargingAllowed BooleanType + PwmEnabled BooleanType + CpLineVoltage Voltage9BitType +} + +type BoardReadyType uint +type BooleanType uint +type ErrorType uint +type ContactorInternalOtherErrorType uint +type ContactorGroupChangedType uint +type V2GModeType uint +type Voltage11BitType uint +type Current10BitType uint +type CpLineLevelType uint +type IsolationStateType uint +type Voltage9BitType float32 + +func StartProtocolParsing(frames <-chan *can.CanFrame) <-chan Packet { + result := make(chan Packet) + + go func() { + defer close(result) + var contactorsFirstState [CONTACTOR_MAX]bool + var contactorsFirstErr [CONTACTOR_MAX]bool + var puFirst [CONNECTOR_MAX]bool + var puPeripheryFirst [CONNECTOR_MAX]bool + + for frame := range frames { + if frames == nil { + continue + } + + var unitId uint = uint((frame.CanId & UNIT_ID_MASK) >> UNIT_ID_OFFSET) + + switch { + case frame.CanId&ACTION_ID_MASK == can.CAN_ID_071: + changed := false + var model = ContactorInternalState{UnitId: unitId} + if val, ok := get(0, 3, frame.Payload); ok && model.ContactorReady != BoardReadyType(val) { + model.ContactorReady = BoardReadyType(val) + changed = true + } + if val, ok := get(3, 2, frame.Payload); ok && model.ContactorOn != BooleanType(val) { + model.ContactorOn = BooleanType(val) + changed = true + } + if val, ok := get(5, 2, frame.Payload); ok && model.UnexpectedState != ErrorType(val) { + model.UnexpectedState = ErrorType(val) + changed = true + } + if val, ok := get(7, 2, frame.Payload); ok && model.Isolated != ErrorType(val) { + model.Isolated = ErrorType(val) + changed = true + } + if val, ok := get(9, 2, frame.Payload); ok && model.DebugEnabled != BooleanType(val) { + model.DebugEnabled = BooleanType(val) + changed = true + } + if changed || !contactorsFirstState[unitId] { + contactorsFirstState[unitId] = true + result <- &model + } + case frame.CanId&ACTION_ID_MASK == can.CAN_ID_073: + changed := false + model := ContactorInternalErrors{UnitId: unitId} + if val, ok := get(0, 3, frame.Payload); ok && model.BoardReady != BoardReadyType(val) { + model.BoardReady = BoardReadyType(val) + changed = true + } + if val, ok := get(3, 4, frame.Payload); ok && model.OtherError != ContactorInternalOtherErrorType(val) { + model.OtherError = ContactorInternalOtherErrorType(val) + changed = true + } + if val, ok := get(7, 3, frame.Payload); ok && model.ContactorGroupChanged != ContactorGroupChangedType(val) { + model.ContactorGroupChanged = ContactorGroupChangedType(val) + changed = true + } + if val, ok := get(10, 4, frame.Payload); ok && model.UnexpectedFormation != ContactorInternalOtherErrorType(val) { + model.UnexpectedFormation = ContactorInternalOtherErrorType(val) + changed = true + } + if val, ok := get(14, 2, frame.Payload); ok && model.CpuNotReady != ErrorType(val) { + model.CpuNotReady = ErrorType(val) + changed = true + } + if val, ok := get(16, 2, frame.Payload); ok && model.PuNotReady != ErrorType(val) { + model.PuNotReady = ErrorType(val) + changed = true + } + if val, ok := get(18, 2, frame.Payload); ok && model.Debug != BooleanType(val) { + model.Debug = BooleanType(val) + changed = true + } + if changed || !contactorsFirstErr[unitId] { + contactorsFirstErr[unitId] = true + result <- &model + } + case frame.CanId&ACTION_ID_MASK == can.CAN_ID_021: + changed := false + model := PuPresentEnergy{UnitId: unitId} + if val, ok := get(0, 2, frame.Payload); ok && model.V2GMode != V2GModeType(val) { + model.V2GMode = V2GModeType(val) + changed = true + } + if val, ok := get(2, 11, frame.Payload); ok && model.VoltageBefore != Voltage11BitType(val) { + model.VoltageBefore = Voltage11BitType(val) + changed = true + } + if val, ok := get(13, 11, frame.Payload); ok && model.VoltageAfter != Voltage11BitType(val) { + model.VoltageAfter = Voltage11BitType(val) + changed = true + } + if val, ok := get(24, 10, frame.Payload); ok && model.PresentCurrent != Current10BitType(val) { + model.PresentCurrent = Current10BitType(val) + changed = true + } + if changed || !puFirst[unitId] { + puFirst[unitId] = true + result <- &model + } + case frame.CanId&ACTION_ID_MASK == can.CAN_ID_022: + changed := false + model := PuPeriphery{UnitId: unitId} + if val, ok := get(0, 2, frame.Payload); ok && model.ConnectorInsert != BooleanType(val) { + model.ConnectorInsert = BooleanType(val) + changed = true + } + if val, ok := get(2, 2, frame.Payload); ok && model.ContactorOn != BooleanType(val) { + model.ContactorOn = BooleanType(val) + changed = true + } + if val, ok := get(4, 2, frame.Payload); ok && model.ConnectorLocked != BooleanType(val) { + model.ConnectorLocked = BooleanType(val) + changed = true + } + if val, ok := get(6, 3, frame.Payload); ok && model.CpLineLevel != CpLineLevelType(val) { + model.CpLineLevel = CpLineLevelType(val) + changed = true + } + if val, ok := get(9, 3, frame.Payload); ok && model.IsolationState != IsolationStateType(val) { + model.IsolationState = IsolationStateType(val) + changed = true + } + if val, ok := get(12, 2, frame.Payload); ok && model.ChargingAllowed != BooleanType(val) { + model.ChargingAllowed = BooleanType(val) + changed = true + } + if val, ok := get(14, 2, frame.Payload); ok && model.PwmEnabled != BooleanType(val) { + model.PwmEnabled = BooleanType(val) + changed = true + } + if val, ok := get(16, 9, frame.Payload); ok && model.CpLineVoltage != (Voltage9BitType(val)*0.1-15) { + model.CpLineVoltage = Voltage9BitType(val)*0.1 - 15 + changed = true + } + if changed || !puPeripheryFirst[unitId] { + puPeripheryFirst[unitId] = true + result <- &model + } + + } + } + }() + + return result +} + +func get(from uint, size uint, buffer []byte) (uint64, bool) { + value := binary.LittleEndian.Uint64(buffer) + + var mask uint64 = ^(ALL_BITS << size) + mask = mask << from + selected := mask & value + + if selected == mask { + return 0, false + } else { + return selected >> from, true + } +} + +func (t ContactorInternalState) GetUnitId() uint { + return t.UnitId +} + +func (t ContactorInternalErrors) GetUnitId() uint { + return t.UnitId +} + +func (t PuPresentEnergy) GetUnitId() uint { + return t.UnitId +} + +func (t PuPeriphery) GetUnitId() uint { + return t.UnitId +} diff --git a/yabl/strings.go b/yabl/strings.go new file mode 100644 index 0000000..a44789f --- /dev/null +++ b/yabl/strings.go @@ -0,0 +1,129 @@ +package yabl + +import "fmt" + +func (t BooleanType) String() string { + switch t { + case ON: + return "ON" + case OFF: + return "OFF" + default: + panic("BooleanType not defended") + } +} + +func (t ErrorType) String() string { + switch t { + case NO_ERROR: + return "OK" + case ERROR: + return "ERROR" + default: + panic("ErrorType not defended") + } +} + +func (t BoardReadyType) String() string { + switch t { + case BOARD_READY_OK: + return "OK" + case BOARD_READY_INFO: + return "INFO" + case BOARD_READY_WARNING: + return "WARNING" + case BOARD_READY_DEBUG: + return "DEBUG" + case BOARD_READY_DEP_ERROR: + return "DEP_ERROR" + case BOARD_READY_ERROR: + return "ERROR" + case BOARD_READY_CRITICAL: + return "CRITICAL" + default: + panic("BoardReadyType not defended") + } +} + +func (t ContactorInternalOtherErrorType) String() string { + switch t { + case OTHER_NO_ERROR: + return "OK" + default: + return fmt.Sprintf("ERROR_CODE_%d", t) + } +} + +func (t ContactorGroupChangedType) String() string { + switch t { + case NO_PENDING_CHANGES: + return "NO_PENDING" + case INITIALIZED_CHANGE: + return "INITIALIZED" + case CHANGE_IN_PROGRESS: + return "IN_PROGRESS" + default: + panic("ContactorGroupChangedType not defended") + } +} + +func (t V2GModeType) String() string { + switch t { + case V2G_MODE_G2V: + return "G2V" + case V2G_MODE_V2G: + return "V2G" + case V2G_MODE_INV: + return "INV" + default: + panic("V2GModeType not defended") + } +} + +func (t Voltage11BitType) String() string { + return fmt.Sprintf("%dV", t) +} + +func (t Current10BitType) String() string { + return fmt.Sprintf("%dA", t) +} + +func (t CpLineLevelType) String() string { + switch t { + case NOT_ENABLE: + return "NOT_ENABLE" + case TWELVE: + return "12V_NOT_CONNECTED" + case NINE: + return "9V_CONNECTED" + case SIX: + return "6V_CHARGING" + case THREE: + return "3V" + case MINUS_TWELVE: + return "-12V" + default: + panic("CpLineLevelType not defended") + } +} + +func (t IsolationStateType) String() string { + switch t { + case UNKNOWN: + return "UNKNOWN" + case ONGOING: + return "ONGOING" + case PASSED: + return "PASSED" + case WARNING: + return "WARNING" + case FAILED: + return "FAILED" + default: + panic("IsolationStateType not defended") + } +} + +func (t Voltage9BitType) String() string { + return fmt.Sprintf("%.1fV", t) +}