From 52f21d47670d36832857de91d65916345ce18ae6 Mon Sep 17 00:00:00 2001 From: Samuel Shi Date: Tue, 5 Nov 2024 22:15:39 -0500 Subject: [PATCH 01/22] Moved files to new branch --- scripts/can_errgo/Demos/listenerDemo.go | 92 ++++ scripts/can_errgo/Demos/threadDemo.go | 29 + scripts/can_errgo/README.md | 24 + scripts/can_errgo/error_tui.go | 512 ++++++++++++++++++ scripts/can_errgo/go.mod | 24 + scripts/can_errgo/go.sum | 41 ++ scripts/can_errgo/setup.txt | 5 + scripts/can_errgo/setup_vcan.sh | 8 + scripts/can_errgo/utilityFiles/test_cli.sh | 12 + .../utilityFiles/utility_commands.txt | 10 + 10 files changed, 757 insertions(+) create mode 100644 scripts/can_errgo/Demos/listenerDemo.go create mode 100644 scripts/can_errgo/Demos/threadDemo.go create mode 100644 scripts/can_errgo/README.md create mode 100644 scripts/can_errgo/error_tui.go create mode 100644 scripts/can_errgo/go.mod create mode 100644 scripts/can_errgo/go.sum create mode 100644 scripts/can_errgo/setup.txt create mode 100644 scripts/can_errgo/setup_vcan.sh create mode 100644 scripts/can_errgo/utilityFiles/test_cli.sh create mode 100644 scripts/can_errgo/utilityFiles/utility_commands.txt diff --git a/scripts/can_errgo/Demos/listenerDemo.go b/scripts/can_errgo/Demos/listenerDemo.go new file mode 100644 index 000000000..786450a86 --- /dev/null +++ b/scripts/can_errgo/Demos/listenerDemo.go @@ -0,0 +1,92 @@ +package main + +import ( + "encoding/binary" + "fmt" + "log" + "net" + "golang.org/x/sys/unix" +) + +const ( + canInterface = "vcan0" // Use the virtual CAN interface + canFrameSize = 16 // CAN frame size (for a standard CAN frame) +) + +func IntPow(n, m int) int { + if m == 0 { + return 1 + } + + if m == 1 { + return n + } + + result := n + for i := 2; i <= m; i++ { + result *= n + } + return result +} + + +func main() { + // Create a raw CAN socket using the unix package + var sock int; + var err error; + var mask uint64 = ^uint64(0) // All 64 bits set to 1 + + fmt.Printf("%d",mask) + + sock, err = unix.Socket(unix.AF_CAN, unix.SOCK_RAW, unix.CAN_RAW) + if err != nil { + log.Fatalf("Error creating CAN socket: %v", err) + } + defer unix.Close(sock) + + // Get the index of the interface (vcan0) + ifi, err := net.InterfaceByName(canInterface) + if err != nil { + log.Fatalf("Error getting interface index: %v", err) + } + + // Bind the socket to the CAN interface + addr := &unix.SockaddrCAN{Ifindex: ifi.Index} + if err := unix.Bind(sock, addr); err != nil { + log.Fatalf("Error binding CAN socket: %v", err) + } + + // Create a buffer to hold incoming CAN frames + buf := make([]byte, canFrameSize) + + fmt.Println("Listening for CAN messages on", canInterface) + + for { + // Receive CAN message + _, err := unix.Read(sock, buf) + if err != nil { + log.Printf("Error receiving CAN message: %v", err) + continue + } + + // Extract CAN ID and data + canID := binary.LittleEndian.Uint32(buf[0:4]) & 0x1FFFFFFF // 29-bit CAN ID (masked) + data := buf[8:] //& mask // CAN data bytes (8 bytes) + fmt.Printf("CAN ID: 0x%X, Data: %08b\n", canID, data) + for i :=len(data) - 1; i >= 0; i--{ + bit := 1 + //fmt.Printf("CAN ID: 0x%X, Data: %08b\n",canID, data[i]) + for k :=0; k < 8; k++{ + //fmt.Printf("%d\n", IntPow(2,k)) + if (data[i] & byte(bit) != 0){ + fmt.Printf("error%d occured\n", (7-i)*8 + k) + + } + bit*=2 + } + + // Print the CAN message ID and data + //fmt.Printf("CAN ID: 0x%X, Data: %08b\n", canID, data) + }} +} + diff --git a/scripts/can_errgo/Demos/threadDemo.go b/scripts/can_errgo/Demos/threadDemo.go new file mode 100644 index 000000000..fb92db586 --- /dev/null +++ b/scripts/can_errgo/Demos/threadDemo.go @@ -0,0 +1,29 @@ +package main + +import ( + "fmt" + "time" +) + + + +func printHello(seconds float32){ + for { + time.Sleep(time.Duration(seconds * 1000) * time.Millisecond) + fmt.Printf("Hello") + } +} + +func printWorld(seconds float32){ + for { + time.Sleep(time.Duration(seconds * 1000) * time.Millisecond) + fmt.Printf("World\n") + } +} + +func main(){ + go printHello(4) + go printWorld(1) + for{ + } +} diff --git a/scripts/can_errgo/README.md b/scripts/can_errgo/README.md new file mode 100644 index 000000000..64eca3a3b --- /dev/null +++ b/scripts/can_errgo/README.md @@ -0,0 +1,24 @@ +To test the virtual can and go script: + +kill all existing instances of can-dump, can-listen, and end ip-links: + +ps aux | grep candump +ps aux | grep can-listen +ip link show + + +kill +kill +sudo ip link delete + + + +rerun (for now make CAN_PORT vcan0): + +./setup_vcan.sh +candump -L > & +./can_errgo/can-listen & + +You can now run: + +cansend # diff --git a/scripts/can_errgo/error_tui.go b/scripts/can_errgo/error_tui.go new file mode 100644 index 000000000..8707b44cc --- /dev/null +++ b/scripts/can_errgo/error_tui.go @@ -0,0 +1,512 @@ +package main + +import ( + "encoding/binary" + "fmt" + "log" + "math" + "net" + "sort" + "strconv" + "time" + + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/table" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "golang.org/x/sys/unix" +) + + +const ( + canInterface = "vcan0" // Use the virtual CAN interface + canFrameSize = 16 // CAN frame size (for a standard CAN frame) +) + +func deepCopy(rows []table.Row) []table.Row { + newRows := make([]table.Row, len(rows)) + for i := range rows { + newRows[i] = make(table.Row, len(rows[i])) + copy(newRows[i], rows[i]) + } + return newRows +} + +func bytesToUint64(b []byte) uint64 { + var result uint64 + for i := 0; i < len(b) && i < 8; i++ { + result |= uint64(b[i]) << (8 * i) + } + return result +} + +var baseStyle = lipgloss.NewStyle(). + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("240")) + + +type CANMsg struct { + ID uint32 + Value uint64 +} + +type TickMsg struct{} + +type model struct { + table table.Model + tableKeys KeyMap + + submenuTable table.Model + submenuActive bool + submenuKeys KeyMap + + hiddenCounts map[string]int + errorToBit map[string]int + ignoreMask uint64 + + isTimeout bool + lastCANTime time.Time + t time.Duration +} + + +// toggleBit toggles the specified bit in the number +//If the bit is 0, it sets it to 1; if it is 1, it sets it to 0. +func toggleBit(n uint64, bitPosition int) uint64 { + mask := uint64(1) << bitPosition + return n ^ mask +} + + +func deleteElementRow(slice []table.Row, index int) []table.Row { + // Create a new slice with the same length minus 1 + newSlice := make([]table.Row, 0, len(slice)-1) + + // Append the elements before the index + newSlice = append(newSlice, slice[:index]...) + + // Append the elements after the index + newSlice = append(newSlice, slice[index+1:]...) + + return newSlice +} + +func tickEvery(t time.Duration) tea.Cmd { + return func() tea.Msg { + time.Sleep(t) + return TickMsg{} + } +} + +// Might have future use +func findRow(s []table.Row, e table.Row) int { + for i, a := range s { + if a[0] == e[0] { + return i + } + } + return -1 +} + +func findRowString(s []table.Row, e string) int { + for i, a := range s { + if a[0] == e { + return i + } + } + return -1 +} + +func overBounds(table table.Model) bool{ + return len(table.Rows())==0 || table.Cursor() < 0 || table.Cursor() >= len(table.Rows()) +} + + +func (m *model) re_fresh(){ + rows := m.table.Rows() + for i:=0; i < len(rows); i++{ + if val,err := strconv.Atoi(rows[i][2]); err != nil{ + fmt.Printf("Something went wrong during refresh!") + } else{ + rows[i][2] = strconv.Itoa(val + 1) + } + } +} + +func (m *model) SortByColumn(columnIndex int) error { + rows := m.table.Rows() + + // Perform sorting with a custom less function + sort.Slice(rows, func(i, j int) bool { + // Make sure columnIndex is within bounds for both rows + if columnIndex >= len(rows[i]) || columnIndex >= len(rows[j]) { + fmt.Println("Error: columnIndex out of bounds") + return false + } + + // Attempt to convert the values to integers for comparison + val1, err1 := strconv.Atoi(rows[i][columnIndex]) + val2, err2 := strconv.Atoi(rows[j][columnIndex]) + + // If both are valid integers, sort numerically + if err1 == nil && err2 == nil { + return val1 < val2 + } + + // If conversion fails, sort lexicographically + return rows[i][columnIndex] < rows[j][columnIndex] + }) + + return nil +} + +func (m model) Init() tea.Cmd { return tickEvery(m.t) } + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + switch msg := msg.(type) { + case tea.KeyMsg: + if m.submenuActive{ + // If submenu is active, only let the submenu handle KeyMsg + switch msg.String() { + case "i": + if overBounds(m.submenuTable){ + return m, nil + } + + m.ignoreMask = toggleBit(m.ignoreMask, m.errorToBit[m.submenuTable.SelectedRow()[0]]) + newRows := deleteElementRow(m.submenuTable.Rows(), m.submenuTable.Cursor()) + m.submenuTable.SetRows(newRows) + + case "s": + m.submenuActive = false // Hide submenu + m.table.Focus() + m.submenuTable.Blur() + case "q", "ctrl+c": + return m, tea.Quit + } + + m.submenuTable, cmd = m.submenuTable.Update(msg) + return m, cmd + } + switch msg.String() { + case "esc": + if m.table.Focused() { + m.table.Blur() + } else { + m.table.Focus() + } + case "q", "ctrl+c": + return m, tea.Quit + case "a": + + if overBounds(m.table){ + return m, nil + } + + m.hiddenCounts[m.table.SelectedRow()[0]] = 0 + newRows := deleteElementRow(m.table.Rows(), m.table.Cursor()) + m.table.SetRows(newRows) + m.table.UpdateViewport() + return m, nil + case "i": + + if overBounds(m.table){ + return m, nil + } + + m.ignoreMask = toggleBit(m.ignoreMask, m.errorToBit[m.table.SelectedRow()[0]] ) + newSubRow := append(m.submenuTable.Rows(), table.Row{m.table.SelectedRow()[0]}) + m.submenuTable.SetRows(newSubRow) + m.submenuTable.UpdateViewport() + + m.hiddenCounts[m.table.SelectedRow()[0]] = 0 + newRows := deleteElementRow(m.table.Rows(), m.table.Cursor()) + m.table.SetRows(newRows) + m.table.UpdateViewport() + + return m, nil + case "s": + if m.submenuActive { + // Hide submenu and return to main menu + m.submenuActive = false + m.table.Focus() + } else { + // Show submenu and focus it + m.submenuActive = true + m.table.Blur() + m.submenuTable.Focus() + } + } + case CANMsg: + m.lastCANTime = time.Now() + m.isTimeout = false + msg.Value = msg.Value & m.ignoreMask + m.re_fresh() + for k := 0; k < 64; k++ { // Iterate through all 32 bits + if msg.Value&(1< m.t { + m.isTimeout = true + } + return m, tickEvery(m.t) + } + + if _, ok := msg.(CANMsg); ok { + // Update the mainTable regardless of submenu state + m.table, cmd = m.table.Update(msg) + }else if m.submenuActive { + m.submenuTable, cmd = m.submenuTable.Update(msg) + } else { + m.table, cmd = m.table.Update(msg) + } + + return m, cmd +} + +type KeyMap struct { + a key.Binding + i key.Binding + s key.Binding + q key.Binding + +} + +// ShortHelp implements the KeyMap interface. +func (km KeyMap) ShortHelp() []key.Binding { + return []key.Binding{km.a, km.i, km.s, km.q} +} + + +// DefaultKeyMap returns a default set of keybindings. +func additionalKeyMap() KeyMap { + return KeyMap{ + a: key.NewBinding( + key.WithKeys("a"), + key.WithHelp("a", "Acknowledge"), + ), + i: key.NewBinding( + key.WithKeys("i"), + key.WithHelp("i", "Ignore"), + ), + s: key.NewBinding( + key.WithKeys("s"), + key.WithHelp("s", "Ignored Menu"), + ), + q: key.NewBinding( + key.WithKeys("q"), + key.WithHelp("q/ctrl+c", "quit"), + ), + } +} + +func subMenuKeyMap() KeyMap { + return KeyMap{ + i: key.NewBinding( + key.WithKeys("i"), + key.WithHelp("i", "Unignore"), + ), + s: key.NewBinding( + key.WithKeys("s"), + key.WithHelp("s", "Close Ignored Menu"), + ), + q: key.NewBinding( + key.WithKeys("q"), + key.WithHelp("q/ctrl+c", "quit"), + ), + } +} + +func (m model) View() string { + out := baseStyle.Render(m.table.View()) + "\n" + if m.isTimeout{ + out = fmt.Sprint("\n\x1b[41m\x1b[37mWarning! Last recorded message was over ",math.Round(time.Since(m.lastCANTime).Seconds()), " seconds ago!\x1b[0m") + "\n" + out + } + + if m.submenuActive { + // Display both the main menu and submenu + return out + "\n" + + baseStyle.Render(m.submenuTable.View()) + "\n" + + m.table.HelpView() + "\n" + + m.submenuTable.Help.ShortHelpView(m.submenuKeys.ShortHelp()) + "\n" + } + // Only show the main table when submenu is not active + return out + "\n" + + m.table.HelpView() + "\n" + + m.table.Help.ShortHelpView(m.tableKeys.ShortHelp()) + "\n" +} + +func createTable() model{ + + columns := []table.Column{ + {Title: "Error:", Width: 30}, + {Title: "Count:", Width: 10}, + {Title: "Freshness", Width: 10}, + + } + rows := []table.Row {} + + + t := table.New( + table.WithColumns(columns), + table.WithRows(rows), + table.WithFocused(true), + table.WithHeight(7), + ) + + s := table.DefaultStyles() + s.Header = s.Header. + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("240")). + BorderBottom(true). + Bold(false) + s.Selected = s.Selected. + Foreground(lipgloss.Color("229")). + Background(lipgloss.Color("57")). + Bold(false) + t.SetStyles(s) + + table2 := createSubMenu() + + m := model{ + t, + additionalKeyMap(), + + table2, + false, + subMenuKeyMap(), + + map[string]int{}, + map[string]int{}, + ^uint64(0), + + false, + time.Now(), + 5 * time.Second, + + } + return m +} + +// Initialization of the submenu +func createSubMenu() table.Model { + columns := []table.Column{ + {Title: "Ignored Errors:", Width: 30}, + } + + rows := []table.Row{} + t := table.New( + table.WithColumns(columns), + table.WithRows(rows), + table.WithFocused(true), + table.WithHeight(5), + ) + + s := table.DefaultStyles() + s.Header = s.Header. + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("240")). + BorderBottom(true). + Bold(false) + s.Selected = s.Selected. + Foreground(lipgloss.Color("112")). + Background(lipgloss.Color("240")). + Bold(false) + t.SetStyles(s) + + return t +} + + +func can_listener(p *tea.Program) { + // Create a raw CAN socket using the unix package + var sock int; + var err error; + + sock, err = unix.Socket(unix.AF_CAN, unix.SOCK_RAW, unix.CAN_RAW) + if err != nil { + log.Fatalf("Error creating CAN socket: %v", err) + } + defer unix.Close(sock) + + // Get the index of the interface (vcan0) + ifi, err := net.InterfaceByName(canInterface) + if err != nil { + log.Fatalf("Error getting interface index: %v", err) + } + + // Bind the socket to the CAN interface + addr := &unix.SockaddrCAN{Ifindex: ifi.Index} + if err := unix.Bind(sock, addr); err != nil { + log.Fatalf("Error binding CAN socket: %v", err) + } + + // Create a buffer to hold incoming CAN frames + buf := make([]byte, canFrameSize) + + fmt.Println("Listening for CAN messages on", canInterface) + + for { + // Receive CAN message + _, err := unix.Read(sock, buf) + if err != nil { + log.Printf("Error receiving CAN message: %v", err) + continue + } + + // Extract CAN ID and data + canID := binary.LittleEndian.Uint32(buf[0:4]) & 0x1FFFFFFF // 29-bit CAN ID (masked) + data := bytesToUint64(buf[8:]) + + + msg := CANMsg{ ID : canID, Value: data} + p.Send(msg) + } +} + + +func main() { + var m model = createTable() + quit := make(chan struct{}) + + p := tea.NewProgram(m) + go func() { + if _,err := p.Run(); err != nil { + fmt.Println("Error running TUI:", err) + } + close(quit) + }() + + go can_listener(p) + <-quit +} + diff --git a/scripts/can_errgo/go.mod b/scripts/can_errgo/go.mod new file mode 100644 index 000000000..e6b67e72f --- /dev/null +++ b/scripts/can_errgo/go.mod @@ -0,0 +1,24 @@ +module main + +go 1.23.2 + +require ( + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/bubbles v0.20.0 // indirect + github.com/charmbracelet/bubbletea v1.1.1 // indirect + github.com/charmbracelet/lipgloss v0.13.0 // indirect + github.com/charmbracelet/x/ansi v0.2.3 // indirect + github.com/charmbracelet/x/term v0.2.0 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.15.2 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.3.8 // indirect +) diff --git a/scripts/can_errgo/go.sum b/scripts/can_errgo/go.sum new file mode 100644 index 000000000..9bcc1b26c --- /dev/null +++ b/scripts/can_errgo/go.sum @@ -0,0 +1,41 @@ +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= +github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= +github.com/charmbracelet/bubbletea v1.1.1 h1:KJ2/DnmpfqFtDNVTvYZ6zpPFL9iRCRr0qqKOCvppbPY= +github.com/charmbracelet/bubbletea v1.1.1/go.mod h1:9Ogk0HrdbHolIKHdjfFpyXJmiCzGwy+FesYkZr7hYU4= +github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw= +github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY= +github.com/charmbracelet/x/ansi v0.2.3 h1:VfFN0NUpcjBRd4DnKfRaIRo53KRgey/nhOoEqosGDEY= +github.com/charmbracelet/x/ansi v0.2.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0= +github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= diff --git a/scripts/can_errgo/setup.txt b/scripts/can_errgo/setup.txt new file mode 100644 index 000000000..ea8ab4239 --- /dev/null +++ b/scripts/can_errgo/setup.txt @@ -0,0 +1,5 @@ +sudo apt-get update +sudo apt-get upgrade + +sudo apt install net-tools iproute2 can-utils linux-modules-extra-$(uname -r) + diff --git a/scripts/can_errgo/setup_vcan.sh b/scripts/can_errgo/setup_vcan.sh new file mode 100644 index 000000000..16a885e48 --- /dev/null +++ b/scripts/can_errgo/setup_vcan.sh @@ -0,0 +1,8 @@ +#!/bin/bash + + +CAN_PORT=$1 + +sudo modprobe vcan +sudo ip link add dev $CAN_PORT type vcan +sudo ip link set up $CAN_PORT diff --git a/scripts/can_errgo/utilityFiles/test_cli.sh b/scripts/can_errgo/utilityFiles/test_cli.sh new file mode 100644 index 000000000..ffd5535bf --- /dev/null +++ b/scripts/can_errgo/utilityFiles/test_cli.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# Infinite loop +while true +do + # Send CAN message + cansend vcan0 FFF#1101 + + # Wait for 1 seconds + sleep 1 +done + diff --git a/scripts/can_errgo/utilityFiles/utility_commands.txt b/scripts/can_errgo/utilityFiles/utility_commands.txt new file mode 100644 index 000000000..943b3cd78 --- /dev/null +++ b/scripts/can_errgo/utilityFiles/utility_commands.txt @@ -0,0 +1,10 @@ +ps aux | grep candump + +kill + + +candump -L > & +cansend + +ip link show +sudo ip link delete From a421af21251e89b799def2bc9e40285b1287263f Mon Sep 17 00:00:00 2001 From: Samuel Shi Date: Thu, 7 Nov 2024 15:19:06 -0500 Subject: [PATCH 02/22] error description added with temp description --- scripts/can_errgo/components/text-box.go | 58 +++++++++++ scripts/can_errgo/error_tui.go | 118 ++++++++++++++++++----- scripts/can_errgo/go.sum | 8 ++ 3 files changed, 161 insertions(+), 23 deletions(-) create mode 100644 scripts/can_errgo/components/text-box.go diff --git a/scripts/can_errgo/components/text-box.go b/scripts/can_errgo/components/text-box.go new file mode 100644 index 000000000..9e629bded --- /dev/null +++ b/scripts/can_errgo/components/text-box.go @@ -0,0 +1,58 @@ +// component/box.go +package component + +import ( + "strings" + + "github.com/charmbracelet/lipgloss" +) + +type Box struct { + Width int + Title string + Description string +} + +func (b *Box) SetWidth(width int){ + b.Width = width +} + +// SetText updates the Title and Description of the Box +func (b *Box) SetText(title, description string) { + b.Title = title + b.Description = description +} + +func (b Box) View() string { + borderStyle := lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).Padding(1, 2) + titleStyle := lipgloss.NewStyle().Bold(true).Underline(true).Align(lipgloss.Center).Width(b.Width) + descriptionStyle := lipgloss.NewStyle().Align(lipgloss.Left).Width(b.Width - 4) + + titleContent := titleStyle.Render(b.Title) + "\n" + descriptionContent := descriptionStyle.Render(b.wrapText(b.Description)) + + content := lipgloss.JoinVertical(lipgloss.Left, titleContent, descriptionContent) + + boxHeight := 3 + strings.Count(content, "\n \n \n") + return borderStyle.Width(b.Width).Height(boxHeight).Render(content) +} + +func (b Box) wrapText(text string) string { + var wrapped string + maxLineLength := b.Width - 4 + + for len(text) > 0 { + if len(text) < maxLineLength { + wrapped += text + break + } + + lineEnd := maxLineLength + if idx := strings.LastIndex(text[:maxLineLength], " "); idx != -1 { + lineEnd = idx + } + wrapped += text[:lineEnd] + "\n" + text = text[lineEnd+1:] + } + return wrapped +} diff --git a/scripts/can_errgo/error_tui.go b/scripts/can_errgo/error_tui.go index 8707b44cc..ca5853660 100644 --- a/scripts/can_errgo/error_tui.go +++ b/scripts/can_errgo/error_tui.go @@ -2,14 +2,18 @@ package main import ( "encoding/binary" + "flag" "fmt" "log" "math" "net" + "os" "sort" "strconv" "time" + component "main/components" + "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/table" tea "github.com/charmbracelet/bubbletea" @@ -67,6 +71,9 @@ type model struct { isTimeout bool lastCANTime time.Time t time.Duration + + box component.Box + showBox bool } @@ -117,7 +124,7 @@ func findRowString(s []table.Row, e string) int { return -1 } -func overBounds(table table.Model) bool{ +func overBounds(table table.Model) bool{ return len(table.Rows())==0 || table.Cursor() < 0 || table.Cursor() >= len(table.Rows()) } @@ -177,6 +184,19 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.ignoreMask = toggleBit(m.ignoreMask, m.errorToBit[m.submenuTable.SelectedRow()[0]]) newRows := deleteElementRow(m.submenuTable.Rows(), m.submenuTable.Cursor()) m.submenuTable.SetRows(newRows) + case "d": + if overBounds(m.submenuTable){ + m.showBox =false + return m, nil + } + if m.showBox && (m.table.SelectedRow()[0] == m.box.Title){ + m.showBox =false + return m, nil + }else{ + m.box.SetText(m.table.SelectedRow()[0], "temp description") + m.showBox = true + return m, nil + } case "s": m.submenuActive = false // Hide submenu @@ -189,6 +209,8 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.submenuTable, cmd = m.submenuTable.Update(msg) return m, cmd } + + switch msg.String() { case "esc": if m.table.Focused() { @@ -237,6 +259,20 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.table.Blur() m.submenuTable.Focus() } + case "d": + tea.Printf("test"); + if overBounds(m.table){ + m.showBox =false + return m, nil + } + if m.showBox && m.table.SelectedRow()[0] == m.box.Title{ + m.showBox =false + return m, nil + }else{ + m.box.SetText(m.table.SelectedRow()[0], "temp description temp description temp description temp description temp description temp description temp description temp description temp description") + m.showBox = true + return m, nil + } } case CANMsg: m.lastCANTime = time.Now() @@ -298,12 +334,13 @@ type KeyMap struct { i key.Binding s key.Binding q key.Binding + d key.Binding } // ShortHelp implements the KeyMap interface. func (km KeyMap) ShortHelp() []key.Binding { - return []key.Binding{km.a, km.i, km.s, km.q} + return []key.Binding{km.a, km.i, km.s,km.d, km.q} } @@ -322,6 +359,10 @@ func additionalKeyMap() KeyMap { key.WithKeys("s"), key.WithHelp("s", "Ignored Menu"), ), + d: key.NewBinding( + key.WithKeys("d"), + key.WithHelp("d", "Show description"), + ), q: key.NewBinding( key.WithKeys("q"), key.WithHelp("q/ctrl+c", "quit"), @@ -339,6 +380,10 @@ func subMenuKeyMap() KeyMap { key.WithKeys("s"), key.WithHelp("s", "Close Ignored Menu"), ), + d: key.NewBinding( + key.WithKeys("d"), + key.WithHelp("d", "Show description"), + ), q: key.NewBinding( key.WithKeys("q"), key.WithHelp("q/ctrl+c", "quit"), @@ -351,21 +396,24 @@ func (m model) View() string { if m.isTimeout{ out = fmt.Sprint("\n\x1b[41m\x1b[37mWarning! Last recorded message was over ",math.Round(time.Since(m.lastCANTime).Seconds()), " seconds ago!\x1b[0m") + "\n" + out } - + if m.showBox{ + out = out + m.box.View(); + } if m.submenuActive { // Display both the main menu and submenu - return out + "\n" + + out = out + "\n" + baseStyle.Render(m.submenuTable.View()) + "\n" + m.table.HelpView() + "\n" + m.submenuTable.Help.ShortHelpView(m.submenuKeys.ShortHelp()) + "\n" - } - // Only show the main table when submenu is not active - return out + "\n" + - m.table.HelpView() + "\n" + - m.table.Help.ShortHelpView(m.tableKeys.ShortHelp()) + "\n" + }else{ + out = out + "\n" + + m.table.HelpView() + "\n" + + m.table.Help.ShortHelpView(m.tableKeys.ShortHelp()) + "\n" + } + return out; } -func createTable() model{ +func createTable(warning int) model{ columns := []table.Column{ {Title: "Error:", Width: 30}, @@ -397,6 +445,9 @@ func createTable() model{ table2 := createSubMenu() + box := component.Box{}; + box.SetWidth(60) + m := model{ t, additionalKeyMap(), @@ -411,8 +462,10 @@ func createTable() model{ false, time.Now(), - 5 * time.Second, + time.Duration(warning) * time.Second, + box, + false, } return m } @@ -495,18 +548,37 @@ func can_listener(p *tea.Program) { func main() { - var m model = createTable() - quit := make(chan struct{}) + // Define the command line flag + warnFlag := flag.String("w", "", "warning time for the table (integer value)") + + flag.Parse() + + var warning int + if *warnFlag == "" { + warning = 999 // Default value + } else { + // Parse the value for the -w flag + var err error + warning, err = strconv.Atoi(*warnFlag) + if err != nil { + // If it's not a valid integer, show the usage message and exit + fmt.Println("Usage: [program name] -w [int]") + os.Exit(1) + } + } - p := tea.NewProgram(m) - go func() { - if _,err := p.Run(); err != nil { - fmt.Println("Error running TUI:", err) - } - close(quit) - }() + var m model = createTable(warning) + quit := make(chan struct{}) + + p := tea.NewProgram(m) + go func() { + if _, err := p.Run(); err != nil { + fmt.Println("Error running TUI:", err) + } + close(quit) + }() + go can_listener(p) - go can_listener(p) - <-quit -} + <-quit +} diff --git a/scripts/can_errgo/go.sum b/scripts/can_errgo/go.sum index 9bcc1b26c..f7b3b0811 100644 --- a/scripts/can_errgo/go.sum +++ b/scripts/can_errgo/go.sum @@ -4,10 +4,16 @@ github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQW github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= github.com/charmbracelet/bubbletea v1.1.1 h1:KJ2/DnmpfqFtDNVTvYZ6zpPFL9iRCRr0qqKOCvppbPY= github.com/charmbracelet/bubbletea v1.1.1/go.mod h1:9Ogk0HrdbHolIKHdjfFpyXJmiCzGwy+FesYkZr7hYU4= +github.com/charmbracelet/bubbletea v1.2.0 h1:WYHclJaFDOz4dPxiGx7owwb8P4000lYPcuXPIALS5Z8= +github.com/charmbracelet/bubbletea v1.2.0/go.mod h1:viLoDL7hG4njLJSKU2gw7kB3LSEmWsrM80rO1dBJWBI= github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw= github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY= +github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg= +github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo= github.com/charmbracelet/x/ansi v0.2.3 h1:VfFN0NUpcjBRd4DnKfRaIRo53KRgey/nhOoEqosGDEY= github.com/charmbracelet/x/ansi v0.2.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/ansi v0.4.5 h1:LqK4vwBNaXw2AyGIICa5/29Sbdq58GbGdFngSexTdRM= +github.com/charmbracelet/x/ansi v0.4.5/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0= github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= @@ -39,3 +45,5 @@ golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= From 267814ea2da4a7dcb0a81d209cccf0620d1c85f8 Mon Sep 17 00:00:00 2001 From: Samuel Shi Date: Thu, 7 Nov 2024 18:17:51 -0500 Subject: [PATCH 03/22] Fixed bug with bits --- scripts/can_errgo/error_tui.go | 18 ++++++++++-------- scripts/can_errgo/utilityFiles/test_once.sh | 5 +++++ 2 files changed, 15 insertions(+), 8 deletions(-) create mode 100644 scripts/can_errgo/utilityFiles/test_once.sh diff --git a/scripts/can_errgo/error_tui.go b/scripts/can_errgo/error_tui.go index ca5853660..89602738d 100644 --- a/scripts/can_errgo/error_tui.go +++ b/scripts/can_errgo/error_tui.go @@ -37,11 +37,12 @@ func deepCopy(rows []table.Row) []table.Row { } func bytesToUint64(b []byte) uint64 { - var result uint64 - for i := 0; i < len(b) && i < 8; i++ { - result |= uint64(b[i]) << (8 * i) - } - return result + var result uint64 + for i := 0; i < len(b) && i < 8; i++ { + // Reverse the order: least significant byte (rightmost) gets the lowest bits + result |= uint64(b[len(b)-1-i]) << (8 * i) + } + return result } var baseStyle = lipgloss.NewStyle(). @@ -279,10 +280,10 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.isTimeout = false msg.Value = msg.Value & m.ignoreMask m.re_fresh() - for k := 0; k < 64; k++ { // Iterate through all 32 bits + for k := 0; k < 64; k++ { // Iterate through all 64 bits if msg.Value&(1< Date: Thu, 7 Nov 2024 19:42:45 -0500 Subject: [PATCH 04/22] minor error with submenu when opening description --- scripts/can_errgo/error_tui.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/scripts/can_errgo/error_tui.go b/scripts/can_errgo/error_tui.go index 89602738d..d7ed88d26 100644 --- a/scripts/can_errgo/error_tui.go +++ b/scripts/can_errgo/error_tui.go @@ -190,11 +190,11 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.showBox =false return m, nil } - if m.showBox && (m.table.SelectedRow()[0] == m.box.Title){ + if m.showBox && (m.submenuTable.SelectedRow()[0] == m.box.Title){ m.showBox =false return m, nil }else{ - m.box.SetText(m.table.SelectedRow()[0], "temp description") + m.box.SetText(m.submenuTable.SelectedRow()[0], "temp description") m.showBox = true return m, nil } @@ -541,8 +541,6 @@ func can_listener(p *tea.Program) { canID := binary.LittleEndian.Uint32(buf[0:4]) & 0x1FFFFFFF // 29-bit CAN ID (masked) data := bytesToUint64(buf[8:]) - fmt.Printf("%d", buf[8:]) - fmt.Printf("%d", data) msg := CANMsg{ ID : canID, Value: data} p.Send(msg) } From df36d5d802d60f786bfdc9d4672fb3cf78bfb8a6 Mon Sep 17 00:00:00 2001 From: Samuel Shi Date: Fri, 8 Nov 2024 02:17:57 -0500 Subject: [PATCH 05/22] new description features added --- scripts/can_errgo/error_tui.go | 212 +++++++++++++++++++++++---------- 1 file changed, 152 insertions(+), 60 deletions(-) diff --git a/scripts/can_errgo/error_tui.go b/scripts/can_errgo/error_tui.go index d7ed88d26..46dcbeec7 100644 --- a/scripts/can_errgo/error_tui.go +++ b/scripts/can_errgo/error_tui.go @@ -144,30 +144,57 @@ func (m *model) re_fresh(){ func (m *model) SortByColumn(columnIndex int) error { rows := m.table.Rows() - // Perform sorting with a custom less function - sort.Slice(rows, func(i, j int) bool { - // Make sure columnIndex is within bounds for both rows - if columnIndex >= len(rows[i]) || columnIndex >= len(rows[j]) { - fmt.Println("Error: columnIndex out of bounds") - return false - } + // Check if sorting should adjust cursor position + if !overBounds(m.table) { + // Capture the current row data before sorting + currentIndex := m.table.Cursor() + selectedRow := rows[currentIndex] + + // Perform sorting with a custom less function + sort.SliceStable(rows, func(i, j int) bool { + if columnIndex >= len(rows[i]) || columnIndex >= len(rows[j]) { + fmt.Println("Error: columnIndex out of bounds") + return false + } - // Attempt to convert the values to integers for comparison - val1, err1 := strconv.Atoi(rows[i][columnIndex]) - val2, err2 := strconv.Atoi(rows[j][columnIndex]) + val1, err1 := strconv.Atoi(rows[i][columnIndex]) + val2, err2 := strconv.Atoi(rows[j][columnIndex]) - // If both are valid integers, sort numerically - if err1 == nil && err2 == nil { - return val1 < val2 + if err1 == nil && err2 == nil { + return val1 < val2 + } + return rows[i][columnIndex] < rows[j][columnIndex] + }) + + // Find new index of the originally selected row + for i, row := range rows { + if row[0] == selectedRow[0] { + m.table.SetCursor(i) + break + } } + } else { + // Just sort if out of bounds + sort.SliceStable(rows, func(i, j int) bool { + if columnIndex >= len(rows[i]) || columnIndex >= len(rows[j]) { + fmt.Println("Error: columnIndex out of bounds") + return false + } - // If conversion fails, sort lexicographically - return rows[i][columnIndex] < rows[j][columnIndex] - }) + val1, err1 := strconv.Atoi(rows[i][columnIndex]) + val2, err2 := strconv.Atoi(rows[j][columnIndex]) + + if err1 == nil && err2 == nil { + return val1 < val2 + } + return rows[i][columnIndex] < rows[j][columnIndex] + }) + } return nil } + func (m model) Init() tea.Cmd { return tickEvery(m.t) } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { @@ -177,32 +204,54 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if m.submenuActive{ // If submenu is active, only let the submenu handle KeyMsg switch msg.String() { + case "up": + if overBounds(m.submenuTable){ + m.table.SetCursor(0) + return m, nil + } + m.submenuTable.MoveUp(1) + // m.box.SetText(m.submenuTable.SelectedRow()[0], "temp description for subtable") + + return m,nil + case "down": + if overBounds(m.submenuTable){ + m.table.SetCursor(0) + return m, nil + } + m.submenuTable.MoveDown(1) + // m.box.SetText(m.submenuTable.SelectedRow()[0], "temp description for subtable") + return m,nil case "i": if overBounds(m.submenuTable){ + m.table.SetCursor(0) return m, nil } - + + if m.submenuTable.SelectedRow() == nil{ + return m, nil + } + m.ignoreMask = toggleBit(m.ignoreMask, m.errorToBit[m.submenuTable.SelectedRow()[0]]) newRows := deleteElementRow(m.submenuTable.Rows(), m.submenuTable.Cursor()) m.submenuTable.SetRows(newRows) - case "d": - if overBounds(m.submenuTable){ - m.showBox =false - return m, nil - } - if m.showBox && (m.submenuTable.SelectedRow()[0] == m.box.Title){ - m.showBox =false - return m, nil + if m.submenuTable.Cursor() == 0{ + m.submenuTable.MoveDown(1) }else{ - m.box.SetText(m.submenuTable.SelectedRow()[0], "temp description") - m.showBox = true - return m, nil + m.submenuTable.MoveUp(1) } case "s": m.submenuActive = false // Hide submenu m.table.Focus() - m.submenuTable.Blur() + m.submenuTable.Blur() + + if m.table.SelectedRow() == nil{ + m.table.SetCursor(0) + return m, nil + } + // m.box.SetText(m.table.SelectedRow()[0], "temp description for main table") + return m, nil + case "q", "ctrl+c": return m, tea.Quit } @@ -224,20 +273,36 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case "a": if overBounds(m.table){ + m.table.SetCursor(0) + return m, nil + } + + if m.table.SelectedRow() == nil{ + m.table.SetCursor(0) return m, nil } m.hiddenCounts[m.table.SelectedRow()[0]] = 0 newRows := deleteElementRow(m.table.Rows(), m.table.Cursor()) m.table.SetRows(newRows) - m.table.UpdateViewport() + if m.table.Cursor() == 0{ + m.table.MoveDown(1) + }else{ + m.table.MoveUp(1) + } return m, nil case "i": if overBounds(m.table){ + m.table.SetCursor(0) return m, nil } - + + if m.table.SelectedRow() == nil{ + m.table.SetCursor(0) + return m, nil + } + m.ignoreMask = toggleBit(m.ignoreMask, m.errorToBit[m.table.SelectedRow()[0]] ) newSubRow := append(m.submenuTable.Rows(), table.Row{m.table.SelectedRow()[0]}) m.submenuTable.SetRows(newSubRow) @@ -246,7 +311,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.hiddenCounts[m.table.SelectedRow()[0]] = 0 newRows := deleteElementRow(m.table.Rows(), m.table.Cursor()) m.table.SetRows(newRows) - m.table.UpdateViewport() + m.table.MoveDown(1) return m, nil case "s": @@ -260,20 +325,38 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.table.Blur() m.submenuTable.Focus() } - case "d": - tea.Printf("test"); + if len(m.submenuTable.Rows()) > 0{ + m.submenuTable.SetCursor(0) + // m.box.SetText(m.submenuTable.SelectedRow()[0], "temp description for subtable") + } + case "up": if overBounds(m.table){ - m.showBox =false + m.table.SetCursor(0) + return m, nil + } + + if m.table.SelectedRow() == nil{ + m.table.SetCursor(0) return m, nil } - if m.showBox && m.table.SelectedRow()[0] == m.box.Title{ - m.showBox =false + + m.table.MoveUp(1) + // m.box.SetText(m.table.SelectedRow()[0], "temp description for main table") + return m,nil + case "down": + if overBounds(m.table){ + m.table.SetCursor(0) return m, nil - }else{ - m.box.SetText(m.table.SelectedRow()[0], "temp description temp description temp description temp description temp description temp description temp description temp description temp description") - m.showBox = true + } + + if m.table.SelectedRow() == nil{ + m.table.SetCursor(0) return m, nil } + + m.table.MoveDown(1) + // m.box.SetText(m.table.SelectedRow()[0], "temp description for main table") + return m,nil } case CANMsg: m.lastCANTime = time.Now() @@ -335,13 +418,12 @@ type KeyMap struct { i key.Binding s key.Binding q key.Binding - d key.Binding } // ShortHelp implements the KeyMap interface. func (km KeyMap) ShortHelp() []key.Binding { - return []key.Binding{km.a, km.i, km.s,km.d, km.q} + return []key.Binding{km.a, km.i, km.s, km.q} } @@ -360,10 +442,6 @@ func additionalKeyMap() KeyMap { key.WithKeys("s"), key.WithHelp("s", "Ignored Menu"), ), - d: key.NewBinding( - key.WithKeys("d"), - key.WithHelp("d", "Show description"), - ), q: key.NewBinding( key.WithKeys("q"), key.WithHelp("q/ctrl+c", "quit"), @@ -381,10 +459,6 @@ func subMenuKeyMap() KeyMap { key.WithKeys("s"), key.WithHelp("s", "Close Ignored Menu"), ), - d: key.NewBinding( - key.WithKeys("d"), - key.WithHelp("d", "Show description"), - ), q: key.NewBinding( key.WithKeys("q"), key.WithHelp("q/ctrl+c", "quit"), @@ -397,27 +471,45 @@ func (m model) View() string { if m.isTimeout{ out = fmt.Sprint("\n\x1b[41m\x1b[37mWarning! Last recorded message was over ",math.Round(time.Since(m.lastCANTime).Seconds()), " seconds ago!\x1b[0m") + "\n" + out } - if m.showBox{ + if m.submenuActive{ + if m.submenuTable.SelectedRow() != nil{ + m.box.SetText(m.submenuTable.SelectedRow()[0], "temp description for sub table") + } out = out + m.box.View(); - } - if m.submenuActive { - // Display both the main menu and submenu out = out + "\n" + baseStyle.Render(m.submenuTable.View()) + "\n" + m.table.HelpView() + "\n" + m.submenuTable.Help.ShortHelpView(m.submenuKeys.ShortHelp()) + "\n" - }else{ - out = out + "\n" + - m.table.HelpView() + "\n" + - m.table.Help.ShortHelpView(m.tableKeys.ShortHelp()) + "\n" - } + }else{ + if m.table.SelectedRow() != nil { + m.box.SetText(m.table.SelectedRow()[0], "temp description for main table") + } + out = out + m.box.View() + out = out + "\n" + + m.table.HelpView() + "\n" + + m.table.Help.ShortHelpView(m.tableKeys.ShortHelp()) + "\n" + } + // if m.showBox{ + // out = out + m.box.View(); + // } + // if m.submenuActive { + // // Display both the main menu and submenu + // out = out + "\n" + + // baseStyle.Render(m.submenuTable.View()) + "\n" + + // m.table.HelpView() + "\n" + + // m.submenuTable.Help.ShortHelpView(m.submenuKeys.ShortHelp()) + "\n" + // }else{ + // out = out + "\n" + + // m.table.HelpView() + "\n" + + // m.table.Help.ShortHelpView(m.tableKeys.ShortHelp()) + "\n" + // } return out; } func createTable(warning int) model{ columns := []table.Column{ - {Title: "Error:", Width: 30}, + {Title: "Error:", Width: 40}, {Title: "Count:", Width: 10}, {Title: "Freshness", Width: 10}, From 9ac02de4b592a40f21dbeadc2060e10bda45c708 Mon Sep 17 00:00:00 2001 From: Samuel Shi Date: Mon, 11 Nov 2024 18:47:55 -0500 Subject: [PATCH 06/22] finished: ~10. Make cursor move to the ignored menu when the ignored menu is opened.~ ~11. Make border purple for the description.~ ~12. Move the help menu to the top.~ ~13. Make all the displays the same size.~ --- .../components/{ => box}/text-box.go | 8 +- scripts/can_errgo/components/table/table.go | 489 ++++++++++++++++++ scripts/can_errgo/error_tui.go | 158 +++--- 3 files changed, 571 insertions(+), 84 deletions(-) rename scripts/can_errgo/components/{ => box}/text-box.go (89%) create mode 100644 scripts/can_errgo/components/table/table.go diff --git a/scripts/can_errgo/components/text-box.go b/scripts/can_errgo/components/box/text-box.go similarity index 89% rename from scripts/can_errgo/components/text-box.go rename to scripts/can_errgo/components/box/text-box.go index 9e629bded..e6276d9f3 100644 --- a/scripts/can_errgo/components/text-box.go +++ b/scripts/can_errgo/components/box/text-box.go @@ -1,5 +1,5 @@ // component/box.go -package component +package box import ( "strings" @@ -24,9 +24,9 @@ func (b *Box) SetText(title, description string) { } func (b Box) View() string { - borderStyle := lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).Padding(1, 2) - titleStyle := lipgloss.NewStyle().Bold(true).Underline(true).Align(lipgloss.Center).Width(b.Width) - descriptionStyle := lipgloss.NewStyle().Align(lipgloss.Left).Width(b.Width - 4) + borderStyle := lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).Padding(1, 2).BorderForeground(lipgloss.Color("57")) + titleStyle := lipgloss.NewStyle().Bold(true).Underline(true).Align(lipgloss.Center).Width(b.Width).MaxWidth(b.Width) + descriptionStyle := lipgloss.NewStyle().Align(lipgloss.Left).Width(b.Width).MaxWidth(b.Width) titleContent := titleStyle.Render(b.Title) + "\n" descriptionContent := descriptionStyle.Render(b.wrapText(b.Description)) diff --git a/scripts/can_errgo/components/table/table.go b/scripts/can_errgo/components/table/table.go new file mode 100644 index 000000000..649eb241f --- /dev/null +++ b/scripts/can_errgo/components/table/table.go @@ -0,0 +1,489 @@ +// Modified from "github.com/charmbracelet/bubbles/table" +package table + +import ( + "strings" + + "github.com/charmbracelet/bubbles/help" + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/mattn/go-runewidth" +) + +// Model defines a state for the table widget. +type Model struct { + KeyMap KeyMap + Help help.Model + + cols []Column + rows []Row + cursor int + focus bool + styles Styles + ShowCursor bool + + viewport viewport.Model + start int + end int +} + +// Row represents one line in the table. +type Row []string + +// Column defines the table structure. +type Column struct { + Title string + Width int +} + +// KeyMap defines keybindings. It satisfies to the help.KeyMap interface, which +// is used to render the help menu. +type KeyMap struct { + LineUp key.Binding + LineDown key.Binding + PageUp key.Binding + PageDown key.Binding + HalfPageUp key.Binding + HalfPageDown key.Binding + GotoTop key.Binding + GotoBottom key.Binding +} + +// ShortHelp implements the KeyMap interface. +func (km KeyMap) ShortHelp() []key.Binding { + return []key.Binding{km.LineUp, km.LineDown} +} + +// FullHelp implements the KeyMap interface. +func (km KeyMap) FullHelp() [][]key.Binding { + return [][]key.Binding{ + {km.LineUp, km.LineDown, km.GotoTop, km.GotoBottom}, + {km.PageUp, km.PageDown, km.HalfPageUp, km.HalfPageDown}, + } +} + +// DefaultKeyMap returns a default set of keybindings. +func DefaultKeyMap() KeyMap { + const spacebar = " " + return KeyMap{ + LineUp: key.NewBinding( + key.WithKeys("up", "k"), + key.WithHelp("↑/k", "up"), + ), + LineDown: key.NewBinding( + key.WithKeys("down", "j"), + key.WithHelp("↓/j", "down"), + ), + PageUp: key.NewBinding( + key.WithKeys("b", "pgup"), + key.WithHelp("b/pgup", "page up"), + ), + PageDown: key.NewBinding( + key.WithKeys("f", "pgdown", spacebar), + key.WithHelp("f/pgdn", "page down"), + ), + HalfPageUp: key.NewBinding( + key.WithKeys("u", "ctrl+u"), + key.WithHelp("u", "½ page up"), + ), + HalfPageDown: key.NewBinding( + key.WithKeys("d", "ctrl+d"), + key.WithHelp("d", "½ page down"), + ), + GotoTop: key.NewBinding( + key.WithKeys("home", "g"), + key.WithHelp("g/home", "go to start"), + ), + GotoBottom: key.NewBinding( + key.WithKeys("end", "G"), + key.WithHelp("G/end", "go to end"), + ), + } +} + +// Styles contains style definitions for this list component. By default, these +// values are generated by DefaultStyles. +type Styles struct { + Header lipgloss.Style + Cell lipgloss.Style + Selected lipgloss.Style +} + +// DefaultStyles returns a set of default style definitions for this table. +func DefaultStyles() Styles { + return Styles{ + Selected: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("212")), + Header: lipgloss.NewStyle().Bold(true).Padding(0, 1), + Cell: lipgloss.NewStyle().Padding(0, 1), + } +} + +// SetStyles sets the table styles. +func (m *Model) SetStyles(s Styles) { + m.styles = s + m.UpdateViewport() +} + +// Option is used to set options in New. For example: +// +// table := New(WithColumns([]Column{{Title: "ID", Width: 10}})) +type Option func(*Model) + +// New creates a new model for the table widget. +func New(opts ...Option) Model { + m := Model{ + cursor: 0, + viewport: viewport.New(0, 20), + ShowCursor: true, + + KeyMap: DefaultKeyMap(), + Help: help.New(), + styles: DefaultStyles(), + } + + for _, opt := range opts { + opt(&m) + } + + m.UpdateViewport() + + return m +} + +// WithColumns sets the table columns (headers). +func WithColumns(cols []Column) Option { + return func(m *Model) { + m.cols = cols + } +} + +// WithRows sets the table rows (data). +func WithRows(rows []Row) Option { + return func(m *Model) { + m.rows = rows + } +} + +// WithHeight sets the height of the table. +func WithHeight(h int) Option { + return func(m *Model) { + m.viewport.Height = h - lipgloss.Height(m.headersView()) + } +} + +// WithWidth sets the width of the table. +func WithWidth(w int) Option { + return func(m *Model) { + m.viewport.Width = w + } +} + +// WithFocused sets the focus state of the table. +func WithFocused(f bool) Option { + return func(m *Model) { + m.focus = f + } +} + +// WithStyles sets the table styles. +func WithStyles(s Styles) Option { + return func(m *Model) { + m.styles = s + } +} + +// WithKeyMap sets the key map. +func WithKeyMap(km KeyMap) Option { + return func(m *Model) { + m.KeyMap = km + } +} + +// Update is the Bubble Tea update loop. +func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { + if !m.focus { + return m, nil + } + + switch msg := msg.(type) { + case tea.KeyMsg: + switch { + case key.Matches(msg, m.KeyMap.LineUp): + m.MoveUp(1) + case key.Matches(msg, m.KeyMap.LineDown): + m.MoveDown(1) + case key.Matches(msg, m.KeyMap.PageUp): + m.MoveUp(m.viewport.Height) + case key.Matches(msg, m.KeyMap.PageDown): + m.MoveDown(m.viewport.Height) + case key.Matches(msg, m.KeyMap.HalfPageUp): + m.MoveUp(m.viewport.Height / 2) + case key.Matches(msg, m.KeyMap.HalfPageDown): + m.MoveDown(m.viewport.Height / 2) + case key.Matches(msg, m.KeyMap.LineDown): + m.MoveDown(1) + case key.Matches(msg, m.KeyMap.GotoTop): + m.GotoTop() + case key.Matches(msg, m.KeyMap.GotoBottom): + m.GotoBottom() + } + } + + return m, nil +} + +// Focused returns the focus state of the table. +func (m Model) Focused() bool { + return m.focus +} + +// Focus focuses the table, allowing the user to move around the rows and +// interact. +func (m *Model) Focus() { + m.focus = true + m.UpdateViewport() +} + +// Blur blurs the table, preventing selection or movement. +func (m *Model) Blur() { + m.focus = false + m.UpdateViewport() +} + +// View renders the component. +func (m Model) View() string { + return m.headersView() + "\n" + m.viewport.View() +} + +// HelpView is a helper method for rendering the help menu from the keymap. +// Note that this view is not rendered by default and you must call it +// manually in your application, where applicable. +func (m Model) HelpView() string { + return m.Help.View(m.KeyMap) +} + +// UpdateViewport updates the list content based on the previously defined +// columns and rows. +func (m *Model) UpdateViewport() { + renderedRows := make([]string, 0, len(m.rows)) + + // Render only rows from: m.cursor-m.viewport.Height to: m.cursor+m.viewport.Height + // Constant runtime, independent of number of rows in a table. + // Limits the number of renderedRows to a maximum of 2*m.viewport.Height + if m.cursor >= 0 { + m.start = clamp(m.cursor-m.viewport.Height, 0, m.cursor) + } else { + m.start = 0 + } + m.end = clamp(m.cursor+m.viewport.Height, m.cursor, len(m.rows)) + for i := m.start; i < m.end; i++ { + renderedRows = append(renderedRows, m.renderRow(i)) + } + + m.viewport.SetContent( + lipgloss.JoinVertical(lipgloss.Left, renderedRows...), + ) +} + +// SelectedRow returns the selected row. +// You can cast it to your own implementation. +func (m Model) SelectedRow() Row { + if m.cursor < 0 || m.cursor >= len(m.rows) { + return nil + } + + return m.rows[m.cursor] +} + +// Rows returns the current rows. +func (m Model) Rows() []Row { + return m.rows +} + +// Columns returns the current columns. +func (m Model) Columns() []Column { + return m.cols +} + +// SetRows sets a new rows state. +func (m *Model) SetRows(r []Row) { + m.rows = r + m.UpdateViewport() +} + +// SetColumns sets a new columns state. +func (m *Model) SetColumns(c []Column) { + m.cols = c + m.UpdateViewport() +} + +// SetWidth sets the width of the viewport of the table. +func (m *Model) SetWidth(w int) { + m.viewport.Width = w + m.UpdateViewport() +} + +// SetHeight sets the height of the viewport of the table. +func (m *Model) SetHeight(h int) { + m.viewport.Height = h - lipgloss.Height(m.headersView()) + m.UpdateViewport() +} + +// Height returns the viewport height of the table. +func (m Model) Height() int { + return m.viewport.Height +} + +// Width returns the viewport width of the table. +func (m Model) Width() int { + return m.viewport.Width +} + +// Cursor returns the index of the selected row. +func (m Model) Cursor() int { + return m.cursor +} + +// ForceCursor forces the cursor to a specific row, regardless if the row is out of bounds. +func (m *Model) ForceCursor(n int) { + // Clamp the cursor position within bounds + m.cursor = clamp(n, 0, len(m.rows)-1) + m.UpdateViewport() +} + +// SetCursor sets the cursor position in the table. +func (m *Model) SetCursor(n int) { + m.cursor = clamp(n, 0, len(m.rows)-1) + m.UpdateViewport() +} +// SetCursor sets the cursor position in the table. +func (m *Model) SetCursorAndViewport(n int) { + m.cursor = clamp(n, 0, len(m.rows)-1) + + switch { + case m.start == 0: + m.viewport.SetYOffset(clamp(m.viewport.YOffset, 0, m.cursor)) + case m.end == len(m.rows) && m.cursor > m.viewport.Height: + m.viewport.SetYOffset(clamp(m.viewport.YOffset-n, 1, m.viewport.Height)) + } + + m.UpdateViewport() +} + +func (m *Model) SetShowCursor(show bool) { + m.ShowCursor = show + m.UpdateViewport() +} + +// MoveUp moves the selection up by any number of rows. +// It can not go above the first row. +func (m *Model) MoveUp(n int) { + m.cursor = clamp(m.cursor-n, 0, len(m.rows)-1) + switch { + case m.start == 0: + m.viewport.SetYOffset(clamp(m.viewport.YOffset, 0, m.cursor)) + case m.start < m.viewport.Height: + m.viewport.YOffset = (clamp(clamp(m.viewport.YOffset+n, 0, m.cursor), 0, m.viewport.Height)) + case m.viewport.YOffset >= 1: + m.viewport.YOffset = clamp(m.viewport.YOffset+n, 1, m.viewport.Height) + } + m.UpdateViewport() +} + +// MoveDown moves the selection down by any number of rows. +// It can not go below the last row. +func (m *Model) MoveDown(n int) { + m.cursor = clamp(m.cursor+n, 0, len(m.rows)-1) + m.UpdateViewport() + + switch { + case m.end == len(m.rows) && m.viewport.YOffset > 0: + m.viewport.SetYOffset(clamp(m.viewport.YOffset-n, 1, m.viewport.Height)) + case m.cursor > (m.end-m.start)/2 && m.viewport.YOffset > 0: + m.viewport.SetYOffset(clamp(m.viewport.YOffset-n, 1, m.cursor)) + case m.viewport.YOffset > 1: + case m.cursor > m.viewport.YOffset+m.viewport.Height-1: + m.viewport.SetYOffset(clamp(m.viewport.YOffset+1, 0, 1)) + } +} + +// GotoTop moves the selection to the first row. +func (m *Model) GotoTop() { + m.MoveUp(m.cursor) +} + +// GotoBottom moves the selection to the last row. +func (m *Model) GotoBottom() { + m.MoveDown(len(m.rows)) +} + +// FromValues create the table rows from a simple string. It uses `\n` by +// default for getting all the rows and the given separator for the fields on +// each row. +func (m *Model) FromValues(value, separator string) { + rows := []Row{} + for _, line := range strings.Split(value, "\n") { + r := Row{} + for _, field := range strings.Split(line, separator) { + r = append(r, field) + } + rows = append(rows, r) + } + + m.SetRows(rows) +} + +func (m Model) headersView() string { + s := make([]string, 0, len(m.cols)) + for _, col := range m.cols { + if col.Width <= 0 { + continue + } + style := lipgloss.NewStyle().Width(col.Width).MaxWidth(col.Width).Inline(true) + renderedCell := style.Render(runewidth.Truncate(col.Title, col.Width, "…")) + s = append(s, m.styles.Header.Render(renderedCell)) + } + return lipgloss.JoinHorizontal(lipgloss.Top, s...) +} + +func (m *Model) renderRow(r int) string { + s := make([]string, 0, len(m.cols)) + for i, value := range m.rows[r] { + if m.cols[i].Width <= 0 { + continue + } + style := lipgloss.NewStyle().Width(m.cols[i].Width).MaxWidth(m.cols[i].Width).Inline(true) + renderedCell := m.styles.Cell.Render(style.Render(runewidth.Truncate(value, m.cols[i].Width, "…"))) + s = append(s, renderedCell) + } + + row := lipgloss.JoinHorizontal(lipgloss.Top, s...) + + if r == m.cursor && m.ShowCursor { + return m.styles.Selected.Render(row) + } + + return row +} + +func max(a, b int) int { + if a > b { + return a + } + + return b +} + +func min(a, b int) int { + if a < b { + return a + } + + return b +} + +func clamp(v, low, high int) int { + return min(max(v, low), high) +} diff --git a/scripts/can_errgo/error_tui.go b/scripts/can_errgo/error_tui.go index 46dcbeec7..6309d312d 100644 --- a/scripts/can_errgo/error_tui.go +++ b/scripts/can_errgo/error_tui.go @@ -12,10 +12,10 @@ import ( "strconv" "time" - component "main/components" + box "main/components/box" + table "main/components/table" "github.com/charmbracelet/bubbles/key" - "github.com/charmbracelet/bubbles/table" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "golang.org/x/sys/unix" @@ -27,6 +27,7 @@ const ( canFrameSize = 16 // CAN frame size (for a standard CAN frame) ) +// Might have future use func deepCopy(rows []table.Row) []table.Row { newRows := make([]table.Row, len(rows)) for i := range rows { @@ -73,7 +74,7 @@ type model struct { lastCANTime time.Time t time.Duration - box component.Box + box box.Box showBox bool } @@ -143,8 +144,7 @@ func (m *model) re_fresh(){ func (m *model) SortByColumn(columnIndex int) error { rows := m.table.Rows() - - // Check if sorting should adjust cursor position + // Check if sorting should adjust cursor position (In the odd case that no cursor is set, we don't want to run into a panic) if !overBounds(m.table) { // Capture the current row data before sorting currentIndex := m.table.Cursor() @@ -169,10 +169,11 @@ func (m *model) SortByColumn(columnIndex int) error { // Find new index of the originally selected row for i, row := range rows { if row[0] == selectedRow[0] { - m.table.SetCursor(i) + m.table.SetCursorAndViewport(i) break } } + } else { // Just sort if out of bounds sort.SliceStable(rows, func(i, j int) bool { @@ -190,7 +191,6 @@ func (m *model) SortByColumn(columnIndex int) error { return rows[i][columnIndex] < rows[j][columnIndex] }) } - return nil } @@ -210,8 +210,6 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } m.submenuTable.MoveUp(1) - // m.box.SetText(m.submenuTable.SelectedRow()[0], "temp description for subtable") - return m,nil case "down": if overBounds(m.submenuTable){ @@ -219,7 +217,6 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } m.submenuTable.MoveDown(1) - // m.box.SetText(m.submenuTable.SelectedRow()[0], "temp description for subtable") return m,nil case "i": if overBounds(m.submenuTable){ @@ -244,12 +241,12 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.submenuActive = false // Hide submenu m.table.Focus() m.submenuTable.Blur() + m.table.SetShowCursor(true) if m.table.SelectedRow() == nil{ m.table.SetCursor(0) return m, nil } - // m.box.SetText(m.table.SelectedRow()[0], "temp description for main table") return m, nil case "q", "ctrl+c": @@ -324,10 +321,10 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.submenuActive = true m.table.Blur() m.submenuTable.Focus() + m.table.SetShowCursor(false) } if len(m.submenuTable.Rows()) > 0{ m.submenuTable.SetCursor(0) - // m.box.SetText(m.submenuTable.SelectedRow()[0], "temp description for subtable") } case "up": if overBounds(m.table){ @@ -341,7 +338,6 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } m.table.MoveUp(1) - // m.box.SetText(m.table.SelectedRow()[0], "temp description for main table") return m,nil case "down": if overBounds(m.table){ @@ -355,7 +351,6 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } m.table.MoveDown(1) - // m.box.SetText(m.table.SelectedRow()[0], "temp description for main table") return m,nil } case CANMsg: @@ -368,6 +363,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var errorNumberStr string = strconv.Itoa(k) + // Instead of "error" + #, this would be what the DBC file should map to, for now keep this for testing if val, ok := m.hiddenCounts["error" + errorNumberStr]; ok{ m.hiddenCounts["error" + errorNumberStr ] = val + 1 @@ -376,6 +372,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.errorToBit["error" + errorNumberStr] = k } + index := findRowString(m.table.Rows(), "error" + errorNumberStr) if index == -1{ newRow := append(m.table.Rows(), table.Row{ "error" + errorNumberStr, strconv.Itoa(m.hiddenCounts["error" + errorNumberStr]),"0"}) @@ -387,8 +384,6 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { newRows[index][2] = "0" m.table.SetRows(newRows) } - - } m.SortByColumn(2) m.table.UpdateViewport() @@ -466,48 +461,76 @@ func subMenuKeyMap() KeyMap { } } +// View returns the string representation of the model. We could combine and condense, but its clearer to keep it separate. func (m model) View() string { - out := baseStyle.Render(m.table.View()) + "\n" + + out := "\n \n \n" + // Help menu changes based on the state of the submenu + if m.submenuActive{ + out = m.table.HelpView() + "\n" + + m.submenuTable.Help.ShortHelpView(m.submenuKeys.ShortHelp()) + "\n" + }else{ + out = m.table.HelpView() + "\n" + + m.table.Help.ShortHelpView(m.tableKeys.ShortHelp()) + "\n" + } + + // Timeout warning if m.isTimeout{ out = fmt.Sprint("\n\x1b[41m\x1b[37mWarning! Last recorded message was over ",math.Round(time.Since(m.lastCANTime).Seconds()), " seconds ago!\x1b[0m") + "\n" + out } + + // Render the table + out = out + baseStyle.Render(m.table.View()) + "\n" + + // Render the submenu if it is active if m.submenuActive{ - if m.submenuTable.SelectedRow() != nil{ - m.box.SetText(m.submenuTable.SelectedRow()[0], "temp description for sub table") - } - out = out + m.box.View(); - out = out + "\n" + - baseStyle.Render(m.submenuTable.View()) + "\n" + - m.table.HelpView() + "\n" + - m.submenuTable.Help.ShortHelpView(m.submenuKeys.ShortHelp()) + "\n" - }else{ - if m.table.SelectedRow() != nil { - m.box.SetText(m.table.SelectedRow()[0], "temp description for main table") - } + out = out + baseStyle.Render(m.submenuTable.View()) + "\n" + } + + // Render the box + if m.submenuActive && m.submenuTable.SelectedRow() != nil{ + m.box.SetText(m.submenuTable.SelectedRow()[0], "temp description for " + m.submenuTable.SelectedRow()[0]) out = out + m.box.View() - out = out + "\n" + - m.table.HelpView() + "\n" + - m.table.Help.ShortHelpView(m.tableKeys.ShortHelp()) + "\n" - } - // if m.showBox{ - // out = out + m.box.View(); - // } - // if m.submenuActive { - // // Display both the main menu and submenu - // out = out + "\n" + - // baseStyle.Render(m.submenuTable.View()) + "\n" + - // m.table.HelpView() + "\n" + - // m.submenuTable.Help.ShortHelpView(m.submenuKeys.ShortHelp()) + "\n" - // }else{ - // out = out + "\n" + - // m.table.HelpView() + "\n" + - // m.table.Help.ShortHelpView(m.tableKeys.ShortHelp()) + "\n" - // } - return out; + } else if m.table.SelectedRow() != nil{ + m.box.SetText(m.table.SelectedRow()[0], "temp description for " + m.table.SelectedRow()[0]) + out = out + m.box.View() + } + + return out } -func createTable(warning int) model{ +func initViewer(warning int) model{ + mainTable := createTable() + table2 := createSubMenu() + + box := box.Box{}; + // Each column has padding of 2, so the width is 60 + 2*3(error columns) = 66 + box.SetWidth(66) + + m := model{ + mainTable, + additionalKeyMap(), + + table2, + false, + subMenuKeyMap(), + + map[string]int{}, + map[string]int{}, + ^uint64(0), + + false, + time.Now(), + time.Duration(warning) * time.Second, + + box, + false, + } + return m +} + +func createTable() table.Model{ columns := []table.Column{ {Title: "Error:", Width: 40}, {Title: "Count:", Width: 10}, @@ -521,7 +544,7 @@ func createTable(warning int) model{ table.WithColumns(columns), table.WithRows(rows), table.WithFocused(true), - table.WithHeight(7), + table.WithHeight(11), ) s := table.DefaultStyles() @@ -535,38 +558,13 @@ func createTable(warning int) model{ Background(lipgloss.Color("57")). Bold(false) t.SetStyles(s) - - table2 := createSubMenu() - - box := component.Box{}; - box.SetWidth(60) - - m := model{ - t, - additionalKeyMap(), - - table2, - false, - subMenuKeyMap(), - - map[string]int{}, - map[string]int{}, - ^uint64(0), - - false, - time.Now(), - time.Duration(warning) * time.Second, - - box, - false, - } - return m + return t } // Initialization of the submenu func createSubMenu() table.Model { columns := []table.Column{ - {Title: "Ignored Errors:", Width: 30}, + {Title: "Ignored Errors:", Width: 60 + 4} , } rows := []table.Row{} @@ -584,8 +582,8 @@ func createSubMenu() table.Model { BorderBottom(true). Bold(false) s.Selected = s.Selected. - Foreground(lipgloss.Color("112")). - Background(lipgloss.Color("240")). + Foreground(lipgloss.Color("229")). + Background(lipgloss.Color("57")). Bold(false) t.SetStyles(s) @@ -659,7 +657,7 @@ func main() { } } - var m model = createTable(warning) + var m model = initViewer(warning) quit := make(chan struct{}) p := tea.NewProgram(m) From 4ef003d082838638e3fea603b931df1c0fcd2418 Mon Sep 17 00:00:00 2001 From: Samuel Shi Date: Mon, 11 Nov 2024 19:21:00 -0500 Subject: [PATCH 07/22] refactored some of the code to reduce redundancies, added comments to make code more legible. --- scripts/can_errgo/components/box/text-box.go | 4 +- scripts/can_errgo/error_tui.go | 120 +++++++++---------- 2 files changed, 60 insertions(+), 64 deletions(-) diff --git a/scripts/can_errgo/components/box/text-box.go b/scripts/can_errgo/components/box/text-box.go index e6276d9f3..3c107805a 100644 --- a/scripts/can_errgo/components/box/text-box.go +++ b/scripts/can_errgo/components/box/text-box.go @@ -1,4 +1,4 @@ -// component/box.go +// Custom text-box component for rendering a box with a title and description, will dynamically wrap text to fit within the box width. package box import ( @@ -23,6 +23,7 @@ func (b *Box) SetText(title, description string) { b.Description = description } +// View renders the Box component func (b Box) View() string { borderStyle := lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).Padding(1, 2).BorderForeground(lipgloss.Color("57")) titleStyle := lipgloss.NewStyle().Bold(true).Underline(true).Align(lipgloss.Center).Width(b.Width).MaxWidth(b.Width) @@ -37,6 +38,7 @@ func (b Box) View() string { return borderStyle.Width(b.Width).Height(boxHeight).Render(content) } +// Function to wrap text to fit within the box width func (b Box) wrapText(text string) string { var wrapped string maxLineLength := b.Width - 4 diff --git a/scripts/can_errgo/error_tui.go b/scripts/can_errgo/error_tui.go index 6309d312d..2be34c40f 100644 --- a/scripts/can_errgo/error_tui.go +++ b/scripts/can_errgo/error_tui.go @@ -37,6 +37,7 @@ func deepCopy(rows []table.Row) []table.Row { return newRows } +// Converts a 8-byte slice to a uint64, this is for masking purposes. func bytesToUint64(b []byte) uint64 { var result uint64 for i := 0; i < len(b) && i < 8; i++ { @@ -56,24 +57,30 @@ type CANMsg struct { Value uint64 } +// Empty type for the tick message type TickMsg struct{} type model struct { + // Main table table table.Model tableKeys KeyMap + // Submenu table submenuTable table.Model submenuActive bool submenuKeys KeyMap + // Hidden error counts and error to bit mapping hiddenCounts map[string]int errorToBit map[string]int ignoreMask uint64 + // Timeout flag and last CAN message time isTimeout bool lastCANTime time.Time t time.Duration + // Box for displaying error descriptions box box.Box showBox bool } @@ -117,6 +124,7 @@ func findRow(s []table.Row, e table.Row) int { return -1 } +// Find the index of a row with a specific error name. func findRowString(s []table.Row, e string) int { for i, a := range s { if a[0] == e { @@ -126,6 +134,7 @@ func findRowString(s []table.Row, e string) int { return -1 } +// Check if the cursor is over bounds func overBounds(table table.Model) bool{ return len(table.Rows())==0 || table.Cursor() < 0 || table.Cursor() >= len(table.Rows()) } @@ -142,6 +151,7 @@ func (m *model) re_fresh(){ } } +// Sort the table by the specified column index. func (m *model) SortByColumn(columnIndex int) error { rows := m.table.Rows() // Check if sorting should adjust cursor position (In the odd case that no cursor is set, we don't want to run into a panic) @@ -197,6 +207,8 @@ func (m *model) SortByColumn(columnIndex int) error { func (m model) Init() tea.Cmd { return tickEvery(m.t) } + +// Table and ignore subtable update loop. func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd switch msg := msg.(type) { @@ -205,32 +217,22 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // If submenu is active, only let the submenu handle KeyMsg switch msg.String() { case "up": - if overBounds(m.submenuTable){ - m.table.SetCursor(0) - return m, nil - } m.submenuTable.MoveUp(1) return m,nil case "down": - if overBounds(m.submenuTable){ - m.table.SetCursor(0) - return m, nil - } m.submenuTable.MoveDown(1) return m,nil case "i": - if overBounds(m.submenuTable){ - m.table.SetCursor(0) - return m, nil - } - if m.submenuTable.SelectedRow() == nil{ return m, nil } + // Toggle the mask for the selected error and delete the row. m.ignoreMask = toggleBit(m.ignoreMask, m.errorToBit[m.submenuTable.SelectedRow()[0]]) newRows := deleteElementRow(m.submenuTable.Rows(), m.submenuTable.Cursor()) m.submenuTable.SetRows(newRows) + + // May be unnecessary, but just in case we update the cursor. if m.submenuTable.Cursor() == 0{ m.submenuTable.MoveDown(1) }else{ @@ -239,16 +241,12 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case "s": m.submenuActive = false // Hide submenu - m.table.Focus() - m.submenuTable.Blur() - m.table.SetShowCursor(true) + m.table.Focus() // Refocus the main table + m.submenuTable.Blur() // Unfocus the submenu + m.table.SetShowCursor(true) // Reshow the cursor on the main table - if m.table.SelectedRow() == nil{ - m.table.SetCursor(0) - return m, nil - } return m, nil - + case "q", "ctrl+c": return m, tea.Quit } @@ -267,21 +265,20 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } case "q", "ctrl+c": return m, tea.Quit - case "a": - - if overBounds(m.table){ - m.table.SetCursor(0) - return m, nil - } + case "a": + // Prevent panic if no row is selected if m.table.SelectedRow() == nil{ - m.table.SetCursor(0) + m.table.SetCursorAndViewport(0) return m, nil } + // Remove the row and reset its count. m.hiddenCounts[m.table.SelectedRow()[0]] = 0 newRows := deleteElementRow(m.table.Rows(), m.table.Cursor()) m.table.SetRows(newRows) + + // May be unnecessary, but just in case we update the cursor. if m.table.Cursor() == 0{ m.table.MoveDown(1) }else{ @@ -289,25 +286,25 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } return m, nil case "i": - - if overBounds(m.table){ - m.table.SetCursor(0) - return m, nil - } - + + // Prevent panic if no row is selected if m.table.SelectedRow() == nil{ - m.table.SetCursor(0) + m.table.SetCursorAndViewport(0) return m, nil } - + + // Toggle the mask for the selected error and move it to the ignore menu. m.ignoreMask = toggleBit(m.ignoreMask, m.errorToBit[m.table.SelectedRow()[0]] ) newSubRow := append(m.submenuTable.Rows(), table.Row{m.table.SelectedRow()[0]}) m.submenuTable.SetRows(newSubRow) m.submenuTable.UpdateViewport() + // Remove the row from the main table and reset its count. m.hiddenCounts[m.table.SelectedRow()[0]] = 0 newRows := deleteElementRow(m.table.Rows(), m.table.Cursor()) m.table.SetRows(newRows) + + // May be unnecessary, but just in case we update the cursor. m.table.MoveDown(1) return m, nil @@ -323,41 +320,23 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.submenuTable.Focus() m.table.SetShowCursor(false) } - if len(m.submenuTable.Rows()) > 0{ - m.submenuTable.SetCursor(0) - } case "up": - if overBounds(m.table){ - m.table.SetCursor(0) - return m, nil - } - - if m.table.SelectedRow() == nil{ - m.table.SetCursor(0) - return m, nil - } - m.table.MoveUp(1) return m,nil case "down": - if overBounds(m.table){ - m.table.SetCursor(0) - return m, nil - } - - if m.table.SelectedRow() == nil{ - m.table.SetCursor(0) - return m, nil - } - m.table.MoveDown(1) return m,nil } case CANMsg: + // Reset the timeout flag and update the last CAN message time m.lastCANTime = time.Now() m.isTimeout = false + // Perform bitwise AND to filter out the bits that are not of interest msg.Value = msg.Value & m.ignoreMask + // Update the freshness of the errors m.re_fresh() + + for k := 0; k < 64; k++ { // Iterate through all 64 bits if msg.Value&(1< m.t { m.isTimeout = true @@ -396,6 +378,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, tickEvery(m.t) } + // Update the table with the new message if _, ok := msg.(CANMsg); ok { // Update the mainTable regardless of submenu state m.table, cmd = m.table.Update(msg) @@ -408,6 +391,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, cmd } +// KeyMap is a set of keybindings for the application. type KeyMap struct { a key.Binding i key.Binding @@ -530,11 +514,14 @@ func initViewer(warning int) model{ return m } -func createTable() table.Model{ +// Default setup for the main table +func createTable() table.Model{ + + // True Width = 60 + 2(padding) * (# cols) = 66 columns := []table.Column{ {Title: "Error:", Width: 40}, {Title: "Count:", Width: 10}, - {Title: "Freshness", Width: 10}, + {Title: "Recency", Width: 10}, } rows := []table.Row {} @@ -544,6 +531,7 @@ func createTable() table.Model{ table.WithColumns(columns), table.WithRows(rows), table.WithFocused(true), + // The header will take up 1 row. If we want N rows, we need to set the height to N+1 table.WithHeight(11), ) @@ -563,6 +551,7 @@ func createTable() table.Model{ // Initialization of the submenu func createSubMenu() table.Model { + // Submenu has 1 column meaning it's true width is 60 + 2(padding) = 62, we need to account for the padding of the main table (66), so we add 4. columns := []table.Column{ {Title: "Ignored Errors:", Width: 60 + 4} , } @@ -572,6 +561,7 @@ func createSubMenu() table.Model { table.WithColumns(columns), table.WithRows(rows), table.WithFocused(true), + // The header will take up 1 row. If we want N rows, we need to set the height to N+1 table.WithHeight(5), ) @@ -645,7 +635,7 @@ func main() { var warning int if *warnFlag == "" { - warning = 999 // Default value + warning = 5 // Default value } else { // Parse the value for the -w flag var err error @@ -657,9 +647,12 @@ func main() { } } + // Initialize the model and run the program var m model = initViewer(warning) + // channel to listen for the quit signal quit := make(chan struct{}) + // Start the TUI p := tea.NewProgram(m) go func() { if _, err := p.Run(); err != nil { @@ -667,6 +660,7 @@ func main() { } close(quit) }() + // Start the CAN listener go can_listener(p) From d9b345ae07c2c946c5d50412718cf324797a311c Mon Sep 17 00:00:00 2001 From: Samuel Shi Date: Mon, 11 Nov 2024 23:01:58 -0500 Subject: [PATCH 08/22] small fix to description box showing the main table description when the error table is opened, and added a testing script --- scripts/can_errgo/error_tui.go | 2 +- scripts/can_errgo/utilityFiles/test_64.sh | 49 +++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 scripts/can_errgo/utilityFiles/test_64.sh diff --git a/scripts/can_errgo/error_tui.go b/scripts/can_errgo/error_tui.go index 2be34c40f..f4e3e5b23 100644 --- a/scripts/can_errgo/error_tui.go +++ b/scripts/can_errgo/error_tui.go @@ -475,7 +475,7 @@ func (m model) View() string { if m.submenuActive && m.submenuTable.SelectedRow() != nil{ m.box.SetText(m.submenuTable.SelectedRow()[0], "temp description for " + m.submenuTable.SelectedRow()[0]) out = out + m.box.View() - } else if m.table.SelectedRow() != nil{ + } else if m.table.SelectedRow() != nil && !m.submenuActive{ m.box.SetText(m.table.SelectedRow()[0], "temp description for " + m.table.SelectedRow()[0]) out = out + m.box.View() } diff --git a/scripts/can_errgo/utilityFiles/test_64.sh b/scripts/can_errgo/utilityFiles/test_64.sh new file mode 100644 index 000000000..49ffc3c15 --- /dev/null +++ b/scripts/can_errgo/utilityFiles/test_64.sh @@ -0,0 +1,49 @@ + +#!/bin/bash + +# Infinite loop +while true +do + # Send CAN message + cansend vcan0 FFF#0000000000000001 + sleep 1 + cansend vcan0 FFF#0000000000000002 + sleep 1 + cansend vcan0 FFF#0000000000000004 + sleep 1 + cansend vcan0 FFF#0000000000000008 + sleep 1 + cansend vcan0 FFF#0000000000000010 + sleep 1 + cansend vcan0 FFF#0000000000000020 + sleep 1 + cansend vcan0 FFF#0000000000000040 + sleep 1 + cansend vcan0 FFF#0000000000000080 + sleep 1 + cansend vcan0 FFF#0000000000000100 + sleep 1 + cansend vcan0 FFF#0000000000000200 + sleep 1 + cansend vcan0 FFF#0000000000000400 + sleep 1 + cansend vcan0 FFF#0000000000000800 + sleep 1 + cansend vcan0 FFF#0000000000001000 + sleep 1 + cansend vcan0 FFF#0000000000002000 + sleep 1 + cansend vcan0 FFF#0000000000004000 + sleep 1 + cansend vcan0 FFF#0000000000008000 + sleep 1 + cansend vcan0 FFF#0000000000010000 + sleep 1 + cansend vcan0 FFF#0000000000020000 + sleep 1 + cansend vcan0 FFF#0000000000040000 + sleep 1 + cansend vcan0 FFF#0000000000080000 + # Wait for 1 seconds + sleep 1 +done \ No newline at end of file From 648dd8118e9ff1cb92da64f36350847110bbf3c2 Mon Sep 17 00:00:00 2001 From: Samuel Shi Date: Sun, 17 Nov 2024 16:53:42 -0500 Subject: [PATCH 09/22] added structs to store data and changed functionality to implement, moved box/table under components, removed demos. --- .../FrontController/vehicle_control_system | 1 + .../vehicle_control_system | 1 + scripts/can_errgo/Demos/listenerDemo.go | 92 ------ scripts/can_errgo/Demos/threadDemo.go | 29 -- .../can_errgo/components/{table => }/table.go | 3 +- .../components/{box => }/text-box.go | 2 +- scripts/can_errgo/error_tui.go | 301 ++++++++---------- 7 files changed, 143 insertions(+), 286 deletions(-) create mode 160000 firmware/projects/FrontController/vehicle_control_system create mode 160000 firmware/projects/debug/FrontControllerSimple/vehicle_control_system delete mode 100644 scripts/can_errgo/Demos/listenerDemo.go delete mode 100644 scripts/can_errgo/Demos/threadDemo.go rename scripts/can_errgo/components/{table => }/table.go (99%) rename scripts/can_errgo/components/{box => }/text-box.go (98%) diff --git a/firmware/projects/FrontController/vehicle_control_system b/firmware/projects/FrontController/vehicle_control_system new file mode 160000 index 000000000..4fddb9369 --- /dev/null +++ b/firmware/projects/FrontController/vehicle_control_system @@ -0,0 +1 @@ +Subproject commit 4fddb9369730a8cee4e74f83d71d3b1e33b8d5ea diff --git a/firmware/projects/debug/FrontControllerSimple/vehicle_control_system b/firmware/projects/debug/FrontControllerSimple/vehicle_control_system new file mode 160000 index 000000000..a04e3a93f --- /dev/null +++ b/firmware/projects/debug/FrontControllerSimple/vehicle_control_system @@ -0,0 +1 @@ +Subproject commit a04e3a93f0def3d748ca7c636cb0f226328c0a10 diff --git a/scripts/can_errgo/Demos/listenerDemo.go b/scripts/can_errgo/Demos/listenerDemo.go deleted file mode 100644 index 786450a86..000000000 --- a/scripts/can_errgo/Demos/listenerDemo.go +++ /dev/null @@ -1,92 +0,0 @@ -package main - -import ( - "encoding/binary" - "fmt" - "log" - "net" - "golang.org/x/sys/unix" -) - -const ( - canInterface = "vcan0" // Use the virtual CAN interface - canFrameSize = 16 // CAN frame size (for a standard CAN frame) -) - -func IntPow(n, m int) int { - if m == 0 { - return 1 - } - - if m == 1 { - return n - } - - result := n - for i := 2; i <= m; i++ { - result *= n - } - return result -} - - -func main() { - // Create a raw CAN socket using the unix package - var sock int; - var err error; - var mask uint64 = ^uint64(0) // All 64 bits set to 1 - - fmt.Printf("%d",mask) - - sock, err = unix.Socket(unix.AF_CAN, unix.SOCK_RAW, unix.CAN_RAW) - if err != nil { - log.Fatalf("Error creating CAN socket: %v", err) - } - defer unix.Close(sock) - - // Get the index of the interface (vcan0) - ifi, err := net.InterfaceByName(canInterface) - if err != nil { - log.Fatalf("Error getting interface index: %v", err) - } - - // Bind the socket to the CAN interface - addr := &unix.SockaddrCAN{Ifindex: ifi.Index} - if err := unix.Bind(sock, addr); err != nil { - log.Fatalf("Error binding CAN socket: %v", err) - } - - // Create a buffer to hold incoming CAN frames - buf := make([]byte, canFrameSize) - - fmt.Println("Listening for CAN messages on", canInterface) - - for { - // Receive CAN message - _, err := unix.Read(sock, buf) - if err != nil { - log.Printf("Error receiving CAN message: %v", err) - continue - } - - // Extract CAN ID and data - canID := binary.LittleEndian.Uint32(buf[0:4]) & 0x1FFFFFFF // 29-bit CAN ID (masked) - data := buf[8:] //& mask // CAN data bytes (8 bytes) - fmt.Printf("CAN ID: 0x%X, Data: %08b\n", canID, data) - for i :=len(data) - 1; i >= 0; i--{ - bit := 1 - //fmt.Printf("CAN ID: 0x%X, Data: %08b\n",canID, data[i]) - for k :=0; k < 8; k++{ - //fmt.Printf("%d\n", IntPow(2,k)) - if (data[i] & byte(bit) != 0){ - fmt.Printf("error%d occured\n", (7-i)*8 + k) - - } - bit*=2 - } - - // Print the CAN message ID and data - //fmt.Printf("CAN ID: 0x%X, Data: %08b\n", canID, data) - }} -} - diff --git a/scripts/can_errgo/Demos/threadDemo.go b/scripts/can_errgo/Demos/threadDemo.go deleted file mode 100644 index fb92db586..000000000 --- a/scripts/can_errgo/Demos/threadDemo.go +++ /dev/null @@ -1,29 +0,0 @@ -package main - -import ( - "fmt" - "time" -) - - - -func printHello(seconds float32){ - for { - time.Sleep(time.Duration(seconds * 1000) * time.Millisecond) - fmt.Printf("Hello") - } -} - -func printWorld(seconds float32){ - for { - time.Sleep(time.Duration(seconds * 1000) * time.Millisecond) - fmt.Printf("World\n") - } -} - -func main(){ - go printHello(4) - go printWorld(1) - for{ - } -} diff --git a/scripts/can_errgo/components/table/table.go b/scripts/can_errgo/components/table.go similarity index 99% rename from scripts/can_errgo/components/table/table.go rename to scripts/can_errgo/components/table.go index 649eb241f..024b75f8a 100644 --- a/scripts/can_errgo/components/table/table.go +++ b/scripts/can_errgo/components/table.go @@ -1,5 +1,6 @@ +package components + // Modified from "github.com/charmbracelet/bubbles/table" -package table import ( "strings" diff --git a/scripts/can_errgo/components/box/text-box.go b/scripts/can_errgo/components/text-box.go similarity index 98% rename from scripts/can_errgo/components/box/text-box.go rename to scripts/can_errgo/components/text-box.go index 3c107805a..316c3145b 100644 --- a/scripts/can_errgo/components/box/text-box.go +++ b/scripts/can_errgo/components/text-box.go @@ -1,5 +1,5 @@ // Custom text-box component for rendering a box with a title and description, will dynamically wrap text to fit within the box width. -package box +package components import ( "strings" diff --git a/scripts/can_errgo/error_tui.go b/scripts/can_errgo/error_tui.go index f4e3e5b23..420cda34a 100644 --- a/scripts/can_errgo/error_tui.go +++ b/scripts/can_errgo/error_tui.go @@ -7,13 +7,12 @@ import ( "log" "math" "net" - "os" "sort" "strconv" "time" - box "main/components/box" - table "main/components/table" + box "main/components" + table "main/components" "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" @@ -24,19 +23,9 @@ import ( const ( canInterface = "vcan0" // Use the virtual CAN interface - canFrameSize = 16 // CAN frame size (for a standard CAN frame) + canFrameSize = 16 // CAN frame size (for a standard CAN frame) ) -// Might have future use -func deepCopy(rows []table.Row) []table.Row { - newRows := make([]table.Row, len(rows)) - for i := range rows { - newRows[i] = make(table.Row, len(rows[i])) - copy(newRows[i], rows[i]) - } - return newRows -} - // Converts a 8-byte slice to a uint64, this is for masking purposes. func bytesToUint64(b []byte) uint64 { var result uint64 @@ -51,6 +40,90 @@ var baseStyle = lipgloss.NewStyle(). BorderStyle(lipgloss.NormalBorder()). BorderForeground(lipgloss.Color("240")) +func RowValuesToRow(r RowValues) table.Row{ + return table.Row{r.Error, strconv.Itoa(r.Count), strconv.Itoa(r.Recency)} +} + +func SortRowValuesBy(r []RowValues, column int) []RowValues { + // Create a deep copy of the input slice + copied := make([]RowValues, len(r)) + copy(copied, r) + + // Sort the copied slice + sort.SliceStable(copied, func(i, j int) bool { + switch column { + case 0: + return copied[i].Error < copied[j].Error + case 1: + return copied[i].Count < copied[j].Count + case 2: + return copied[i].Recency < copied[j].Recency + } + return false + }) + + return copied +} + + + +func toTableRows(r []RowValues) []table.Row { + var rows []table.Row + for _, v := range r { + // Check if the RowValues is "non-empty" and Active + if v.Error != "" && v.Count != 0 && v.Active{ + rows = append(rows, RowValuesToRow(v)) + } + } + return rows +} + +func (m model) getDescription(errorName string) string { + for i := range(len(m.rowsValues)){ + if m.rowsValues[i].Error == errorName{ + return m.rowsValues[i].Description + } + } + return "No description available" +} + +func (m model) findError(errorName string) int { + for i := range(len(m.rowsValues)){ + if m.rowsValues[i].Error == errorName{ + return i + } + } + return -1 +} + +func (m model) resetRowValue(errorName string) { + for i := range(len(m.rowsValues)){ + if m.rowsValues[i].Error == errorName{ + m.rowsValues[i].Count = 0 + m.rowsValues[i].Recency = 0 + } + } +} + + +func (m model) getIgnoredRows() []table.Row { + var rows []table.Row + for i := range(len(m.rowsValues)){ + if !m.rowsValues[i].Active && m.rowsValues[i].Error != ""{ + rows = append(rows, table.Row{m.rowsValues[i].Error}) + } + } + return rows +} + +type RowValues struct{ + Error string + Count int + Recency int + Description string + Active bool +} + type CANMsg struct { ID uint32 @@ -71,8 +144,7 @@ type model struct { submenuKeys KeyMap // Hidden error counts and error to bit mapping - hiddenCounts map[string]int - errorToBit map[string]int + rowsValues []RowValues ignoreMask uint64 // Timeout flag and last CAN message time @@ -93,20 +165,6 @@ func toggleBit(n uint64, bitPosition int) uint64 { return n ^ mask } - -func deleteElementRow(slice []table.Row, index int) []table.Row { - // Create a new slice with the same length minus 1 - newSlice := make([]table.Row, 0, len(slice)-1) - - // Append the elements before the index - newSlice = append(newSlice, slice[:index]...) - - // Append the elements after the index - newSlice = append(newSlice, slice[index+1:]...) - - return newSlice -} - func tickEvery(t time.Duration) tea.Cmd { return func() tea.Msg { time.Sleep(t) @@ -114,16 +172,6 @@ func tickEvery(t time.Duration) tea.Cmd { } } -// Might have future use -func findRow(s []table.Row, e table.Row) int { - for i, a := range s { - if a[0] == e[0] { - return i - } - } - return -1 -} - // Find the index of a row with a specific error name. func findRowString(s []table.Row, e string) int { for i, a := range s { @@ -139,75 +187,17 @@ func overBounds(table table.Model) bool{ return len(table.Rows())==0 || table.Cursor() < 0 || table.Cursor() >= len(table.Rows()) } - -func (m *model) re_fresh(){ - rows := m.table.Rows() - for i:=0; i < len(rows); i++{ - if val,err := strconv.Atoi(rows[i][2]); err != nil{ - fmt.Printf("Something went wrong during refresh!") - } else{ - rows[i][2] = strconv.Itoa(val + 1) +func (m *model) refresh(){ + rows := m.rowsValues + for i := range(len(rows)){ + if m.rowsValues[i].Count !=0{ + m.rowsValues[i].Recency += 1 } } } -// Sort the table by the specified column index. -func (m *model) SortByColumn(columnIndex int) error { - rows := m.table.Rows() - // Check if sorting should adjust cursor position (In the odd case that no cursor is set, we don't want to run into a panic) - if !overBounds(m.table) { - // Capture the current row data before sorting - currentIndex := m.table.Cursor() - selectedRow := rows[currentIndex] - - // Perform sorting with a custom less function - sort.SliceStable(rows, func(i, j int) bool { - if columnIndex >= len(rows[i]) || columnIndex >= len(rows[j]) { - fmt.Println("Error: columnIndex out of bounds") - return false - } - - val1, err1 := strconv.Atoi(rows[i][columnIndex]) - val2, err2 := strconv.Atoi(rows[j][columnIndex]) - - if err1 == nil && err2 == nil { - return val1 < val2 - } - return rows[i][columnIndex] < rows[j][columnIndex] - }) - - // Find new index of the originally selected row - for i, row := range rows { - if row[0] == selectedRow[0] { - m.table.SetCursorAndViewport(i) - break - } - } - - } else { - // Just sort if out of bounds - sort.SliceStable(rows, func(i, j int) bool { - if columnIndex >= len(rows[i]) || columnIndex >= len(rows[j]) { - fmt.Println("Error: columnIndex out of bounds") - return false - } - - val1, err1 := strconv.Atoi(rows[i][columnIndex]) - val2, err2 := strconv.Atoi(rows[j][columnIndex]) - - if err1 == nil && err2 == nil { - return val1 < val2 - } - return rows[i][columnIndex] < rows[j][columnIndex] - }) - } - return nil -} - - func (m model) Init() tea.Cmd { return tickEvery(m.t) } - // Table and ignore subtable update loop. func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd @@ -228,8 +218,14 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } // Toggle the mask for the selected error and delete the row. - m.ignoreMask = toggleBit(m.ignoreMask, m.errorToBit[m.submenuTable.SelectedRow()[0]]) - newRows := deleteElementRow(m.submenuTable.Rows(), m.submenuTable.Cursor()) + // m.ignoreMask = toggleBit(m.ignoreMask, m.errorToBit[m.submenuTable.SelectedRow()[0]]) + // newRows := deleteElementRow(m.submenuTable.Rows(), m.submenuTable.Cursor()) + // m.submenuTable.SetRows(newRows) + + errorIndex := m.findError(m.submenuTable.SelectedRow()[0]) + m.ignoreMask = toggleBit(m.ignoreMask, errorIndex) + m.rowsValues[errorIndex].Active = true + newRows := m.getIgnoredRows() m.submenuTable.SetRows(newRows) // May be unnecessary, but just in case we update the cursor. @@ -274,10 +270,12 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } // Remove the row and reset its count. - m.hiddenCounts[m.table.SelectedRow()[0]] = 0 - newRows := deleteElementRow(m.table.Rows(), m.table.Cursor()) + errorIndex := m.findError(m.table.SelectedRow()[0]) + m.rowsValues[errorIndex].Count = 0 + newRows := toTableRows(m.rowsValues) m.table.SetRows(newRows) + // May be unnecessary, but just in case we update the cursor. if m.table.Cursor() == 0{ m.table.MoveDown(1) @@ -294,16 +292,18 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } // Toggle the mask for the selected error and move it to the ignore menu. - m.ignoreMask = toggleBit(m.ignoreMask, m.errorToBit[m.table.SelectedRow()[0]] ) - newSubRow := append(m.submenuTable.Rows(), table.Row{m.table.SelectedRow()[0]}) - m.submenuTable.SetRows(newSubRow) - m.submenuTable.UpdateViewport() + errorIndex := m.findError(m.table.SelectedRow()[0]) + m.ignoreMask = toggleBit(m.ignoreMask, errorIndex) + m.rowsValues[errorIndex].Active = false // Remove the row from the main table and reset its count. - m.hiddenCounts[m.table.SelectedRow()[0]] = 0 - newRows := deleteElementRow(m.table.Rows(), m.table.Cursor()) + m.rowsValues[errorIndex].Count = 0 + newRows := toTableRows(m.rowsValues) m.table.SetRows(newRows) + //Set add the ignored rows to submenu + ignoredRows := m.getIgnoredRows() + m.submenuTable.SetRows(ignoredRows) // May be unnecessary, but just in case we update the cursor. m.table.MoveDown(1) @@ -334,39 +334,28 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Perform bitwise AND to filter out the bits that are not of interest msg.Value = msg.Value & m.ignoreMask // Update the freshness of the errors - m.re_fresh() - - + m.refresh() for k := 0; k < 64; k++ { // Iterate through all 64 bits if msg.Value&(1< Date: Sun, 17 Nov 2024 17:35:20 -0500 Subject: [PATCH 10/22] Removed last instance of findRowString and added cli flag for canInterface --- scripts/can_errgo/error_tui.go | 52 ++++++++++++++++------------------ 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/scripts/can_errgo/error_tui.go b/scripts/can_errgo/error_tui.go index 420cda34a..53c607cde 100644 --- a/scripts/can_errgo/error_tui.go +++ b/scripts/can_errgo/error_tui.go @@ -21,10 +21,7 @@ import ( ) -const ( - canInterface = "vcan0" // Use the virtual CAN interface - canFrameSize = 16 // CAN frame size (for a standard CAN frame) -) +const canFrameSize = 16 // CAN frame size (for a standard CAN frame) // Converts a 8-byte slice to a uint64, this is for masking purposes. func bytesToUint64(b []byte) uint64 { @@ -65,8 +62,6 @@ func SortRowValuesBy(r []RowValues, column int) []RowValues { return copied } - - func toTableRows(r []RowValues) []table.Row { var rows []table.Row for _, v := range r { @@ -87,14 +82,26 @@ func (m model) getDescription(errorName string) string { return "No description available" } -func (m model) findError(errorName string) int { - for i := range(len(m.rowsValues)){ - if m.rowsValues[i].Error == errorName{ +func findError(errorName string, rowValues []RowValues) int { + for i := range(len(rowValues)){ + if rowValues[i].Error == errorName{ return i } } return -1 } +func findErrorAfterSort(errorName string, rowValues []RowValues) int { + cnt := 0 + for i := range(len(rowValues)){ + if rowValues[i].Error != ""{ + cnt += 1 + } + if rowValues[i].Error == errorName{ + return cnt - 1 + } + } + return -1 +} func (m model) resetRowValue(errorName string) { for i := range(len(m.rowsValues)){ @@ -172,16 +179,6 @@ func tickEvery(t time.Duration) tea.Cmd { } } -// Find the index of a row with a specific error name. -func findRowString(s []table.Row, e string) int { - for i, a := range s { - if a[0] == e { - return i - } - } - return -1 -} - // Check if the cursor is over bounds func overBounds(table table.Model) bool{ return len(table.Rows())==0 || table.Cursor() < 0 || table.Cursor() >= len(table.Rows()) @@ -222,7 +219,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // newRows := deleteElementRow(m.submenuTable.Rows(), m.submenuTable.Cursor()) // m.submenuTable.SetRows(newRows) - errorIndex := m.findError(m.submenuTable.SelectedRow()[0]) + errorIndex := findError(m.submenuTable.SelectedRow()[0], m.rowsValues) m.ignoreMask = toggleBit(m.ignoreMask, errorIndex) m.rowsValues[errorIndex].Active = true newRows := m.getIgnoredRows() @@ -270,7 +267,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } // Remove the row and reset its count. - errorIndex := m.findError(m.table.SelectedRow()[0]) + errorIndex := findError(m.table.SelectedRow()[0], m.rowsValues) m.rowsValues[errorIndex].Count = 0 newRows := toTableRows(m.rowsValues) m.table.SetRows(newRows) @@ -292,7 +289,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } // Toggle the mask for the selected error and move it to the ignore menu. - errorIndex := m.findError(m.table.SelectedRow()[0]) + errorIndex := findError(m.table.SelectedRow()[0], m.rowsValues) m.ignoreMask = toggleBit(m.ignoreMask, errorIndex) m.rowsValues[errorIndex].Active = false @@ -348,15 +345,15 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Sort the table by the recency. currentError := m.table.SelectedRow() - newRows := toTableRows(SortRowValuesBy(m.rowsValues,2)) + newRowValues := SortRowValuesBy(m.rowsValues,2) + newRows := toTableRows(newRowValues) m.table.SetRows(newRows) // Fix the cursor back onto the row it was on before the sort. if currentError != nil{ - newIndex := findRowString(newRows, currentError[0]) + newIndex := findErrorAfterSort(currentError[0], newRowValues) m.table.SetCursorAndViewport(newIndex) } - m.table.UpdateViewport() } return m, nil // If the message is a tick, check if the last CAN message was received more than the timeout duration ago @@ -570,7 +567,7 @@ func createSubMenu() table.Model { } -func can_listener(p *tea.Program) { +func can_listener(p *tea.Program, canInterface string) { // Create a raw CAN socket using the unix package var sock int; var err error; @@ -619,6 +616,7 @@ func can_listener(p *tea.Program) { func main() { // Define the command line flag warnFlag := flag.Int("w", 5, "warning time for the table (integer value)") + canInterfaceFlag := flag.String("i", "vcan0", "CAN interface to listen on") flag.Parse() @@ -636,7 +634,7 @@ func main() { close(quit) }() // Start the CAN listener - go can_listener(p) + go can_listener(p, *canInterfaceFlag) <-quit From 261c523d1850540be53ed33e82481d16294c78fe Mon Sep 17 00:00:00 2001 From: Samuel Shi Date: Sun, 17 Nov 2024 17:58:54 -0500 Subject: [PATCH 11/22] Key behavior attached to keymap --- scripts/can_errgo/error_tui.go | 38 +++++++++++++--------------------- 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/scripts/can_errgo/error_tui.go b/scripts/can_errgo/error_tui.go index 53c607cde..0556053b7 100644 --- a/scripts/can_errgo/error_tui.go +++ b/scripts/can_errgo/error_tui.go @@ -202,23 +202,19 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.KeyMsg: if m.submenuActive{ // If submenu is active, only let the submenu handle KeyMsg - switch msg.String() { - case "up": + switch { + case key.Matches(msg, table.DefaultKeyMap().LineUp): m.submenuTable.MoveUp(1) return m,nil - case "down": + case key.Matches(msg, table.DefaultKeyMap().LineDown): m.submenuTable.MoveDown(1) return m,nil - case "i": + case key.Matches(msg, m.submenuKeys.i): if m.submenuTable.SelectedRow() == nil{ return m, nil } // Toggle the mask for the selected error and delete the row. - // m.ignoreMask = toggleBit(m.ignoreMask, m.errorToBit[m.submenuTable.SelectedRow()[0]]) - // newRows := deleteElementRow(m.submenuTable.Rows(), m.submenuTable.Cursor()) - // m.submenuTable.SetRows(newRows) - errorIndex := findError(m.submenuTable.SelectedRow()[0], m.rowsValues) m.ignoreMask = toggleBit(m.ignoreMask, errorIndex) m.rowsValues[errorIndex].Active = true @@ -232,7 +228,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.submenuTable.MoveUp(1) } - case "s": + case key.Matches(msg, m.submenuKeys.s): m.submenuActive = false // Hide submenu m.table.Focus() // Refocus the main table m.submenuTable.Blur() // Unfocus the submenu @@ -240,7 +236,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil - case "q", "ctrl+c": + case key.Matches(msg, m.tableKeys.q): return m, tea.Quit } @@ -249,16 +245,10 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } - switch msg.String() { - case "esc": - if m.table.Focused() { - m.table.Blur() - } else { - m.table.Focus() - } - case "q", "ctrl+c": + switch { + case key.Matches(msg, m.tableKeys.q): return m, tea.Quit - case "a": + case key.Matches(msg, m.tableKeys.a): // Prevent panic if no row is selected if m.table.SelectedRow() == nil{ @@ -280,7 +270,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.table.MoveUp(1) } return m, nil - case "i": + case key.Matches(msg, m.tableKeys.i): // Prevent panic if no row is selected if m.table.SelectedRow() == nil{ @@ -305,7 +295,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.table.MoveDown(1) return m, nil - case "s": + case key.Matches(msg, m.tableKeys.s): if m.submenuActive { // Hide submenu and return to main menu m.submenuActive = false @@ -317,10 +307,10 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.submenuTable.Focus() m.table.SetShowCursor(false) } - case "up": + case key.Matches(msg, table.DefaultKeyMap().LineUp): m.table.MoveUp(1) return m,nil - case "down": + case key.Matches(msg, table.DefaultKeyMap().LineDown): m.table.MoveDown(1) return m,nil } @@ -408,7 +398,7 @@ func additionalKeyMap() KeyMap { key.WithHelp("s", "Ignored Menu"), ), q: key.NewBinding( - key.WithKeys("q"), + key.WithKeys("q", "ctrl+c"), key.WithHelp("q/ctrl+c", "quit"), ), } From 4a149b68579ebfe29106059972dc5a8712d801aa Mon Sep 17 00:00:00 2001 From: Samuel Shi Date: Sun, 17 Nov 2024 18:30:53 -0500 Subject: [PATCH 12/22] Updated the README.md --- scripts/can_errgo/README.md | 26 +++++++------------ .../{test_64.sh => test_multi.sh} | 0 2 files changed, 9 insertions(+), 17 deletions(-) rename scripts/can_errgo/utilityFiles/{test_64.sh => test_multi.sh} (100%) diff --git a/scripts/can_errgo/README.md b/scripts/can_errgo/README.md index 64eca3a3b..5e96b50c4 100644 --- a/scripts/can_errgo/README.md +++ b/scripts/can_errgo/README.md @@ -1,24 +1,16 @@ -To test the virtual can and go script: +# To test the virtual can and go script -kill all existing instances of can-dump, can-listen, and end ip-links: +Kill all existing instances of ip-links: -ps aux | grep candump -ps aux | grep can-listen -ip link show +`ip link show` +`sudo ip link delete ` +Setup the can network: -kill -kill -sudo ip link delete +`./setup_vcan.sh ` - - -rerun (for now make CAN_PORT vcan0): - -./setup_vcan.sh -candump -L > & -./can_errgo/can-listen & +Start the CLI viewer: +`go run error_tui.go -i ` You can now run: - -cansend # +`cansend #` diff --git a/scripts/can_errgo/utilityFiles/test_64.sh b/scripts/can_errgo/utilityFiles/test_multi.sh similarity index 100% rename from scripts/can_errgo/utilityFiles/test_64.sh rename to scripts/can_errgo/utilityFiles/test_multi.sh From c6681c96303c8ff609485461ebd56206d00f260c Mon Sep 17 00:00:00 2001 From: Samuel Shi Date: Sun, 17 Nov 2024 18:49:14 -0500 Subject: [PATCH 13/22] fixed findError when done after sort --- scripts/can_errgo/error_tui.go | 63 ++++++++++++++++------------------ 1 file changed, 29 insertions(+), 34 deletions(-) diff --git a/scripts/can_errgo/error_tui.go b/scripts/can_errgo/error_tui.go index 0556053b7..ea8deaf8f 100644 --- a/scripts/can_errgo/error_tui.go +++ b/scripts/can_errgo/error_tui.go @@ -33,14 +33,29 @@ func bytesToUint64(b []byte) uint64 { return result } +// Default style for the table var baseStyle = lipgloss.NewStyle(). BorderStyle(lipgloss.NormalBorder()). BorderForeground(lipgloss.Color("240")) +// RowValuesToRow converts a RowValues struct to a table.Row func RowValuesToRow(r RowValues) table.Row{ return table.Row{r.Error, strconv.Itoa(r.Count), strconv.Itoa(r.Recency)} } +// Convert a slice of RowValues to a slice of table.Rows +func toTableRows(r []RowValues) []table.Row { + var rows []table.Row + for _, v := range r { + // Check if the RowValues is "non-empty" and Active + if v.Error != "" && v.Count != 0 && !v.Ignored{ + rows = append(rows, RowValuesToRow(v)) + } + } + return rows +} + +// SortRowValuesBy sorts a slice of RowValues by the specified column, this creates a deep copy of the input slice. func SortRowValuesBy(r []RowValues, column int) []RowValues { // Create a deep copy of the input slice copied := make([]RowValues, len(r)) @@ -62,17 +77,7 @@ func SortRowValuesBy(r []RowValues, column int) []RowValues { return copied } -func toTableRows(r []RowValues) []table.Row { - var rows []table.Row - for _, v := range r { - // Check if the RowValues is "non-empty" and Active - if v.Error != "" && v.Count != 0 && v.Active{ - rows = append(rows, RowValuesToRow(v)) - } - } - return rows -} - +// getDescription returns the description of the error, if the error is not found it returns a default message. func (m model) getDescription(errorName string) string { for i := range(len(m.rowsValues)){ if m.rowsValues[i].Error == errorName{ @@ -82,27 +87,21 @@ func (m model) getDescription(errorName string) string { return "No description available" } +// findError returns the index of the error in the slice of RowValues, if the error is not found it returns -1. func findError(errorName string, rowValues []RowValues) int { - for i := range(len(rowValues)){ - if rowValues[i].Error == errorName{ - return i - } - } - return -1 -} -func findErrorAfterSort(errorName string, rowValues []RowValues) int { cnt := 0 for i := range(len(rowValues)){ - if rowValues[i].Error != ""{ + if rowValues[i].Error == ""{ cnt += 1 } if rowValues[i].Error == errorName{ - return cnt - 1 + return i - cnt } } return -1 } +// resetRowValue resets the count and recency of the error in the slice of RowValues. func (m model) resetRowValue(errorName string) { for i := range(len(m.rowsValues)){ if m.rowsValues[i].Error == errorName{ @@ -112,11 +111,11 @@ func (m model) resetRowValue(errorName string) { } } - +// getIgnoredRows returns a slice of table.Rows that are ignored and have an error message. func (m model) getIgnoredRows() []table.Row { var rows []table.Row for i := range(len(m.rowsValues)){ - if !m.rowsValues[i].Active && m.rowsValues[i].Error != ""{ + if m.rowsValues[i].Ignored && m.rowsValues[i].Error != ""{ rows = append(rows, table.Row{m.rowsValues[i].Error}) } } @@ -128,7 +127,7 @@ type RowValues struct{ Count int Recency int Description string - Active bool + Ignored bool } @@ -150,7 +149,7 @@ type model struct { submenuActive bool submenuKeys KeyMap - // Hidden error counts and error to bit mapping + // Slice of row information and ignore mask for incoming errors/bits rowsValues []RowValues ignoreMask uint64 @@ -179,11 +178,7 @@ func tickEvery(t time.Duration) tea.Cmd { } } -// Check if the cursor is over bounds -func overBounds(table table.Model) bool{ - return len(table.Rows())==0 || table.Cursor() < 0 || table.Cursor() >= len(table.Rows()) -} - +// Increment the recency of all errors func (m *model) refresh(){ rows := m.rowsValues for i := range(len(rows)){ @@ -217,7 +212,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Toggle the mask for the selected error and delete the row. errorIndex := findError(m.submenuTable.SelectedRow()[0], m.rowsValues) m.ignoreMask = toggleBit(m.ignoreMask, errorIndex) - m.rowsValues[errorIndex].Active = true + m.rowsValues[errorIndex].Ignored = false newRows := m.getIgnoredRows() m.submenuTable.SetRows(newRows) @@ -281,7 +276,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Toggle the mask for the selected error and move it to the ignore menu. errorIndex := findError(m.table.SelectedRow()[0], m.rowsValues) m.ignoreMask = toggleBit(m.ignoreMask, errorIndex) - m.rowsValues[errorIndex].Active = false + m.rowsValues[errorIndex].Ignored = true // Remove the row from the main table and reset its count. m.rowsValues[errorIndex].Count = 0 @@ -326,7 +321,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if msg.Value&(1< Date: Tue, 19 Nov 2024 01:17:49 -0500 Subject: [PATCH 14/22] Refactored code. Added metatable, modified table.go to keep track of error imdex, more readme information, no more searching with string, no more "reading from the table (?), and other cleanup. --- .../FrontController/vehicle_control_system | 1 - .../vehicle_control_system | 1 - scripts/can_errgo/README.md | 53 +++++- scripts/can_errgo/components/table.go | 84 +++++----- scripts/can_errgo/error_tui.go | 158 ++++++++---------- scripts/can_errgo/setup.txt | 5 - .../utilityFiles/utility_commands.txt | 10 -- 7 files changed, 156 insertions(+), 156 deletions(-) delete mode 160000 firmware/projects/EV5/FrontController/vehicle_control_system delete mode 160000 firmware/projects/debug/FrontControllerSimple/vehicle_control_system delete mode 100644 scripts/can_errgo/setup.txt delete mode 100644 scripts/can_errgo/utilityFiles/utility_commands.txt diff --git a/firmware/projects/EV5/FrontController/vehicle_control_system b/firmware/projects/EV5/FrontController/vehicle_control_system deleted file mode 160000 index 4fddb9369..000000000 --- a/firmware/projects/EV5/FrontController/vehicle_control_system +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 4fddb9369730a8cee4e74f83d71d3b1e33b8d5ea diff --git a/firmware/projects/debug/FrontControllerSimple/vehicle_control_system b/firmware/projects/debug/FrontControllerSimple/vehicle_control_system deleted file mode 160000 index a04e3a93f..000000000 --- a/firmware/projects/debug/FrontControllerSimple/vehicle_control_system +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a04e3a93f0def3d748ca7c636cb0f226328c0a10 diff --git a/scripts/can_errgo/README.md b/scripts/can_errgo/README.md index 5e96b50c4..496301231 100644 --- a/scripts/can_errgo/README.md +++ b/scripts/can_errgo/README.md @@ -1,16 +1,55 @@ -# To test the virtual can and go script +# CAN Error TUI + +This is a Tangible User Interface program designed to track CAN Errors including their count, recency, description, and name. + +Users can: + +- Acknowledge the error ( Hide the error until it is sent again) +- Ignore the error ( Hides, resets, and stops reading all future occurrences of the error) +- View ignored errors and choose to unignore those errors + +## Setup + +Install required dependencies: + +```bash +sudo apt-get update +sudo apt-get upgrade +sudo apt install net-tools iproute2 can-utils linux-modules-extra-$(uname -r) +``` Kill all existing instances of ip-links: -`ip link show` -`sudo ip link delete ` +```bash +ip link show +sudo ip link delete +``` Setup the can network: -`./setup_vcan.sh ` +```bash +./setup_vcan.sh +``` -Start the CLI viewer: -`go run error_tui.go -i ` +## Usage + +Start the CLI viewer for single use: + +```bash +go run error_tui.go -i -w +``` + +OR + +Build the go file: + +```bash +go build -o +./ -i -w +``` You can now run: -`cansend #` + +```bash +cansend +``` diff --git a/scripts/can_errgo/components/table.go b/scripts/can_errgo/components/table.go index 024b75f8a..7b77bc795 100644 --- a/scripts/can_errgo/components/table.go +++ b/scripts/can_errgo/components/table.go @@ -1,10 +1,9 @@ package components // Modified from "github.com/charmbracelet/bubbles/table" +// Repurposed for use in CAN error handling application import ( - "strings" - "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/viewport" @@ -13,18 +12,25 @@ import ( "github.com/mattn/go-runewidth" ) +// MetaRow is the implementation of a Error Row with its corresponding error index within the 64-bit CAN frame +type MetaRow struct{ + Row Row + Index int +} + // Model defines a state for the table widget. type Model struct { KeyMap KeyMap Help help.Model cols []Column - rows []Row + metaRow []MetaRow cursor int focus bool styles Styles ShowCursor bool + viewport viewport.Model start int end int @@ -160,10 +166,10 @@ func WithColumns(cols []Column) Option { } } -// WithRows sets the table rows (data). -func WithRows(rows []Row) Option { +//WithRows sets the table rows (data). +func WithRows(rows []MetaRow) Option { return func(m *Model) { - m.rows = rows + m.metaRow = rows } } @@ -268,7 +274,7 @@ func (m Model) HelpView() string { // UpdateViewport updates the list content based on the previously defined // columns and rows. func (m *Model) UpdateViewport() { - renderedRows := make([]string, 0, len(m.rows)) + renderedRows := make([]string, 0, len(m.metaRow)) // Render only rows from: m.cursor-m.viewport.Height to: m.cursor+m.viewport.Height // Constant runtime, independent of number of rows in a table. @@ -278,7 +284,7 @@ func (m *Model) UpdateViewport() { } else { m.start = 0 } - m.end = clamp(m.cursor+m.viewport.Height, m.cursor, len(m.rows)) + m.end = clamp(m.cursor+m.viewport.Height, m.cursor, len(m.metaRow)) for i := m.start; i < m.end; i++ { renderedRows = append(renderedRows, m.renderRow(i)) } @@ -291,16 +297,24 @@ func (m *Model) UpdateViewport() { // SelectedRow returns the selected row. // You can cast it to your own implementation. func (m Model) SelectedRow() Row { - if m.cursor < 0 || m.cursor >= len(m.rows) { + if m.cursor < 0 || m.cursor >= len(m.metaRow) { return nil } - return m.rows[m.cursor] + return m.metaRow[m.cursor].Row +} + +// SelectedIndex returns the Error Index of the selected row. +func (m Model) SelectedIndex() int { + if m.cursor < 0 || m.cursor >= len(m.metaRow) { + return -1 + } + return m.metaRow[m.cursor].Index } -// Rows returns the current rows. -func (m Model) Rows() []Row { - return m.rows +// Rows returns the current metaRow. +func (m Model) Rows() []MetaRow { + return m.metaRow } // Columns returns the current columns. @@ -308,9 +322,9 @@ func (m Model) Columns() []Column { return m.cols } -// SetRows sets a new rows state. -func (m *Model) SetRows(r []Row) { - m.rows = r +// SetRows sets a new metaRow state. +func (m *Model) SetRows(r []MetaRow) { + m.metaRow = r m.UpdateViewport() } @@ -350,23 +364,23 @@ func (m Model) Cursor() int { // ForceCursor forces the cursor to a specific row, regardless if the row is out of bounds. func (m *Model) ForceCursor(n int) { // Clamp the cursor position within bounds - m.cursor = clamp(n, 0, len(m.rows)-1) + m.cursor = clamp(n, 0, len(m.metaRow)-1) m.UpdateViewport() } // SetCursor sets the cursor position in the table. func (m *Model) SetCursor(n int) { - m.cursor = clamp(n, 0, len(m.rows)-1) + m.cursor = clamp(n, 0, len(m.metaRow)-1) m.UpdateViewport() } // SetCursor sets the cursor position in the table. func (m *Model) SetCursorAndViewport(n int) { - m.cursor = clamp(n, 0, len(m.rows)-1) + m.cursor = clamp(n, 0, len(m.metaRow)-1) switch { case m.start == 0: m.viewport.SetYOffset(clamp(m.viewport.YOffset, 0, m.cursor)) - case m.end == len(m.rows) && m.cursor > m.viewport.Height: + case m.end == len(m.metaRow) && m.cursor > m.viewport.Height: m.viewport.SetYOffset(clamp(m.viewport.YOffset-n, 1, m.viewport.Height)) } @@ -378,10 +392,10 @@ func (m *Model) SetShowCursor(show bool) { m.UpdateViewport() } -// MoveUp moves the selection up by any number of rows. +// MoveUp moves the selection up by any number of metaRow. // It can not go above the first row. func (m *Model) MoveUp(n int) { - m.cursor = clamp(m.cursor-n, 0, len(m.rows)-1) + m.cursor = clamp(m.cursor-n, 0, len(m.metaRow)-1) switch { case m.start == 0: m.viewport.SetYOffset(clamp(m.viewport.YOffset, 0, m.cursor)) @@ -393,14 +407,14 @@ func (m *Model) MoveUp(n int) { m.UpdateViewport() } -// MoveDown moves the selection down by any number of rows. +// MoveDown moves the selection down by any number of metaRow. // It can not go below the last row. func (m *Model) MoveDown(n int) { - m.cursor = clamp(m.cursor+n, 0, len(m.rows)-1) + m.cursor = clamp(m.cursor+n, 0, len(m.metaRow)-1) m.UpdateViewport() switch { - case m.end == len(m.rows) && m.viewport.YOffset > 0: + case m.end == len(m.metaRow) && m.viewport.YOffset > 0: m.viewport.SetYOffset(clamp(m.viewport.YOffset-n, 1, m.viewport.Height)) case m.cursor > (m.end-m.start)/2 && m.viewport.YOffset > 0: m.viewport.SetYOffset(clamp(m.viewport.YOffset-n, 1, m.cursor)) @@ -417,23 +431,7 @@ func (m *Model) GotoTop() { // GotoBottom moves the selection to the last row. func (m *Model) GotoBottom() { - m.MoveDown(len(m.rows)) -} - -// FromValues create the table rows from a simple string. It uses `\n` by -// default for getting all the rows and the given separator for the fields on -// each row. -func (m *Model) FromValues(value, separator string) { - rows := []Row{} - for _, line := range strings.Split(value, "\n") { - r := Row{} - for _, field := range strings.Split(line, separator) { - r = append(r, field) - } - rows = append(rows, r) - } - - m.SetRows(rows) + m.MoveDown(len(m.metaRow)) } func (m Model) headersView() string { @@ -451,7 +449,7 @@ func (m Model) headersView() string { func (m *Model) renderRow(r int) string { s := make([]string, 0, len(m.cols)) - for i, value := range m.rows[r] { + for i, value := range m.metaRow[r].Row { if m.cols[i].Width <= 0 { continue } diff --git a/scripts/can_errgo/error_tui.go b/scripts/can_errgo/error_tui.go index ea8deaf8f..09ab00b4c 100644 --- a/scripts/can_errgo/error_tui.go +++ b/scripts/can_errgo/error_tui.go @@ -21,7 +21,7 @@ import ( ) -const canFrameSize = 16 // CAN frame size (for a standard CAN frame) +const canFrameSize = 16 // CAN frame size (for a standard CAN frame) // Converts a 8-byte slice to a uint64, this is for masking purposes. func bytesToUint64(b []byte) uint64 { @@ -38,16 +38,16 @@ var baseStyle = lipgloss.NewStyle(). BorderStyle(lipgloss.NormalBorder()). BorderForeground(lipgloss.Color("240")) -// RowValuesToRow converts a RowValues struct to a table.Row -func RowValuesToRow(r RowValues) table.Row{ - return table.Row{r.Error, strconv.Itoa(r.Count), strconv.Itoa(r.Recency)} +// RowValuesToRow converts a ErrorData struct to a table.Row +func RowValuesToRow(r ErrorData) table.MetaRow{ + return table.MetaRow{Row : table.Row{r.Error, strconv.Itoa(r.Count), strconv.Itoa(r.Recency)}, Index: r.ErrorIndex} } -// Convert a slice of RowValues to a slice of table.Rows -func toTableRows(r []RowValues) []table.Row { - var rows []table.Row +// Convert a slice of ErrorData to a slice of table.Rows +func toTableRows(r []ErrorData) []table.MetaRow { + var rows []table.MetaRow for _, v := range r { - // Check if the RowValues is "non-empty" and Active + // Check if the ErrorData is "non-empty" and Active if v.Error != "" && v.Count != 0 && !v.Ignored{ rows = append(rows, RowValuesToRow(v)) } @@ -55,79 +55,64 @@ func toTableRows(r []RowValues) []table.Row { return rows } -// SortRowValuesBy sorts a slice of RowValues by the specified column, this creates a deep copy of the input slice. -func SortRowValuesBy(r []RowValues, column int) []RowValues { +// SortRowValuesByRecency sorts a slice of ErrorData by the Recency column. +// This creates a deep copy of the input slice. +func SortRowValuesByRecency(r []ErrorData) []ErrorData { // Create a deep copy of the input slice - copied := make([]RowValues, len(r)) + copied := make([]ErrorData, len(r)) copy(copied, r) - // Sort the copied slice + // Sort the copied slice by Recency sort.SliceStable(copied, func(i, j int) bool { - switch column { - case 0: - return copied[i].Error < copied[j].Error - case 1: - return copied[i].Count < copied[j].Count - case 2: - return copied[i].Recency < copied[j].Recency - } - return false + return copied[i].Recency < copied[j].Recency }) return copied } -// getDescription returns the description of the error, if the error is not found it returns a default message. -func (m model) getDescription(errorName string) string { - for i := range(len(m.rowsValues)){ - if m.rowsValues[i].Error == errorName{ - return m.rowsValues[i].Description - } - } - return "No description available" -} -// findError returns the index of the error in the slice of RowValues, if the error is not found it returns -1. -func findError(errorName string, rowValues []RowValues) int { +// findError returns the relative index (Row index) of the error in the slice of ErrorData, if the error is not found it returns -1. +func findError(errorIndex int, rowValues []ErrorData) int { cnt := 0 - for i := range(len(rowValues)){ - if rowValues[i].Error == ""{ + for i ,row := range rowValues{ + if row.Error == "" || row.Ignored{ cnt += 1 } - if rowValues[i].Error == errorName{ - return i - cnt + if row.ErrorIndex == errorIndex{ + return i - cnt } } return -1 } -// resetRowValue resets the count and recency of the error in the slice of RowValues. +// resetRowValue resets the count and recency of the error in the slice of ErrorData. func (m model) resetRowValue(errorName string) { - for i := range(len(m.rowsValues)){ - if m.rowsValues[i].Error == errorName{ - m.rowsValues[i].Count = 0 - m.rowsValues[i].Recency = 0 + for _,row := range m.errorData{ + if row.Error == errorName{ + row.Count = 0 + row.Recency = 0 } } } // getIgnoredRows returns a slice of table.Rows that are ignored and have an error message. -func (m model) getIgnoredRows() []table.Row { - var rows []table.Row - for i := range(len(m.rowsValues)){ - if m.rowsValues[i].Ignored && m.rowsValues[i].Error != ""{ - rows = append(rows, table.Row{m.rowsValues[i].Error}) +func (m model) getIgnoredRows() []table.MetaRow { + var rows []table.MetaRow + for _,row := range m.errorData{ + if row.Ignored && row.Error != ""{ + rows = append(rows, table.MetaRow{Row: table.Row{row.Error}, Index: row.ErrorIndex}) } } return rows } -type RowValues struct{ +type ErrorData struct{ Error string Count int Recency int Description string Ignored bool + ErrorIndex int } @@ -150,7 +135,7 @@ type model struct { submenuKeys KeyMap // Slice of row information and ignore mask for incoming errors/bits - rowsValues []RowValues + errorData []ErrorData ignoreMask uint64 // Timeout flag and last CAN message time @@ -179,13 +164,12 @@ func tickEvery(t time.Duration) tea.Cmd { } // Increment the recency of all errors -func (m *model) refresh(){ - rows := m.rowsValues - for i := range(len(rows)){ - if m.rowsValues[i].Count !=0{ - m.rowsValues[i].Recency += 1 +func (m *model) refresh() { + for i := range m.errorData { // Use index to modify the original slice + if m.errorData[i].Count != 0 { + m.errorData[i].Recency += 1 } - } + } } func (m model) Init() tea.Cmd { return tickEvery(m.t) } @@ -210,9 +194,9 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } // Toggle the mask for the selected error and delete the row. - errorIndex := findError(m.submenuTable.SelectedRow()[0], m.rowsValues) + errorIndex := m.submenuTable.SelectedIndex() m.ignoreMask = toggleBit(m.ignoreMask, errorIndex) - m.rowsValues[errorIndex].Ignored = false + m.errorData[errorIndex].Ignored = false newRows := m.getIgnoredRows() m.submenuTable.SetRows(newRows) @@ -252,9 +236,9 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } // Remove the row and reset its count. - errorIndex := findError(m.table.SelectedRow()[0], m.rowsValues) - m.rowsValues[errorIndex].Count = 0 - newRows := toTableRows(m.rowsValues) + errorIndex := m.table.SelectedIndex() + m.errorData[errorIndex].Count = 0 + newRows := toTableRows(m.errorData) m.table.SetRows(newRows) @@ -274,13 +258,13 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } // Toggle the mask for the selected error and move it to the ignore menu. - errorIndex := findError(m.table.SelectedRow()[0], m.rowsValues) + errorIndex := m.table.SelectedIndex() m.ignoreMask = toggleBit(m.ignoreMask, errorIndex) - m.rowsValues[errorIndex].Ignored = true + m.errorData[errorIndex].Ignored = true // Remove the row from the main table and reset its count. - m.rowsValues[errorIndex].Count = 0 - newRows := toTableRows(m.rowsValues) + m.errorData[errorIndex].Count = 0 + newRows := toTableRows(m.errorData) m.table.SetRows(newRows) //Set add the ignored rows to submenu @@ -320,25 +304,24 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { for k := 0; k < 64; k++ { // Iterate through all 64 bits if msg.Value&(1< - - -candump -L > & -cansend - -ip link show -sudo ip link delete From cf9da83d9087564ba0bad139301323cf70dcfde0 Mon Sep 17 00:00:00 2001 From: Samuel Shi Date: Tue, 19 Nov 2024 02:55:38 -0500 Subject: [PATCH 15/22] remove redundant function --- scripts/can_errgo/error_tui.go | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/scripts/can_errgo/error_tui.go b/scripts/can_errgo/error_tui.go index 09ab00b4c..0b8a180c2 100644 --- a/scripts/can_errgo/error_tui.go +++ b/scripts/can_errgo/error_tui.go @@ -85,16 +85,6 @@ func findError(errorIndex int, rowValues []ErrorData) int { return -1 } -// resetRowValue resets the count and recency of the error in the slice of ErrorData. -func (m model) resetRowValue(errorName string) { - for _,row := range m.errorData{ - if row.Error == errorName{ - row.Count = 0 - row.Recency = 0 - } - } -} - // getIgnoredRows returns a slice of table.Rows that are ignored and have an error message. func (m model) getIgnoredRows() []table.MetaRow { var rows []table.MetaRow From 7dcc3a3c2bc53a47b03f7ab26248a8e065c417c3 Mon Sep 17 00:00:00 2001 From: Samuel Shi Date: Tue, 19 Nov 2024 03:25:58 -0500 Subject: [PATCH 16/22] Small fix in readme and further implemented isolated logic --- .../FrontController/vehicle_control_system | 1 + .../vehicle_control_system | 1 + scripts/can_errgo/README.md | 2 +- scripts/can_errgo/error_tui.go | 117 ++++++++++++------ 4 files changed, 80 insertions(+), 41 deletions(-) create mode 160000 firmware/projects/EV5/FrontController/vehicle_control_system create mode 160000 firmware/projects/debug/FrontControllerSimple/vehicle_control_system diff --git a/firmware/projects/EV5/FrontController/vehicle_control_system b/firmware/projects/EV5/FrontController/vehicle_control_system new file mode 160000 index 000000000..4fddb9369 --- /dev/null +++ b/firmware/projects/EV5/FrontController/vehicle_control_system @@ -0,0 +1 @@ +Subproject commit 4fddb9369730a8cee4e74f83d71d3b1e33b8d5ea diff --git a/firmware/projects/debug/FrontControllerSimple/vehicle_control_system b/firmware/projects/debug/FrontControllerSimple/vehicle_control_system new file mode 160000 index 000000000..a04e3a93f --- /dev/null +++ b/firmware/projects/debug/FrontControllerSimple/vehicle_control_system @@ -0,0 +1 @@ +Subproject commit a04e3a93f0def3d748ca7c636cb0f226328c0a10 diff --git a/scripts/can_errgo/README.md b/scripts/can_errgo/README.md index 496301231..9a9ee40b3 100644 --- a/scripts/can_errgo/README.md +++ b/scripts/can_errgo/README.md @@ -4,7 +4,7 @@ This is a Tangible User Interface program designed to track CAN Errors including Users can: -- Acknowledge the error ( Hide the error until it is sent again) +- Acknowledge the error ( Hide and resets the error until it is sent again) - Ignore the error ( Hides, resets, and stops reading all future occurrences of the error) - View ignored errors and choose to unignore those errors diff --git a/scripts/can_errgo/error_tui.go b/scripts/can_errgo/error_tui.go index 0b8a180c2..36df8151e 100644 --- a/scripts/can_errgo/error_tui.go +++ b/scripts/can_errgo/error_tui.go @@ -48,7 +48,7 @@ func toTableRows(r []ErrorData) []table.MetaRow { var rows []table.MetaRow for _, v := range r { // Check if the ErrorData is "non-empty" and Active - if v.Error != "" && v.Count != 0 && !v.Ignored{ + if v.Count != 0 && !v.Ignored{ rows = append(rows, RowValuesToRow(v)) } } @@ -75,7 +75,7 @@ func SortRowValuesByRecency(r []ErrorData) []ErrorData { func findError(errorIndex int, rowValues []ErrorData) int { cnt := 0 for i ,row := range rowValues{ - if row.Error == "" || row.Ignored{ + if row.Count == 0 || row.Ignored{ cnt += 1 } if row.ErrorIndex == errorIndex{ @@ -102,6 +102,7 @@ type ErrorData struct{ Recency int Description string Ignored bool + // This is used for keeping track of the error bit after sorting ErrorIndex int } @@ -153,15 +154,6 @@ func tickEvery(t time.Duration) tea.Cmd { } } -// Increment the recency of all errors -func (m *model) refresh() { - for i := range m.errorData { // Use index to modify the original slice - if m.errorData[i].Count != 0 { - m.errorData[i].Recency += 1 - } - } -} - func (m model) Init() tea.Cmd { return tickEvery(m.t) } // Table and ignore subtable update loop. @@ -190,13 +182,6 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { newRows := m.getIgnoredRows() m.submenuTable.SetRows(newRows) - // May be unnecessary, but just in case we update the cursor. - if m.submenuTable.Cursor() == 0{ - m.submenuTable.MoveDown(1) - }else{ - m.submenuTable.MoveUp(1) - } - case key.Matches(msg, m.submenuKeys.s): m.submenuActive = false // Hide submenu m.table.Focus() // Refocus the main table @@ -230,14 +215,6 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.errorData[errorIndex].Count = 0 newRows := toTableRows(m.errorData) m.table.SetRows(newRows) - - - // May be unnecessary, but just in case we update the cursor. - if m.table.Cursor() == 0{ - m.table.MoveDown(1) - }else{ - m.table.MoveUp(1) - } return m, nil case key.Matches(msg, m.tableKeys.i): @@ -260,8 +237,6 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { //Set add the ignored rows to submenu ignoredRows := m.getIgnoredRows() m.submenuTable.SetRows(ignoredRows) - // May be unnecessary, but just in case we update the cursor. - m.table.MoveDown(1) return m, nil case key.Matches(msg, m.tableKeys.s): @@ -289,17 +264,11 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.isTimeout = false // Perform bitwise AND to filter out the bits that are not of interest msg.Value = msg.Value & m.ignoreMask - // Update the freshness of the errors - m.refresh() - for k := 0; k < 64; k++ { // Iterate through all 64 bits - if msg.Value&(1< Date: Tue, 19 Nov 2024 03:55:24 -0500 Subject: [PATCH 17/22] other small fixes --- scripts/can_errgo/error_tui.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/can_errgo/error_tui.go b/scripts/can_errgo/error_tui.go index 36df8151e..a779c9dfe 100644 --- a/scripts/can_errgo/error_tui.go +++ b/scripts/can_errgo/error_tui.go @@ -89,7 +89,7 @@ func findError(errorIndex int, rowValues []ErrorData) int { func (m model) getIgnoredRows() []table.MetaRow { var rows []table.MetaRow for _,row := range m.errorData{ - if row.Ignored && row.Error != ""{ + if row.Ignored{ rows = append(rows, table.MetaRow{Row: table.Row{row.Error}, Index: row.ErrorIndex}) } } From dc38cfb61638e1f30f06a4112b488fa3ecfa89c6 Mon Sep 17 00:00:00 2001 From: Samuel Shi Date: Tue, 19 Nov 2024 16:53:36 -0500 Subject: [PATCH 18/22] Removed redundant comments, changed some field/function names to be more descriptive, updated README --- .../FrontController/vehicle_control_system | 1 - .../vehicle_control_system | 1 - scripts/can_errgo/README.md | 16 +-- scripts/can_errgo/components/table.go | 46 +++--- scripts/can_errgo/error_tui.go | 133 ++++-------------- 5 files changed, 59 insertions(+), 138 deletions(-) delete mode 160000 firmware/projects/EV5/FrontController/vehicle_control_system delete mode 160000 firmware/projects/debug/FrontControllerSimple/vehicle_control_system diff --git a/firmware/projects/EV5/FrontController/vehicle_control_system b/firmware/projects/EV5/FrontController/vehicle_control_system deleted file mode 160000 index 4fddb9369..000000000 --- a/firmware/projects/EV5/FrontController/vehicle_control_system +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 4fddb9369730a8cee4e74f83d71d3b1e33b8d5ea diff --git a/firmware/projects/debug/FrontControllerSimple/vehicle_control_system b/firmware/projects/debug/FrontControllerSimple/vehicle_control_system deleted file mode 160000 index a04e3a93f..000000000 --- a/firmware/projects/debug/FrontControllerSimple/vehicle_control_system +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a04e3a93f0def3d748ca7c636cb0f226328c0a10 diff --git a/scripts/can_errgo/README.md b/scripts/can_errgo/README.md index 9a9ee40b3..8373077bd 100644 --- a/scripts/can_errgo/README.md +++ b/scripts/can_errgo/README.md @@ -4,8 +4,8 @@ This is a Tangible User Interface program designed to track CAN Errors including Users can: -- Acknowledge the error ( Hide and resets the error until it is sent again) -- Ignore the error ( Hides, resets, and stops reading all future occurrences of the error) +- Acknowledge the error (Hide and resets the error until it is sent again) +- Ignore the error (Hides, resets, and stops reading all future occurrences of the error) - View ignored errors and choose to unignore those errors ## Setup @@ -13,16 +13,16 @@ Users can: Install required dependencies: ```bash -sudo apt-get update -sudo apt-get upgrade -sudo apt install net-tools iproute2 can-utils linux-modules-extra-$(uname -r) +sudo apt-get update +sudo apt-get upgrade +sudo apt install net-tools iproute2 can-utils linux-modules-extra-$(uname -r) ``` Kill all existing instances of ip-links: ```bash -ip link show -sudo ip link delete +ip link show +sudo ip link delete ``` Setup the can network: @@ -36,7 +36,7 @@ Setup the can network: Start the CLI viewer for single use: ```bash -go run error_tui.go -i -w +go run error_tui.go -i -w ``` OR diff --git a/scripts/can_errgo/components/table.go b/scripts/can_errgo/components/table.go index 7b77bc795..ef5defb34 100644 --- a/scripts/can_errgo/components/table.go +++ b/scripts/can_errgo/components/table.go @@ -24,7 +24,7 @@ type Model struct { Help help.Model cols []Column - metaRow []MetaRow + metaRows []MetaRow cursor int focus bool styles Styles @@ -169,7 +169,7 @@ func WithColumns(cols []Column) Option { //WithRows sets the table rows (data). func WithRows(rows []MetaRow) Option { return func(m *Model) { - m.metaRow = rows + m.metaRows = rows } } @@ -274,7 +274,7 @@ func (m Model) HelpView() string { // UpdateViewport updates the list content based on the previously defined // columns and rows. func (m *Model) UpdateViewport() { - renderedRows := make([]string, 0, len(m.metaRow)) + renderedRows := make([]string, 0, len(m.metaRows)) // Render only rows from: m.cursor-m.viewport.Height to: m.cursor+m.viewport.Height // Constant runtime, independent of number of rows in a table. @@ -284,7 +284,7 @@ func (m *Model) UpdateViewport() { } else { m.start = 0 } - m.end = clamp(m.cursor+m.viewport.Height, m.cursor, len(m.metaRow)) + m.end = clamp(m.cursor+m.viewport.Height, m.cursor, len(m.metaRows)) for i := m.start; i < m.end; i++ { renderedRows = append(renderedRows, m.renderRow(i)) } @@ -297,24 +297,24 @@ func (m *Model) UpdateViewport() { // SelectedRow returns the selected row. // You can cast it to your own implementation. func (m Model) SelectedRow() Row { - if m.cursor < 0 || m.cursor >= len(m.metaRow) { + if m.cursor < 0 || m.cursor >= len(m.metaRows) { return nil } - return m.metaRow[m.cursor].Row + return m.metaRows[m.cursor].Row } // SelectedIndex returns the Error Index of the selected row. func (m Model) SelectedIndex() int { - if m.cursor < 0 || m.cursor >= len(m.metaRow) { + if m.cursor < 0 || m.cursor >= len(m.metaRows) { return -1 } - return m.metaRow[m.cursor].Index + return m.metaRows[m.cursor].Index } -// Rows returns the current metaRow. +// Rows returns the current metaRows. func (m Model) Rows() []MetaRow { - return m.metaRow + return m.metaRows } // Columns returns the current columns. @@ -322,9 +322,9 @@ func (m Model) Columns() []Column { return m.cols } -// SetRows sets a new metaRow state. +// SetRows sets a new metaRows state. func (m *Model) SetRows(r []MetaRow) { - m.metaRow = r + m.metaRows = r m.UpdateViewport() } @@ -364,23 +364,23 @@ func (m Model) Cursor() int { // ForceCursor forces the cursor to a specific row, regardless if the row is out of bounds. func (m *Model) ForceCursor(n int) { // Clamp the cursor position within bounds - m.cursor = clamp(n, 0, len(m.metaRow)-1) + m.cursor = clamp(n, 0, len(m.metaRows)-1) m.UpdateViewport() } // SetCursor sets the cursor position in the table. func (m *Model) SetCursor(n int) { - m.cursor = clamp(n, 0, len(m.metaRow)-1) + m.cursor = clamp(n, 0, len(m.metaRows)-1) m.UpdateViewport() } // SetCursor sets the cursor position in the table. func (m *Model) SetCursorAndViewport(n int) { - m.cursor = clamp(n, 0, len(m.metaRow)-1) + m.cursor = clamp(n, 0, len(m.metaRows)-1) switch { case m.start == 0: m.viewport.SetYOffset(clamp(m.viewport.YOffset, 0, m.cursor)) - case m.end == len(m.metaRow) && m.cursor > m.viewport.Height: + case m.end == len(m.metaRows) && m.cursor > m.viewport.Height: m.viewport.SetYOffset(clamp(m.viewport.YOffset-n, 1, m.viewport.Height)) } @@ -392,10 +392,10 @@ func (m *Model) SetShowCursor(show bool) { m.UpdateViewport() } -// MoveUp moves the selection up by any number of metaRow. +// MoveUp moves the selection up by any number of metaRows. // It can not go above the first row. func (m *Model) MoveUp(n int) { - m.cursor = clamp(m.cursor-n, 0, len(m.metaRow)-1) + m.cursor = clamp(m.cursor-n, 0, len(m.metaRows)-1) switch { case m.start == 0: m.viewport.SetYOffset(clamp(m.viewport.YOffset, 0, m.cursor)) @@ -407,14 +407,14 @@ func (m *Model) MoveUp(n int) { m.UpdateViewport() } -// MoveDown moves the selection down by any number of metaRow. +// MoveDown moves the selection down by any number of metaRows. // It can not go below the last row. func (m *Model) MoveDown(n int) { - m.cursor = clamp(m.cursor+n, 0, len(m.metaRow)-1) + m.cursor = clamp(m.cursor+n, 0, len(m.metaRows)-1) m.UpdateViewport() switch { - case m.end == len(m.metaRow) && m.viewport.YOffset > 0: + case m.end == len(m.metaRows) && m.viewport.YOffset > 0: m.viewport.SetYOffset(clamp(m.viewport.YOffset-n, 1, m.viewport.Height)) case m.cursor > (m.end-m.start)/2 && m.viewport.YOffset > 0: m.viewport.SetYOffset(clamp(m.viewport.YOffset-n, 1, m.cursor)) @@ -431,7 +431,7 @@ func (m *Model) GotoTop() { // GotoBottom moves the selection to the last row. func (m *Model) GotoBottom() { - m.MoveDown(len(m.metaRow)) + m.MoveDown(len(m.metaRows)) } func (m Model) headersView() string { @@ -449,7 +449,7 @@ func (m Model) headersView() string { func (m *Model) renderRow(r int) string { s := make([]string, 0, len(m.cols)) - for i, value := range m.metaRow[r].Row { + for i, value := range m.metaRows[r].Row { if m.cols[i].Width <= 0 { continue } diff --git a/scripts/can_errgo/error_tui.go b/scripts/can_errgo/error_tui.go index a779c9dfe..dfc20e0e5 100644 --- a/scripts/can_errgo/error_tui.go +++ b/scripts/can_errgo/error_tui.go @@ -43,11 +43,10 @@ func RowValuesToRow(r ErrorData) table.MetaRow{ return table.MetaRow{Row : table.Row{r.Error, strconv.Itoa(r.Count), strconv.Itoa(r.Recency)}, Index: r.ErrorIndex} } -// Convert a slice of ErrorData to a slice of table.Rows func toTableRows(r []ErrorData) []table.MetaRow { var rows []table.MetaRow for _, v := range r { - // Check if the ErrorData is "non-empty" and Active + // Check if the ErrorData is active if v.Count != 0 && !v.Ignored{ rows = append(rows, RowValuesToRow(v)) } @@ -55,14 +54,11 @@ func toTableRows(r []ErrorData) []table.MetaRow { return rows } -// SortRowValuesByRecency sorts a slice of ErrorData by the Recency column. // This creates a deep copy of the input slice. func SortRowValuesByRecency(r []ErrorData) []ErrorData { - // Create a deep copy of the input slice copied := make([]ErrorData, len(r)) copy(copied, r) - // Sort the copied slice by Recency sort.SliceStable(copied, func(i, j int) bool { return copied[i].Recency < copied[j].Recency }) @@ -85,7 +81,6 @@ func findError(errorIndex int, rowValues []ErrorData) int { return -1 } -// getIgnoredRows returns a slice of table.Rows that are ignored and have an error message. func (m model) getIgnoredRows() []table.MetaRow { var rows []table.MetaRow for _,row := range m.errorData{ @@ -106,6 +101,9 @@ type ErrorData struct{ ErrorIndex int } +func (m model) isTimeout() bool { + return time.Now().Sub(m.lastCANTime) > m.timeout +} type CANMsg struct { ID uint32 @@ -130,33 +128,28 @@ type model struct { ignoreMask uint64 // Timeout flag and last CAN message time - isTimeout bool lastCANTime time.Time - t time.Duration + timeout time.Duration // Box for displaying error descriptions box box.Box showBox bool } - -// toggleBit toggles the specified bit in the number -//If the bit is 0, it sets it to 1; if it is 1, it sets it to 0. func toggleBit(n uint64, bitPosition int) uint64 { mask := uint64(1) << bitPosition return n ^ mask } -func tickEvery(t time.Duration) tea.Cmd { +func tickEvery(timeout time.Duration) tea.Cmd { return func() tea.Msg { - time.Sleep(t) + time.Sleep(timeout) return TickMsg{} } } -func (m model) Init() tea.Cmd { return tickEvery(m.t) } +func (m model) Init() tea.Cmd { return tickEvery(m.timeout) } -// Table and ignore subtable update loop. func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd switch msg := msg.(type) { @@ -183,10 +176,10 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.submenuTable.SetRows(newRows) case key.Matches(msg, m.submenuKeys.s): - m.submenuActive = false // Hide submenu - m.table.Focus() // Refocus the main table - m.submenuTable.Blur() // Unfocus the submenu - m.table.SetShowCursor(true) // Reshow the cursor on the main table + m.submenuActive = false + m.table.Focus() + m.submenuTable.Blur() + m.table.SetShowCursor(true) return m, nil @@ -259,11 +252,9 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m,nil } case CANMsg: - // Reset the timeout flag and update the last CAN message time m.lastCANTime = time.Now() - m.isTimeout = false - // Perform bitwise AND to filter out the bits that are not of interest msg.Value = msg.Value & m.ignoreMask + for i,errorVal := range m.errorData{ m.errorData[i].Recency += 1 if msg.Value&(1< m.t { - m.isTimeout = true - } - return m, tickEvery(m.t) + return m, tickEvery(m.timeout) } - - // Update the table with the new message - if _, ok := msg.(CANMsg); ok { - // Update the mainTable regardless of submenu state - m.table, cmd = m.table.Update(msg) - }else { - m.submenuTable, cmd = m.submenuTable.Update(msg) - } return m, cmd } @@ -317,7 +296,7 @@ func (km KeyMap) ShortHelp() []key.Binding { // key bindings for the main table -func additionalKeyMap() KeyMap { +func tableKeyMap() KeyMap { return KeyMap{ a: key.NewBinding( key.WithKeys("a"), @@ -370,7 +349,7 @@ func (m model) View() string { } // Timeout warning - if m.isTimeout{ + if m.lastCANTime.Add(m.timeout).Before(time.Now()){ out = fmt.Sprint("\n\x1b[41m\x1b[37mWarning! Last recorded message was over ",math.Round(time.Since(m.lastCANTime).Seconds()), " seconds ago!\x1b[0m") + "\n" + out } @@ -401,70 +380,16 @@ func initViewer(warning int) model{ // This can be determined at runtime later (DBC file parsing), for now it is hardcoded. customErrors := []ErrorData{ - {Error: "error0", Count: 0, Recency: 0, Description: "Description for error 0", Ignored: false, ErrorIndex: 0}, - {Error: "error1", Count: 0, Recency: 0, Description: "Description for error 1", Ignored: false, ErrorIndex: 1}, - {Error: "error2", Count: 0, Recency: 0, Description: "Description for error 2", Ignored: false, ErrorIndex: 2}, - {Error: "error3", Count: 0, Recency: 0, Description: "Description for error 3", Ignored: false, ErrorIndex: 3}, - {Error: "error4", Count: 0, Recency: 0, Description: "Description for error 4", Ignored: false, ErrorIndex: 4}, - {Error: "error5", Count: 0, Recency: 0, Description: "Description for error 5", Ignored: false, ErrorIndex: 5}, - {Error: "error6", Count: 0, Recency: 0, Description: "Description for error 6", Ignored: false, ErrorIndex: 6}, - {Error: "error7", Count: 0, Recency: 0, Description: "Description for error 7", Ignored: false, ErrorIndex: 7}, - {Error: "error8", Count: 0, Recency: 0, Description: "Description for error 8", Ignored: false, ErrorIndex: 8}, - {Error: "error9", Count: 0, Recency: 0, Description: "Description for error 9", Ignored: false, ErrorIndex: 9}, - {Error: "error10", Count: 0, Recency: 0, Description: "Description for error 10", Ignored: false, ErrorIndex: 10}, - {Error: "error11", Count: 0, Recency: 0, Description: "Description for error 11", Ignored: false, ErrorIndex: 11}, - {Error: "error12", Count: 0, Recency: 0, Description: "Description for error 12", Ignored: false, ErrorIndex: 12}, - {Error: "error13", Count: 0, Recency: 0, Description: "Description for error 13", Ignored: false, ErrorIndex: 13}, - {Error: "error14", Count: 0, Recency: 0, Description: "Description for error 14", Ignored: false, ErrorIndex: 14}, - {Error: "error15", Count: 0, Recency: 0, Description: "Description for error 15", Ignored: false, ErrorIndex: 15}, - {Error: "error16", Count: 0, Recency: 0, Description: "Description for error 16", Ignored: false, ErrorIndex: 16}, - {Error: "error17", Count: 0, Recency: 0, Description: "Description for error 17", Ignored: false, ErrorIndex: 17}, - {Error: "error18", Count: 0, Recency: 0, Description: "Description for error 18", Ignored: false, ErrorIndex: 18}, - {Error: "error19", Count: 0, Recency: 0, Description: "Description for error 19", Ignored: false, ErrorIndex: 19}, - {Error: "error20", Count: 0, Recency: 0, Description: "Description for error 20", Ignored: false, ErrorIndex: 20}, - {Error: "error21", Count: 0, Recency: 0, Description: "Description for error 21", Ignored: false, ErrorIndex: 21}, - {Error: "error22", Count: 0, Recency: 0, Description: "Description for error 22", Ignored: false, ErrorIndex: 22}, - {Error: "error23", Count: 0, Recency: 0, Description: "Description for error 23", Ignored: false, ErrorIndex: 23}, - {Error: "error24", Count: 0, Recency: 0, Description: "Description for error 24", Ignored: false, ErrorIndex: 24}, - {Error: "error25", Count: 0, Recency: 0, Description: "Description for error 25", Ignored: false, ErrorIndex: 25}, - {Error: "error26", Count: 0, Recency: 0, Description: "Description for error 26", Ignored: false, ErrorIndex: 26}, - {Error: "error27", Count: 0, Recency: 0, Description: "Description for error 27", Ignored: false, ErrorIndex: 27}, - {Error: "error28", Count: 0, Recency: 0, Description: "Description for error 28", Ignored: false, ErrorIndex: 28}, - {Error: "error29", Count: 0, Recency: 0, Description: "Description for error 29", Ignored: false, ErrorIndex: 29}, - {Error: "error30", Count: 0, Recency: 0, Description: "Description for error 30", Ignored: false, ErrorIndex: 30}, - {Error: "error31", Count: 0, Recency: 0, Description: "Description for error 31", Ignored: false, ErrorIndex: 31}, - {Error: "error32", Count: 0, Recency: 0, Description: "Description for error 32", Ignored: false, ErrorIndex: 32}, - {Error: "error33", Count: 0, Recency: 0, Description: "Description for error 33", Ignored: false, ErrorIndex: 33}, - {Error: "error34", Count: 0, Recency: 0, Description: "Description for error 34", Ignored: false, ErrorIndex: 34}, - {Error: "error35", Count: 0, Recency: 0, Description: "Description for error 35", Ignored: false, ErrorIndex: 35}, - {Error: "error36", Count: 0, Recency: 0, Description: "Description for error 36", Ignored: false, ErrorIndex: 36}, - {Error: "error37", Count: 0, Recency: 0, Description: "Description for error 37", Ignored: false, ErrorIndex: 37}, - {Error: "error38", Count: 0, Recency: 0, Description: "Description for error 38", Ignored: false, ErrorIndex: 38}, - {Error: "error39", Count: 0, Recency: 0, Description: "Description for error 39", Ignored: false, ErrorIndex: 39}, - {Error: "error40", Count: 0, Recency: 0, Description: "Description for error 40", Ignored: false, ErrorIndex: 40}, - {Error: "error41", Count: 0, Recency: 0, Description: "Description for error 41", Ignored: false, ErrorIndex: 41}, - {Error: "error42", Count: 0, Recency: 0, Description: "Description for error 42", Ignored: false, ErrorIndex: 42}, - {Error: "error43", Count: 0, Recency: 0, Description: "Description for error 43", Ignored: false, ErrorIndex: 43}, - {Error: "error44", Count: 0, Recency: 0, Description: "Description for error 44", Ignored: false, ErrorIndex: 44}, - {Error: "error45", Count: 0, Recency: 0, Description: "Description for error 45", Ignored: false, ErrorIndex: 45}, - {Error: "error46", Count: 0, Recency: 0, Description: "Description for error 46", Ignored: false, ErrorIndex: 46}, - {Error: "error47", Count: 0, Recency: 0, Description: "Description for error 47", Ignored: false, ErrorIndex: 47}, - {Error: "error48", Count: 0, Recency: 0, Description: "Description for error 48", Ignored: false, ErrorIndex: 48}, - {Error: "error49", Count: 0, Recency: 0, Description: "Description for error 49", Ignored: false, ErrorIndex: 49}, - {Error: "error50", Count: 0, Recency: 0, Description: "Description for error 50", Ignored: false, ErrorIndex: 50}, - {Error: "error51", Count: 0, Recency: 0, Description: "Description for error 51", Ignored: false, ErrorIndex: 51}, - {Error: "error52", Count: 0, Recency: 0, Description: "Description for error 52", Ignored: false, ErrorIndex: 52}, - {Error: "error53", Count: 0, Recency: 0, Description: "Description for error 53", Ignored: false, ErrorIndex: 53}, - {Error: "error54", Count: 0, Recency: 0, Description: "Description for error 54", Ignored: false, ErrorIndex: 54}, - {Error: "error55", Count: 0, Recency: 0, Description: "Description for error 55", Ignored: false, ErrorIndex: 55}, - {Error: "error56", Count: 0, Recency: 0, Description: "Description for error 56", Ignored: false, ErrorIndex: 56}, - {Error: "error57", Count: 0, Recency: 0, Description: "Description for error 57", Ignored: false, ErrorIndex: 57}, - {Error: "error58", Count: 0, Recency: 0, Description: "Description for error 58", Ignored: false, ErrorIndex: 58}, - {Error: "error59", Count: 0, Recency: 0, Description: "Description for error 59", Ignored: false, ErrorIndex: 59}, - {Error: "error60", Count: 0, Recency: 0, Description: "Description for error 60", Ignored: false, ErrorIndex: 60}, - {Error: "error61", Count: 0, Recency: 0, Description: "Description for error 61", Ignored: false, ErrorIndex: 61}, - {Error: "error62", Count: 0, Recency: 0, Description: "Description for error 62", Ignored: false, ErrorIndex: 62}, - {Error: "error63", Count: 0, Recency: 0, Description: "Description for error 63", Ignored: false, ErrorIndex: 63}, + } + for i := 0; i < 64; i++ { + customErrors = append(customErrors, ErrorData{ + Error: fmt.Sprintf("error%d", i), + Count: 0, + Recency: 0, + Description: fmt.Sprintf("Description for error %d", i), + Ignored: false, + ErrorIndex: i, + }) } box := box.Box{}; @@ -473,7 +398,7 @@ func initViewer(warning int) model{ m := model{ mainTable, - additionalKeyMap(), + tableKeyMap(), table2, false, @@ -482,7 +407,6 @@ func initViewer(warning int) model{ customErrors, ^uint64(0), - false, time.Now(), time.Duration(warning) * time.Second, @@ -570,7 +494,7 @@ func can_listener(p *tea.Program, canInterface string) { } defer unix.Close(sock) - // Get the index of the interface (vcan0) + // Get the index of the interface ifi, err := net.InterfaceByName(canInterface) if err != nil { log.Fatalf("Error getting interface index: %v", err) @@ -628,6 +552,5 @@ func main() { // Start the CAN listener go can_listener(p, *canInterfaceFlag) - <-quit } From 8ad6e9f7fd604b2359bb7f5cce2f43c60aadf0b9 Mon Sep 17 00:00:00 2001 From: Samuel Shi Date: Tue, 19 Nov 2024 16:54:49 -0500 Subject: [PATCH 19/22] remove submodule --- firmware/projects/FrontController/vehicle_control_system | 1 - 1 file changed, 1 deletion(-) delete mode 160000 firmware/projects/FrontController/vehicle_control_system diff --git a/firmware/projects/FrontController/vehicle_control_system b/firmware/projects/FrontController/vehicle_control_system deleted file mode 160000 index 4fddb9369..000000000 --- a/firmware/projects/FrontController/vehicle_control_system +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 4fddb9369730a8cee4e74f83d71d3b1e33b8d5ea From af1760857a08b97457ba96da9ed06461e9137e66 Mon Sep 17 00:00:00 2001 From: BlakeFreer Date: Tue, 19 Nov 2024 20:33:39 -0500 Subject: [PATCH 20/22] Undo submodule change --- firmware/projects/EV5/FrontController/vehicle_control_system | 1 + 1 file changed, 1 insertion(+) create mode 160000 firmware/projects/EV5/FrontController/vehicle_control_system diff --git a/firmware/projects/EV5/FrontController/vehicle_control_system b/firmware/projects/EV5/FrontController/vehicle_control_system new file mode 160000 index 000000000..4fddb9369 --- /dev/null +++ b/firmware/projects/EV5/FrontController/vehicle_control_system @@ -0,0 +1 @@ +Subproject commit 4fddb9369730a8cee4e74f83d71d3b1e33b8d5ea From 3cba99290d209ff8b7a02beb58318d881d6a72b1 Mon Sep 17 00:00:00 2001 From: Samuel Shi Date: Tue, 19 Nov 2024 22:04:28 -0500 Subject: [PATCH 21/22] Program now exits gracefully when an error occurs, setup_vcan now requires an can_port to work --- .../FrontController/vehicle_control_system | 1 + .../vehicle_control_system | 1 + scripts/can_errgo/error_tui.go | 142 ++++++++++-------- scripts/can_errgo/setup_vcan.sh | 8 +- 4 files changed, 90 insertions(+), 62 deletions(-) create mode 160000 firmware/projects/FrontController/vehicle_control_system create mode 160000 firmware/projects/debug/FrontControllerSimple/vehicle_control_system diff --git a/firmware/projects/FrontController/vehicle_control_system b/firmware/projects/FrontController/vehicle_control_system new file mode 160000 index 000000000..4fddb9369 --- /dev/null +++ b/firmware/projects/FrontController/vehicle_control_system @@ -0,0 +1 @@ +Subproject commit 4fddb9369730a8cee4e74f83d71d3b1e33b8d5ea diff --git a/firmware/projects/debug/FrontControllerSimple/vehicle_control_system b/firmware/projects/debug/FrontControllerSimple/vehicle_control_system new file mode 160000 index 000000000..a04e3a93f --- /dev/null +++ b/firmware/projects/debug/FrontControllerSimple/vehicle_control_system @@ -0,0 +1 @@ +Subproject commit a04e3a93f0def3d748ca7c636cb0f226328c0a10 diff --git a/scripts/can_errgo/error_tui.go b/scripts/can_errgo/error_tui.go index dfc20e0e5..caa8a8bad 100644 --- a/scripts/can_errgo/error_tui.go +++ b/scripts/can_errgo/error_tui.go @@ -1,12 +1,14 @@ package main import ( + "context" "encoding/binary" "flag" "fmt" "log" "math" "net" + "os" "sort" "strconv" "time" @@ -482,75 +484,93 @@ func createSubMenu() table.Model { return t } +func gracefulExit(p *tea.Program, cancel context.CancelFunc, quit chan struct{}, errMsg string, err error) { + if cancel != nil { + cancel() + } -func can_listener(p *tea.Program, canInterface string) { - // Create a raw CAN socket using the unix package - var sock int; - var err error; - - sock, err = unix.Socket(unix.AF_CAN, unix.SOCK_RAW, unix.CAN_RAW) - if err != nil { - log.Fatalf("Error creating CAN socket: %v", err) - } - defer unix.Close(sock) + if p != nil { + p.Quit() + } - // Get the index of the interface - ifi, err := net.InterfaceByName(canInterface) - if err != nil { - log.Fatalf("Error getting interface index: %v", err) - } + // Wait for the main loop to handle the quit signal + <-quit - // Bind the socket to the CAN interface - addr := &unix.SockaddrCAN{Ifindex: ifi.Index} - if err := unix.Bind(sock, addr); err != nil { - log.Fatalf("Error binding CAN socket: %v", err) - } + if err != nil { + log.Printf("%s: %v", errMsg, err) + } else { + log.Println(errMsg) + } + os.Exit(1) +} - // Create a buffer to hold incoming CAN frames - buf := make([]byte, canFrameSize) +func can_listener(ctx context.Context, p *tea.Program, canInterface string, errChan chan error) { + sock, err := unix.Socket(unix.AF_CAN, unix.SOCK_RAW, unix.CAN_RAW) + if err != nil { + errChan <- fmt.Errorf("Error creating CAN socket: %w", err) + return + } + defer unix.Close(sock) - fmt.Println("Listening for CAN messages on", canInterface) + ifi, err := net.InterfaceByName(canInterface) + if err != nil { + errChan <- fmt.Errorf("Error getting interface index: %w", err) + return + } - for { - // Receive CAN message - _, err := unix.Read(sock, buf) - if err != nil { - log.Printf("Error receiving CAN message: %v", err) - continue - } + addr := &unix.SockaddrCAN{Ifindex: ifi.Index} + if err := unix.Bind(sock, addr); err != nil { + errChan <- fmt.Errorf("Error binding socket to interface: %w", err) + return + } - // Extract CAN ID and data - canID := binary.LittleEndian.Uint32(buf[0:4]) & 0x1FFFFFFF // 29-bit CAN ID (masked) - data := bytesToUint64(buf[8:]) - - msg := CANMsg{ ID : canID, Value: data} - p.Send(msg) - } -} + buf := make([]byte, canFrameSize) + fmt.Println("Listening for CAN messages on", canInterface) + + for { + select { + case <-ctx.Done(): // Exit loop when context is canceled + log.Println("CAN listener shutting down...") + return + default: + _, err := unix.Read(sock, buf) + if err != nil { + log.Printf("Error receiving CAN message: %v", err) + continue + } + canID := binary.LittleEndian.Uint32(buf[0:4]) & 0x1FFFFFFF + data := bytesToUint64(buf[8:]) + msg := CANMsg{ID: canID, Value: data} + p.Send(msg) + } + } +} func main() { - // Define the command line flag - warnFlag := flag.Int("w", 5, "warning time for the table (integer value)") - canInterfaceFlag := flag.String("i", "vcan0", "CAN interface to listen on") - - flag.Parse() + warnFlag := flag.Int("w", 5, "warning time for the table (integer value)") + canInterfaceFlag := flag.String("i", "vcan0", "CAN interface to listen on") + flag.Parse() + m := initViewer(*warnFlag) - // Initialize the model and run the program - var m model = initViewer(*warnFlag) - // channel to listen for the quit signal - quit := make(chan struct{}) - - // Start the TUI - p := tea.NewProgram(m) - go func() { - if _, err := p.Run(); err != nil { - fmt.Println("Error running TUI:", err) - } - close(quit) - }() - // Start the CAN listener - go can_listener(p, *canInterfaceFlag) - - <-quit -} + quit := make(chan struct{}) + errChan := make(chan error) + ctx, cancel := context.WithCancel(context.Background()) + + p := tea.NewProgram(m) + go func() { + if _, err := p.Run(); err != nil { + errChan <- fmt.Errorf("Error running TUI: %w", err) + } + close(quit) // Notify the main loop that TUI has exited + }() + + go can_listener(ctx, p, *canInterfaceFlag, errChan) + + select { + case err := <-errChan: + gracefulExit(p, cancel, quit, "Exiting due to error", err) + case <-quit: + cancel() // Ensure all goroutines stop + } +} \ No newline at end of file diff --git a/scripts/can_errgo/setup_vcan.sh b/scripts/can_errgo/setup_vcan.sh index 16a885e48..d8da66f20 100644 --- a/scripts/can_errgo/setup_vcan.sh +++ b/scripts/can_errgo/setup_vcan.sh @@ -3,6 +3,12 @@ CAN_PORT=$1 -sudo modprobe vcan +if [ -z "$CAN_PORT" ]; then + echo "Error: CAN_PORT is required as an argument." + echo "Usage: $0 " + exit 1 +fi + +sudo modprobe -a can can_raw vcan sudo ip link add dev $CAN_PORT type vcan sudo ip link set up $CAN_PORT From a8e255c545b282c658284e55adbfd098e730c94e Mon Sep 17 00:00:00 2001 From: Samuel Shi Date: Tue, 19 Nov 2024 22:17:18 -0500 Subject: [PATCH 22/22] remove submodule --- firmware/projects/FrontController/vehicle_control_system | 1 - .../projects/debug/FrontControllerSimple/vehicle_control_system | 1 - 2 files changed, 2 deletions(-) delete mode 160000 firmware/projects/FrontController/vehicle_control_system delete mode 160000 firmware/projects/debug/FrontControllerSimple/vehicle_control_system diff --git a/firmware/projects/FrontController/vehicle_control_system b/firmware/projects/FrontController/vehicle_control_system deleted file mode 160000 index 4fddb9369..000000000 --- a/firmware/projects/FrontController/vehicle_control_system +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 4fddb9369730a8cee4e74f83d71d3b1e33b8d5ea diff --git a/firmware/projects/debug/FrontControllerSimple/vehicle_control_system b/firmware/projects/debug/FrontControllerSimple/vehicle_control_system deleted file mode 160000 index a04e3a93f..000000000 --- a/firmware/projects/debug/FrontControllerSimple/vehicle_control_system +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a04e3a93f0def3d748ca7c636cb0f226328c0a10