From 2ce04ad91710d274e476afb3780a16564150a599 Mon Sep 17 00:00:00 2001 From: Blake Freer Date: Tue, 23 Sep 2025 14:55:09 -0400 Subject: [PATCH] Squashed commit of the following: commit 5e5e3880fe89c78b80b4e6547faf84860db0de34 Author: sokosam Date: Sat Jun 14 22:19:06 2025 -0400 dashboard tuning interface commit e5f995e6fc6158e50373d38bfde573556e7baf77 Author: sokosam Date: Sat Jun 7 14:44:41 2025 -0400 change states commit 4137d823107f3f3e3ac957c28f3abd8657ea3363 Author: sokosam Date: Sat Jun 7 14:33:25 2025 -0400 dashboard changes commit 82aed62fd414d344f0046f0e2adc3f6ebdfe7f76 Author: Samuel Shi Date: Wed May 7 13:59:04 2025 -0400 tuning --- projects/dashboard/include/Display.hpp | 2 +- projects/dashboard/include/Tuning.hpp | 11 ++ projects/dashboard/src/ProfileSelect.cpp | 7 ++ .../dashboard/src/platforms/linux/fc_sim.py | 26 ++++ projects/veh.dbc | 3 +- scripts/canflash/CanFlashApp.py | 58 +++++++++ scripts/canflash/LiveTuning.py | 87 +++++++++++++ scripts/canflash/README.md | 2 +- scripts/canflash/constants.py | 3 - scripts/canflash/main.py | 114 ++++++++---------- scripts/canflash/scripts/setup.sh | 2 +- scripts/canflash/scripts/teardown.sh | 2 +- scripts/canflash/scripts/tune.sh | 42 +++++++ 13 files changed, 286 insertions(+), 73 deletions(-) create mode 100644 projects/dashboard/include/Tuning.hpp create mode 100644 scripts/canflash/CanFlashApp.py create mode 100644 scripts/canflash/LiveTuning.py mode change 100644 => 100755 scripts/canflash/scripts/setup.sh create mode 100644 scripts/canflash/scripts/tune.sh diff --git a/projects/dashboard/include/Display.hpp b/projects/dashboard/include/Display.hpp index f9f2e0b38..f95edbc60 100644 --- a/projects/dashboard/include/Display.hpp +++ b/projects/dashboard/include/Display.hpp @@ -17,6 +17,7 @@ #include "StartingMotors.hpp" #include "generated/can/veh_bus.hpp" #include "generated/can/veh_messages.hpp" +#include "inc/Tuning.hpp" class Display { public: @@ -44,7 +45,6 @@ class Display { std::optional transition_; State state_ = State::LOGO; Screen* screen_; - LogoScreen logo_screen; ProfileSelect profile_select; ConfirmMenu confirm_menu; diff --git a/projects/dashboard/include/Tuning.hpp b/projects/dashboard/include/Tuning.hpp new file mode 100644 index 000000000..3b3300eee --- /dev/null +++ b/projects/dashboard/include/Tuning.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include "inc/Screen.hpp" + +class Tuning : public Screen { +public: + Tuning(Display* display); + + void CreateGUI() override; + void Update() override; +}; diff --git a/projects/dashboard/src/ProfileSelect.cpp b/projects/dashboard/src/ProfileSelect.cpp index ad0a80739..c30be3a9e 100644 --- a/projects/dashboard/src/ProfileSelect.cpp +++ b/projects/dashboard/src/ProfileSelect.cpp @@ -1,5 +1,7 @@ #include "ProfileSelect.hpp" +#include + #include "Display.hpp" #include "generated/can/veh_bus.hpp" @@ -35,7 +37,12 @@ void ProfileSelect::Update() { if (display_->enter.PosEdge()) { display_->selected_profile = static_cast(lv_roller_get_selected(roller)); + // if (static_cast( + // GetProfileName(display_->selected_profile)) == "TUNING") { + // display_->ChangeState(State::TUNING); + // } else { display_->ChangeState(State::CONFIRM_SELECTION); + // } } else if (display_->scroll.PosEdge()) { int new_position = lv_roller_get_selected(roller) + 1; diff --git a/projects/dashboard/src/platforms/linux/fc_sim.py b/projects/dashboard/src/platforms/linux/fc_sim.py index d2a1d89e5..a0d38eb27 100644 --- a/projects/dashboard/src/platforms/linux/fc_sim.py +++ b/projects/dashboard/src/platforms/linux/fc_sim.py @@ -115,6 +115,32 @@ def run(self): lambda ds: ds["State"] == "WAIT_SELECTION_ACK", ) print(f"Profile:\t{ds['Profile']}") + + if ds["Profile"] == "Tuning": + while True: + self.bus.set_filters( + [ + { + "can_id": dash_msg.frame_id, + "can_mask": 0x7FF, + "extended": False, + }, + { + "can_id": rberry_msg.frame_id, + "can_mask": 0x7FF, + "extended": False, + }, + ] + ) + tune_msg = self.bus.recv() + # print(f"ID: {hex(tune_msg.arbitration_id)}, DLC: {tune_msg.dlc}, data: {tune_msg.data.hex()}") + + tune = dbc.decode_message(tune_msg.arbitration_id, tune_msg.data) + if tune_msg.arbitration_id == 260: + print(tune) + break + # print(tune) + sleep(DELAY) self.FcStatus["ConfigReceived"] = True diff --git a/projects/veh.dbc b/projects/veh.dbc index 42efae436..1c0a17189 100644 --- a/projects/veh.dbc +++ b/projects/veh.dbc @@ -151,7 +151,8 @@ BO_ 310 SuspensionTravel34: 2 LVC SG_ STP4 : 8|8@1+ (1,0) [0|255] "" FC BO_ 340 TuningParams: 8 RPI - SG_ aggressiveness : 0|8@1+ (1,0) [0|100] "" FC + SG_ damping : 0|8@1+ (1,0) [0|100] "" FC + SG_ aggressiveness : 8|8@1+ (1,0) [0|100] "" FC CM_ "4xn - General / debugging info"; diff --git a/scripts/canflash/CanFlashApp.py b/scripts/canflash/CanFlashApp.py new file mode 100644 index 000000000..0ae55027f --- /dev/null +++ b/scripts/canflash/CanFlashApp.py @@ -0,0 +1,58 @@ +from nicegui import ui +from nicegui.events import UploadEventArguments +from flash_logic import save_uploaded_file, flash_file +from constants import ECU_CONFIG +import os + + +class CanFlashApp: + def __init__(self): + self.starting_ecu = "Select ECU" + self.selected_ecu = self.starting_ecu + self.uploaded_file_path = None + self.build_ui() + + def handle_upload(self, file: UploadEventArguments): + self.uploaded_file_path = save_uploaded_file(self.selected_ecu, file.name, file.content.read()) + self.flash_button.enable() + + def handle_flash(self): + exception = flash_file(self.selected_ecu, self.uploaded_file_path) + + if not exception: + uploaded_file_name = os.path.split(self.uploaded_file_path)[-1] + ui.notify( + f"Successfully flashed {uploaded_file_name} to {self.selected_ecu}", + position="center", + ) + + # Reset upload box and disable flash button + self.upload_box.reset() + self.flash_button.disable() + + # Reset ECU selection after a short delay (or else the select box won't reset) + ui.timer(0.1, lambda: self.ecu_select.set_value(self.starting_ecu), once=True) + else: + ui.notify(str(exception), type="negative") + + + def build_ui(self): + ui.markdown("Welcome to CAN Flash! Select an ECU and upload a file to flash. See [docs](https://macformula.github.io/racecar/) for more information.") + + # ECU Selection + self.ecu_select = ui.select([self.starting_ecu] + list(ECU_CONFIG.keys())) + self.ecu_select.bind_value(self, "selected_ecu") + + # File Upload + self.upload_box = ui.upload( + label="Select File", + on_upload=self.handle_upload, + auto_upload=True, + ).props("accept=.bin") + self.upload_box.bind_enabled_from( + self.ecu_select, "value", lambda v: v != self.starting_ecu + ) + + # Flash Button + self.flash_button = ui.button("Flash", on_click=self.handle_flash) + self.flash_button.disable() diff --git a/scripts/canflash/LiveTuning.py b/scripts/canflash/LiveTuning.py new file mode 100644 index 000000000..fabff53b0 --- /dev/null +++ b/scripts/canflash/LiveTuning.py @@ -0,0 +1,87 @@ +from nicegui import ui, events +import subprocess + + +class LiveTuning: + + def __init__(self, iface: str = "vcan0") -> None: + self.iface = iface + self.aggressiveness = 0 + self.dampness = 0 + self._build_ui() + + def _slider_row( + self, + title: str, + field_name: str, + init_val: int = 0, + *, + on_change_cb, + ): + with ui.row().classes("items-center gap-4 w-full"): + ui.label(title).classes("w-32 text-right") + + slider = ( + ui.slider(min=0, max=100, value=init_val, on_change=on_change_cb) + .props("step=1") + .classes("flex-1") + ) + + number = ( + ui.number(value=init_val, min=0, max=100) + .props("style='width:80px'") + ) + + number.bind_value_from(slider, "value").bind_value_to(slider, "value") + + setattr(self, f"{field_name}_slider", slider) + setattr(self, f"{field_name}_number", number) + + def _build_ui(self) -> None: + """Compose the entire visible UI.""" + with ui.card().classes("w-full max-w-md mx-auto mt-14 p-6 shadow-lg"): + ui.label("Live Tuning").classes("text-2xl font-semibold mb-6 text-center") + + # Aggressiveness row + self._slider_row( + "Aggressiveness", + "aggressiveness", + init_val=self.aggressiveness, + on_change_cb=lambda e: self._on_change("aggressiveness", e), + ) + + ui.separator() + + # Dampness row + self._slider_row( + "Dampness", + "dampness", + init_val=self.dampness, + on_change_cb=lambda e: self._on_change("dampness", e), + ) + + ui.button( + "Submit", + icon="send", + on_click=self._submit, + ).classes("w-full bg-primary text-white mt-6") + + def _on_change(self, field: str, e: events.ValueChangeEventArguments) -> None: + # Clamp, validate, and store slider changes + value = e.value or 0 + value = max(0, min(100, int(value))) + setattr(self, field, value) + print(f"{field.capitalize()} set to {value}") + + def _submit(self, _=None) -> None: + print(f"Submit pressed (agg={self.aggressiveness}, damp={self.dampness})") + subprocess.run( + ["bash", "./scripts/tune.sh", + str(self.aggressiveness), str(self.dampness), self.iface], + check=True, + ) + + +if __name__ in {"__mp_main__", "__main__"}: + LiveTuning() + ui.run() diff --git a/scripts/canflash/README.md b/scripts/canflash/README.md index 4219dde0e..706704e0c 100644 --- a/scripts/canflash/README.md +++ b/scripts/canflash/README.md @@ -36,4 +36,4 @@ Run the server with the following command: python3 main.py ``` -Access the UI by opening [http://localhost:8000/](http://localhost:8000/) in your web browser. \ No newline at end of file +Access the UI by opening [http://localhost:8000/](http://localhost:8000/) in your web browser. diff --git a/scripts/canflash/constants.py b/scripts/canflash/constants.py index 02ae45f58..933fe099c 100644 --- a/scripts/canflash/constants.py +++ b/scripts/canflash/constants.py @@ -2,9 +2,6 @@ UPLOAD_DIR = "uploads" # ECU Configuration with GPIO pins and CAN message IDs - -# WARNING: These GPIO pins are not correct and need to be fixed. They correspond to -# the Raspberry PI gpio pin which connects to each ECU's boot pin. ECU_CONFIG = { "FrontController": { "gpio_pin": 8, diff --git a/scripts/canflash/main.py b/scripts/canflash/main.py index 723f0ad27..99daf88f4 100644 --- a/scripts/canflash/main.py +++ b/scripts/canflash/main.py @@ -1,69 +1,53 @@ from nicegui import ui -from nicegui.events import UploadEventArguments -from flash_logic import save_uploaded_file, flash_file -from constants import ECU_CONFIG -import os - - -class CanFlashApp: - def __init__(self): - self.starting_ecu = "Select ECU" - self.selected_ecu = self.starting_ecu - self.uploaded_file_path = None - self.build_ui() - - def handle_upload(self, file: UploadEventArguments): - self.uploaded_file_path = save_uploaded_file(self.selected_ecu, file.name, file.content.read()) - self.flash_button.enable() - - def handle_flash(self): - exception = flash_file(self.selected_ecu, self.uploaded_file_path) - - if not exception: - uploaded_file_name = os.path.split(self.uploaded_file_path)[-1] - ui.notify( - f"Successfully flashed {uploaded_file_name} to {self.selected_ecu}", - position="center", - ) - - # Reset upload box and disable flash button - self.upload_box.reset() - self.flash_button.disable() - - # Reset ECU selection after a short delay (or else the select box won't reset) - ui.timer(0.1, lambda: self.ecu_select.set_value(self.starting_ecu), once=True) - else: - ui.notify(str(exception), type="negative") - - - def build_ui(self): - ui.colors(primary="#AA1F26") - ui.markdown("Welcome to CAN Flash! Select an ECU and upload a file to flash. See [docs](https://macformula.github.io/racecar/) for more information.") - - # ECU Selection - self.ecu_select = ui.select([self.starting_ecu] + list(ECU_CONFIG.keys())) - self.ecu_select.bind_value(self, "selected_ecu") - - # File Upload - self.upload_box = ui.upload( - label="Select File", - on_upload=self.handle_upload, - auto_upload=True, - ).props("accept=.bin") - self.upload_box.bind_enabled_from( - self.ecu_select, "value", lambda v: v != self.starting_ecu - ) - - # Flash Button - self.flash_button = ui.button("Flash", on_click=self.handle_flash) - self.flash_button.disable() - - -@ui.page("/") +from CanFlashApp import CanFlashApp +from LiveTuning import LiveTuning +from contextlib import contextmanager + +@contextmanager +def frame(): + ui.colors(primary="#AA1F26") + with ui.header(elevated=True).classes('width-full text-white bg-[#AA1F26] '): + ui.label("Raspberry Pi Resources").classes("text-xl font-bold mr-4 ") + ui.button("Home", on_click=lambda : ui.navigate.to('/')).classes("!bg-[#df535a]") + ui.button('Can Flash', on_click= lambda : ui.navigate.to("/canflash")).classes("!bg-[#df535a]") + ui.button('Tuning', on_click= lambda : ui.navigate.to("/tune")).classes("!bg-[#df535a]") + yield + +@ui.page("/canflash") def main_page(): - CanFlashApp() - - + with frame(): + CanFlashApp() + +@ui.page("/tune") +def tuning_page(): + with frame(): + LiveTuning() + +@ui.page('/') +def home() -> None: + with frame(): # keep the shared header + with ui.column().classes('items-center q-mt-xl gap-4'): + ui.label('Welcome to the Formula‑Electric Raspberry Pi Interface') \ + .classes('text-2xl font-bold') + + ui.markdown( + 'This dashboard lets you interact with the **Formula Electric** car’s ' + 'Raspberry Pi. \n' + 'Use it to *live‑tune* parameters while the car is running or to ' + '*flash* new firmware over CAN. ' + 'More modules will be added soon!' + ).classes('max-w-xl text-center') + + with ui.row().classes('gap-4'): + ui.button('Go to Live Tuning', on_click=lambda: ui.navigate.to('/tune')) \ + .classes('!bg-[#df535a] text-white') + ui.button('Go to CAN Flash', on_click=lambda: ui.navigate.to('/canflash')) \ + .classes('!bg-[#df535a] text-white') + + ui.link('Read the full documentation', + 'https://macformula.github.io/racecar/', + new_tab=True) \ + .classes('text-primary underline q-mt-md') if __name__ in {"__main__", "__mp_main__"}: ui.run( title="CAN Flash", @@ -71,4 +55,4 @@ def main_page(): show=False, dark=None, port=8000, - ) + ) \ No newline at end of file diff --git a/scripts/canflash/scripts/setup.sh b/scripts/canflash/scripts/setup.sh old mode 100644 new mode 100755 index 041e3c011..a225af3f3 --- a/scripts/canflash/scripts/setup.sh +++ b/scripts/canflash/scripts/setup.sh @@ -23,7 +23,7 @@ pip install -r requirements.txt sudo apt update # Install dependencies -sudo apt install -y libgtk-3-dev build-essential gcc g++ pkg-config make hostapd libqrencode-dev libpng-dev iptables +sudo apt install -y libgtk-3-dev build-essential gcc g++ pkg-config make hostapd libqrencode-dev libpng-dev iptables can-utils # Clone repo cd diff --git a/scripts/canflash/scripts/teardown.sh b/scripts/canflash/scripts/teardown.sh index fb4534e2e..f578b662b 100644 --- a/scripts/canflash/scripts/teardown.sh +++ b/scripts/canflash/scripts/teardown.sh @@ -16,7 +16,7 @@ sudo rm -f /lib/systemd/system/canflash.service # Remove installed packages echo "Removing installed packages..." -sudo apt remove --purge -y libgtk-3-dev build-essential gcc g++ pkg-config make hostapd libqrencode-dev libpng-dev iptables +sudo apt remove --purge -y libgtk-3-dev build-essential gcc g++ pkg-config make hostapd libqrencode-dev libpng-dev iptables can-utils sudo apt autoremove -y sudo apt clean diff --git a/scripts/canflash/scripts/tune.sh b/scripts/canflash/scripts/tune.sh new file mode 100644 index 000000000..2c65b9c77 --- /dev/null +++ b/scripts/canflash/scripts/tune.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash + +set -euo pipefail + +show_help() { + cat < [iface] + +Arguments + aggressiveness Integer 0–100 (byte 1) + dampness Integer 0–100 (byte 0) + iface CAN interface to use (default: vcan0) + +The script packs the two values into a 2-byte CAN frame with ID 0x104 and +sends it via 'cansend'. +EOF +} + +# --- Parse args ------------------------------------------------------------- +[[ $# -eq 0 || $1 == "-h" || $1 == "--help" ]] && { show_help; exit 0; } + +if [[ $# -lt 2 || $# -gt 3 ]]; then + echo "Error: need 2 or 3 arguments." >&2 + show_help + exit 1 +fi + +AGGR=$1 +DAMP=$2 +IFACE=${3:-vcan0} + +for val in "$AGGR" "$DAMP"; do + [[ $val =~ ^[0-9]+$ ]] || { echo "Error: values must be integers." >&2; exit 1; } + (( val >= 0 && val <= 100 )) || { echo "Error: values must be 0-100." >&2; exit 1; } +done + +AGGR_HEX=$(printf '%02X' "$AGGR") +DAMP_HEX=$(printf '%02X' "$DAMP") + +echo "Sending TuningParams on $IFACE: dampness=$DAMP, aggressiveness=$AGGR (0x${DAMP_HEX}${AGGR_HEX})" +cansend "$IFACE" 104#${DAMP_HEX}${AGGR_HEX} +echo "Message sent!" \ No newline at end of file