diff --git a/scripts/can_errgo/README.md b/scripts/can_errgo/README.md new file mode 100644 index 000000000..8373077bd --- /dev/null +++ b/scripts/can_errgo/README.md @@ -0,0 +1,55 @@ +# 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 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 + +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: + +```bash +ip link show +sudo ip link delete +``` + +Setup the can network: + +```bash +./setup_vcan.sh +``` + +## 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: + +```bash +cansend +``` diff --git a/scripts/can_errgo/components/table.go b/scripts/can_errgo/components/table.go new file mode 100644 index 000000000..ef5defb34 --- /dev/null +++ b/scripts/can_errgo/components/table.go @@ -0,0 +1,488 @@ +package components + +// Modified from "github.com/charmbracelet/bubbles/table" +// Repurposed for use in CAN error handling application + +import ( + "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" +) + +// 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 + metaRows []MetaRow + 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 []MetaRow) Option { + return func(m *Model) { + m.metaRows = 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.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. + // 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.metaRows)) + 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.metaRows) { + return nil + } + + 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.metaRows) { + return -1 + } + return m.metaRows[m.cursor].Index +} + +// Rows returns the current metaRows. +func (m Model) Rows() []MetaRow { + return m.metaRows +} + +// Columns returns the current columns. +func (m Model) Columns() []Column { + return m.cols +} + +// SetRows sets a new metaRows state. +func (m *Model) SetRows(r []MetaRow) { + m.metaRows = 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.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.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.metaRows)-1) + + switch { + case m.start == 0: + m.viewport.SetYOffset(clamp(m.viewport.YOffset, 0, m.cursor)) + case m.end == len(m.metaRows) && 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 metaRows. +// It can not go above the first row. +func (m *Model) MoveUp(n int) { + 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)) + 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 metaRows. +// It can not go below the last row. +func (m *Model) MoveDown(n int) { + m.cursor = clamp(m.cursor+n, 0, len(m.metaRows)-1) + m.UpdateViewport() + + switch { + 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)) + 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.metaRows)) +} + +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.metaRows[r].Row { + 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/components/text-box.go b/scripts/can_errgo/components/text-box.go new file mode 100644 index 000000000..316c3145b --- /dev/null +++ b/scripts/can_errgo/components/text-box.go @@ -0,0 +1,60 @@ +// Custom text-box component for rendering a box with a title and description, will dynamically wrap text to fit within the box width. +package components + +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 +} + +// 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) + 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)) + + content := lipgloss.JoinVertical(lipgloss.Left, titleContent, descriptionContent) + + boxHeight := 3 + strings.Count(content, "\n \n \n") + 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 + + 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 new file mode 100644 index 000000000..caa8a8bad --- /dev/null +++ b/scripts/can_errgo/error_tui.go @@ -0,0 +1,576 @@ +package main + +import ( + "context" + "encoding/binary" + "flag" + "fmt" + "log" + "math" + "net" + "os" + "sort" + "strconv" + "time" + + box "main/components" + table "main/components" + + "github.com/charmbracelet/bubbles/key" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "golang.org/x/sys/unix" +) + + +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 { + 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 +} + +// Default style for the table +var baseStyle = lipgloss.NewStyle(). + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("240")) + +// 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} +} + +func toTableRows(r []ErrorData) []table.MetaRow { + var rows []table.MetaRow + for _, v := range r { + // Check if the ErrorData is active + if v.Count != 0 && !v.Ignored{ + rows = append(rows, RowValuesToRow(v)) + } + } + return rows +} + +// This creates a deep copy of the input slice. +func SortRowValuesByRecency(r []ErrorData) []ErrorData { + copied := make([]ErrorData, len(r)) + copy(copied, r) + + sort.SliceStable(copied, func(i, j int) bool { + return copied[i].Recency < copied[j].Recency + }) + + return copied +} + + +// 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 ,row := range rowValues{ + if row.Count == 0 || row.Ignored{ + cnt += 1 + } + if row.ErrorIndex == errorIndex{ + return i - cnt + } + } + return -1 +} + +func (m model) getIgnoredRows() []table.MetaRow { + var rows []table.MetaRow + for _,row := range m.errorData{ + if row.Ignored{ + rows = append(rows, table.MetaRow{Row: table.Row{row.Error}, Index: row.ErrorIndex}) + } + } + return rows +} + +type ErrorData struct{ + Error string + Count int + Recency int + Description string + Ignored bool + // This is used for keeping track of the error bit after sorting + ErrorIndex int +} + +func (m model) isTimeout() bool { + return time.Now().Sub(m.lastCANTime) > m.timeout +} + +type CANMsg struct { + ID uint32 + 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 + + // Slice of row information and ignore mask for incoming errors/bits + errorData []ErrorData + ignoreMask uint64 + + // Timeout flag and last CAN message time + lastCANTime time.Time + timeout time.Duration + + // Box for displaying error descriptions + box box.Box + showBox bool +} + +func toggleBit(n uint64, bitPosition int) uint64 { + mask := uint64(1) << bitPosition + return n ^ mask +} + +func tickEvery(timeout time.Duration) tea.Cmd { + return func() tea.Msg { + time.Sleep(timeout) + return TickMsg{} + } +} + +func (m model) Init() tea.Cmd { return tickEvery(m.timeout) } + +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 { + case key.Matches(msg, table.DefaultKeyMap().LineUp): + m.submenuTable.MoveUp(1) + return m,nil + case key.Matches(msg, table.DefaultKeyMap().LineDown): + m.submenuTable.MoveDown(1) + return m,nil + 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. + errorIndex := m.submenuTable.SelectedIndex() + m.ignoreMask = toggleBit(m.ignoreMask, errorIndex) + m.errorData[errorIndex].Ignored = false + newRows := m.getIgnoredRows() + m.submenuTable.SetRows(newRows) + + case key.Matches(msg, m.submenuKeys.s): + m.submenuActive = false + m.table.Focus() + m.submenuTable.Blur() + m.table.SetShowCursor(true) + + return m, nil + + case key.Matches(msg, m.tableKeys.q): + return m, tea.Quit + } + + m.submenuTable, cmd = m.submenuTable.Update(msg) + return m, cmd + } + + + switch { + case key.Matches(msg, m.tableKeys.q): + return m, tea.Quit + case key.Matches(msg, m.tableKeys.a): + + // Prevent panic if no row is selected + if m.table.SelectedRow() == nil{ + m.table.SetCursorAndViewport(0) + return m, nil + } + + // Remove the row and reset its count. + errorIndex := m.table.SelectedIndex() + m.errorData[errorIndex].Count = 0 + newRows := toTableRows(m.errorData) + m.table.SetRows(newRows) + return m, nil + case key.Matches(msg, m.tableKeys.i): + + // Prevent panic if no row is selected + if m.table.SelectedRow() == nil{ + m.table.SetCursorAndViewport(0) + return m, nil + } + + // Toggle the mask for the selected error and move it to the ignore menu. + errorIndex := m.table.SelectedIndex() + m.ignoreMask = toggleBit(m.ignoreMask, errorIndex) + m.errorData[errorIndex].Ignored = true + + // Remove the row from the main table and reset its count. + m.errorData[errorIndex].Count = 0 + newRows := toTableRows(m.errorData) + m.table.SetRows(newRows) + + //Set add the ignored rows to submenu + ignoredRows := m.getIgnoredRows() + m.submenuTable.SetRows(ignoredRows) + + return m, nil + case key.Matches(msg, m.tableKeys.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() + m.table.SetShowCursor(false) + } + case key.Matches(msg, table.DefaultKeyMap().LineUp): + m.table.MoveUp(1) + return m,nil + case key.Matches(msg, table.DefaultKeyMap().LineDown): + m.table.MoveDown(1) + return m,nil + } + case CANMsg: + m.lastCANTime = time.Now() + msg.Value = msg.Value & m.ignoreMask + + for i,errorVal := range m.errorData{ + m.errorData[i].Recency += 1 + if msg.Value&(1<" + 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 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/test_multi.sh b/scripts/can_errgo/utilityFiles/test_multi.sh new file mode 100644 index 000000000..49ffc3c15 --- /dev/null +++ b/scripts/can_errgo/utilityFiles/test_multi.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 diff --git a/scripts/can_errgo/utilityFiles/test_once.sh b/scripts/can_errgo/utilityFiles/test_once.sh new file mode 100644 index 000000000..f62981d2c --- /dev/null +++ b/scripts/can_errgo/utilityFiles/test_once.sh @@ -0,0 +1,5 @@ +#!/bin/bash + + +sleep $1 +cansend vcan0 FFF#$2 \ No newline at end of file