diff --git a/CMakeLists.txt b/CMakeLists.txt index abd3cc7..fe5adea 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.18) +cmake_minimum_required(VERSION 3.16) project("FT22 Steering Wheel Display") @@ -60,6 +60,9 @@ add_executable( src/AMI.cpp src/widgets.cpp src/util.cpp + src/DriverView.cpp + src/BaseDataSource.cpp + src/DemoDataSource.cpp ) target_include_directories( stw-display PUBLIC @@ -78,4 +81,4 @@ target_link_libraries( ${I2C_LIBRARY} fmt spdlog::spdlog) -add_dependencies(stw-display fmt) \ No newline at end of file +add_dependencies(stw-display fmt) diff --git a/include/App.h b/include/App.h index 685cf8f..c88430e 100644 --- a/include/App.h +++ b/include/App.h @@ -2,6 +2,7 @@ #include "AMI.h" #include "AppState.h" +#include "DriverView.h" #include "MissionSelect.h" #include "defines.h" @@ -44,6 +45,7 @@ private: std::unique_ptr state; std::unique_ptr mission_select; std::unique_ptr ami; + std::unique_ptr driver_view; bool running; diff --git a/include/AppState.h b/include/AppState.h index 9bbb8ac..763b424 100644 --- a/include/AppState.h +++ b/include/AppState.h @@ -1,5 +1,7 @@ #pragma once +#include "DemoDataSource.h" + #include #include @@ -32,6 +34,7 @@ public: AppView get_view() const; Mission get_mission() const; + DemoDataSource get_data_source() const; private: void unmarshal_mission_select(uint8_t* data); @@ -42,6 +45,7 @@ private: AppView view; Mission mission; + DemoDataSource data_source; #ifndef NDEBUG friend class KeyboardHandler; diff --git a/include/BaseDataSource.h b/include/BaseDataSource.h new file mode 100644 index 0000000..ed62533 --- /dev/null +++ b/include/BaseDataSource.h @@ -0,0 +1,34 @@ +#pragma once + +class BaseDataSource { +public: + double get_speed(); // in km/h + double get_brake_balance(); // 0.0 - 1.0 + double get_hv_voltage(); // in V + double get_hv_voltage_ratio(); // 0.0 - 1.0 + double get_lv_voltage(); // in V + double get_battery_temperature(); // in °C + double get_throttle_ratio(); // 0.0 - 1.0 + double get_brake_pressure_front_bar(); // in bar + double get_brake_pressure_rear_bar(); // in bar + + bool brake_balance_change_is_recent(); // true if the last change was less + // than 1 second ago + + virtual void poll() = 0; + +protected: + void set_brake_balance(double value); + + double speed; + double brake_balance; + double hv_voltage; + double hv_voltage_ratio; + double lv_voltage; + double battery_temperature; + double throttle_ratio; + double brake_pressure_front_bar; + double brake_pressure_rear_bar; + + int time_of_last_brake_balance_change; +}; diff --git a/include/DemoDataSource.h b/include/DemoDataSource.h new file mode 100644 index 0000000..5ede51a --- /dev/null +++ b/include/DemoDataSource.h @@ -0,0 +1,13 @@ +#pragma once + +#include "BaseDataSource.h" + +class DemoDataSource final : public BaseDataSource { +public: + DemoDataSource(); + ~DemoDataSource(); + + void poll() override; + void bump_brake_balance_up(); + void bump_brake_balance_down(); +}; diff --git a/include/DriverView.h b/include/DriverView.h new file mode 100644 index 0000000..595149e --- /dev/null +++ b/include/DriverView.h @@ -0,0 +1,30 @@ +#pragma once +#include "View.h" +#include "defines.h" +#include "util.h" +#include "widgets.h" + +class DriverView final : public View { +public: + DriverView(SDL_Renderer* renderer); + ~DriverView(); + + void draw(const AppState& state) override; + +private: + TTF_Font* font_large; + TTF_Font* font_detail; + TTF_Font* font_tiny; + TTF_Font* font_giant; + TTF_Font* font_medium; + std::unique_ptr speed_widget; + std::unique_ptr speed_hint_widget; + + std::unique_ptr brake_balance_widget; + std::unique_ptr brake_balance_hint_widget; + + std::unique_ptr general_info_widget; + + std::unique_ptr focus_widget; + std::unique_ptr focus_hint_widget; +}; \ No newline at end of file diff --git a/include/widgets.h b/include/widgets.h index cd48cf2..2186b10 100644 --- a/include/widgets.h +++ b/include/widgets.h @@ -79,12 +79,13 @@ public: ~TextWidget(); void update_text(const std::string& new_text); + void set_wrap_width(int width); protected: TTF_Font* font; std::string text; - + int wrap_width = -1; // -1 means no wrapping (default behavior) SDL_Texture* generate_text(const std::string& text); }; diff --git a/src/App.cpp b/src/App.cpp index 0c8678d..44bba77 100644 --- a/src/App.cpp +++ b/src/App.cpp @@ -2,6 +2,8 @@ #include "AMI.h" #include "AppState.h" +#include "DemoDataSource.h" +#include "DriverView.h" #include "MissionSelect.h" #include "events.h" @@ -15,6 +17,7 @@ #include #include +#include #include #include #include @@ -60,6 +63,11 @@ void App::init_state() { int App::run() { mission_select = std::make_unique(renderer); ami = std::make_unique(renderer); + driver_view = std::make_unique(renderer); + +#ifndef NDEBUG + std::cout << "Change brake balance with W (+) and S (-)" << std::endl; +#endif running = true; @@ -110,6 +118,9 @@ void App::render() { case AppView::AMI: ami->draw(*state); break; + case AppView::DRIVER: + driver_view->draw(*state); + break; default: throw std::runtime_error(fmt::format("Unknown view: {}", view)); } @@ -138,39 +149,44 @@ SDLManager::~SDLManager() { SDL_Quit(); } +/* +Handle key presses +*/ void KeyboardHandler::handle_keyup(const SDL_Event& ev, AppState& state) { - switch (ev.key.keysym.sym) { - case SDLK_DOWN: - case SDLK_RIGHT: - if (state.mission == Mission::MANUAL) { - state.mission = Mission::ACCELERATION; - } else { - state.mission = static_cast(static_cast(state.mission) + 1); + auto pressed_key = ev.key.keysym.sym; + if (state.view == AppView::MISSION_SELECT) { + if (pressed_key == SDLK_DOWN || pressed_key == SDLK_RIGHT) { + auto new_mission = static_cast(state.mission) + 1; + if (new_mission > 7) { + new_mission = 1; + } + std::cout << "New mission: " << new_mission << std::endl; + state.mission = static_cast(new_mission); + } else if (pressed_key == SDLK_UP || pressed_key == SDLK_LEFT) { + auto new_mission = static_cast(state.mission) - 1; + if (new_mission < 1) { + new_mission = 7; + } + state.mission = static_cast(new_mission); + } else if (pressed_key == SDLK_RETURN) { + + if (state.mission == Mission::MANUAL) { + state.view = AppView::DRIVER; + } else { + state.view = AppView::AMI; + } } - break; - case SDLK_UP: - case SDLK_LEFT: - if (state.mission == Mission::NONE || - state.mission == Mission::ACCELERATION) { - state.mission = Mission::MANUAL; - } else { - state.mission = static_cast(static_cast(state.mission) - 1); - } - break; - case SDLK_RETURN: - case SDLK_KP_ENTER: - switch (state.view) { - case AppView::MISSION_SELECT: - state.view = AppView::AMI; - break; - case AppView::AMI: - case AppView::DRIVER: - case AppView::TESTING: - break; - default: - throw std::runtime_error(fmt::format("Unknown view: {}", state.view)); - } - break; - // We can just ignore other keypresses, so no need for a default clause } -} \ No newline at end of file + + if (state.view == AppView::AMI || state.view == AppView::DRIVER) { + if (pressed_key == SDLK_ESCAPE) { + state.view = AppView::MISSION_SELECT; + } + } + + if (pressed_key == SDLK_w) { + state.data_source.bump_brake_balance_up(); + } else if (pressed_key == SDLK_s) { + state.data_source.bump_brake_balance_down(); + } +} diff --git a/src/AppState.cpp b/src/AppState.cpp index ab8a611..ce37386 100644 --- a/src/AppState.cpp +++ b/src/AppState.cpp @@ -76,6 +76,7 @@ AppState::~AppState() { } void AppState::poll() { + data_source.poll(); if (!i2c_dev_file) { return; } @@ -112,4 +113,6 @@ void AppState::unmarshal_mission_select(uint8_t* data) { void AppState::unmarshal_ami(uint8_t* data) { mission = static_cast(data[1]); spdlog::info("Mission after poll: {}", mission); -} \ No newline at end of file +} + +DemoDataSource AppState::get_data_source() const { return data_source; } \ No newline at end of file diff --git a/src/BaseDataSource.cpp b/src/BaseDataSource.cpp new file mode 100644 index 0000000..c2327a1 --- /dev/null +++ b/src/BaseDataSource.cpp @@ -0,0 +1,35 @@ +#include "BaseDataSource.h" + +#include +double BaseDataSource::get_speed() { return speed; } + +double BaseDataSource::get_brake_balance() { return brake_balance; } + +double BaseDataSource::get_hv_voltage() { return hv_voltage; } + +double BaseDataSource::get_hv_voltage_ratio() { return hv_voltage_ratio; } + +double BaseDataSource::get_lv_voltage() { return lv_voltage; } + +double BaseDataSource::get_battery_temperature() { return battery_temperature; } + +double BaseDataSource::get_throttle_ratio() { return throttle_ratio; } + +double BaseDataSource::get_brake_pressure_front_bar() { + return brake_pressure_front_bar; +} + +double BaseDataSource::get_brake_pressure_rear_bar() { + return brake_pressure_rear_bar; +} + +void BaseDataSource::set_brake_balance(double value) { + brake_balance = value; + time_of_last_brake_balance_change = SDL_GetTicks(); +} + +bool BaseDataSource::brake_balance_change_is_recent() { + + return time_of_last_brake_balance_change != 0 && + SDL_GetTicks() - time_of_last_brake_balance_change < 500; +} diff --git a/src/DemoDataSource.cpp b/src/DemoDataSource.cpp new file mode 100644 index 0000000..206f4d0 --- /dev/null +++ b/src/DemoDataSource.cpp @@ -0,0 +1,33 @@ +#include "DemoDataSource.h" + +#include +#include +DemoDataSource::DemoDataSource() { + brake_balance = 0.5; + + lv_voltage = 13.1; + battery_temperature = 20.1; + throttle_ratio = 0.01; + brake_pressure_front_bar = 1; + brake_pressure_rear_bar = 1; +} + +DemoDataSource::~DemoDataSource() {} + +void DemoDataSource::poll() { + int min_voltage = 150; + int max_voltage = 480; + static int count = 0; + count++; + hv_voltage = 220.1 + std::cos(count / 1000.0 + 2) * 50; + hv_voltage_ratio = (hv_voltage - min_voltage) / (max_voltage - min_voltage); + speed = 10 + std::sin(count / 100.0) * 5; +} + +void DemoDataSource::bump_brake_balance_up() { + set_brake_balance(brake_balance + 0.01); +} + +void DemoDataSource::bump_brake_balance_down() { + set_brake_balance(brake_balance - 0.01); +} \ No newline at end of file diff --git a/src/DriverView.cpp b/src/DriverView.cpp new file mode 100644 index 0000000..327a0c3 --- /dev/null +++ b/src/DriverView.cpp @@ -0,0 +1,124 @@ +#include "DriverView.h" + +#include "DemoDataSource.h" +#include "defines.h" + +#include + +#include + +#define LARGE_FONT_SIZE_POINTS 170 +#define GIANT_FONT_SIZE_POINTS 200 +#define MEDIUM_FONT_SIZE_POINTS 50 +#define SOC_BAR_HEIGHT 80 +#define SPEED_TEXT_X 22 +#define SPEED_TEXT_Y 50 + +DriverView::DriverView(SDL_Renderer* renderer) + : View{renderer}, font_large{util::load_font("resources/Avenir-Book.ttf", + LARGE_FONT_SIZE_POINTS)}, + font_detail{util::load_font("resources/Avenir-Book.ttf", 20)}, + font_tiny{util::load_font("resources/Avenir-Book.ttf", 15)}, + font_giant{ + util::load_font("resources/Avenir-Book.ttf", GIANT_FONT_SIZE_POINTS)}, + font_medium{util::load_font("resources/Avenir-Book.ttf", + MEDIUM_FONT_SIZE_POINTS)} { + speed_widget = std::make_unique(renderer, font_large, ""); + speed_widget->set_position(SPEED_TEXT_X, SPEED_TEXT_Y); + speed_widget->set_alignment(Alignment::CENTER, Alignment::TOP); + + speed_hint_widget = std::make_unique(renderer, font_tiny, "SPD"); + speed_hint_widget->set_position(20, 210); + speed_hint_widget->set_alignment(Alignment::CENTER, Alignment::TOP); + + brake_balance_widget = std::make_unique(renderer, font_large, ""); + brake_balance_widget->set_position(SPEED_TEXT_X + SCREEN_WIDTH / 2, + SPEED_TEXT_Y); + brake_balance_widget->set_alignment(Alignment::CENTER, Alignment::TOP); + brake_balance_hint_widget = + std::make_unique(renderer, font_tiny, "BB"); + brake_balance_hint_widget->set_position(20 + SCREEN_WIDTH / 2, 210); + brake_balance_hint_widget->set_alignment(Alignment::CENTER, Alignment::TOP); + + general_info_widget = std::make_unique(renderer, font_detail, ""); + general_info_widget->set_position(SPEED_TEXT_X, SPEED_TEXT_Y + 200); + general_info_widget->set_alignment(Alignment::LEFT, Alignment::TOP); + general_info_widget->set_wrap_width(SCREEN_WIDTH - 50); + + focus_widget = std::make_unique(renderer, font_giant, ""); + focus_widget->set_position(120, 70); + focus_widget->set_alignment(Alignment::LEFT, Alignment::TOP); + focus_hint_widget = std::make_unique(renderer, font_medium, ""); + focus_hint_widget->set_position(120, 40); + // focus_hint_widget->set_alignment(Alignment::CENTER, Alignment::CENTER); +} + +DriverView::~DriverView() {} + +void DriverView::draw(const AppState& state) { + auto ds = state.get_data_source(); + auto hv_ratio = ds.get_hv_voltage_ratio(); + SDL_Rect rect; + rect.x = 0; + rect.y = 0; + rect.w = SCREEN_WIDTH; + rect.h = SCREEN_HEIGHT; + + static auto hv_low = false; + // hysteresis for the hv voltage low + if (hv_ratio < 0.15) { + hv_low = true; + } else if (hv_ratio > 0.2) { + hv_low = false; + } + + int red = hv_low ? 0xa0 : 0x30; + SDL_SetRenderDrawColor(renderer, red, 0x30, 0x30, 255); + SDL_RenderFillRect(renderer, &rect); + + auto brake_balance_text = fmt::format("{:.0f}", ds.get_brake_balance() * 100); + + if (ds.brake_balance_change_is_recent()) { + SDL_SetRenderDrawColor(renderer, 255, 69, 100, 255); + SDL_RenderFillRect(renderer, &rect); + focus_widget->update_text(brake_balance_text); + focus_widget->draw(); + focus_hint_widget->update_text("BBAL"); + focus_hint_widget->draw(); + return; + } + + rect.w = SCREEN_WIDTH * hv_ratio; + rect.h = SOC_BAR_HEIGHT; + SDL_SetRenderDrawColor(renderer, 76, 255, 0, 255); + SDL_RenderFillRect(renderer, &rect); + + speed_widget->update_text(fmt::format("{:02.0f}", ds.get_speed())); + speed_widget->draw(); + speed_hint_widget->draw(); + + brake_balance_widget->update_text(brake_balance_text); + + brake_balance_widget->draw(); + brake_balance_hint_widget->draw(); + + // draw rectangles around the speed and brake balance widgets + SDL_SetRenderDrawColor(renderer, 0xFF, 0xFF, 0xFF, 255); + rect.x = 0; + rect.y = SOC_BAR_HEIGHT; + rect.w = SCREEN_WIDTH / 2; + rect.h = 150; + SDL_RenderDrawRect(renderer, &rect); + rect.x = SCREEN_WIDTH / 2; + SDL_RenderDrawRect(renderer, &rect); + + auto general_string = fmt::format( + "HV: {:.0f}V | LV: {:.1f}V | B_TEMP: {:.1f}C\nAPPS: {:.0f}% | BPF: " + "{:.0f} bar | BPR: {:.0f} bar", + ds.get_hv_voltage(), ds.get_lv_voltage(), ds.get_battery_temperature(), + ds.get_throttle_ratio() * 100, ds.get_brake_pressure_front_bar(), + ds.get_brake_pressure_rear_bar()); + + general_info_widget->update_text(general_string); + general_info_widget->draw(); +} \ No newline at end of file diff --git a/src/MissionSelect.cpp b/src/MissionSelect.cpp index 7e1601e..b5dc204 100644 --- a/src/MissionSelect.cpp +++ b/src/MissionSelect.cpp @@ -1,6 +1,7 @@ #include "MissionSelect.h" #include "AppState.h" +#include "DriverView.h" #include "defines.h" #include "events.h" #include "util.h" diff --git a/src/widgets.cpp b/src/widgets.cpp index 21418d0..2523cdc 100644 --- a/src/widgets.cpp +++ b/src/widgets.cpp @@ -167,8 +167,13 @@ void TextWidget::update_text(const std::string& new_text) { } SDL_Texture* TextWidget::generate_text(const std::string& text) { - SDL_Surface* surf = - TTF_RenderText_Solid(font, text.c_str(), {0xFF, 0xFF, 0XFF, 0xFF}); + SDL_Surface* surf; + if (wrap_width == -1) { + surf = TTF_RenderText_Blended(font, text.c_str(), {0xFF, 0xFF, 0XFF, 0xFF}); + } else { + surf = TTF_RenderText_Blended_Wrapped(font, text.c_str(), + {0xFF, 0xFF, 0XFF, 0xFF}, wrap_width); + } if (surf == nullptr) { throw std::runtime_error( fmt::format("Unable to render text surface: {}", TTF_GetError())); @@ -181,6 +186,8 @@ SDL_Texture* TextWidget::generate_text(const std::string& text) { return texture; } +void TextWidget::set_wrap_width(int width) { wrap_width = width; } + ListWidget::ListWidget(SDL_Renderer* renderer, int element_height, Alignment element_alignment) : Widget{renderer}, element_height{element_height},