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