Compare commits

..

12 Commits

Author SHA1 Message Date
0a97a07da0 small fixes 2022-07-09 12:19:01 +02:00
1e378d1093 working demo driver view 2022-07-09 11:50:10 +02:00
938978f5d4 Unmarshal AMI info 2022-05-28 02:18:57 +02:00
0cd0ce6f3e Implement AMI, more communication with STM etc
It's late and I forgot to commit
2022-05-28 02:04:10 +02:00
c553ea36d4 Implement I2C communication with STM 2022-05-27 17:17:23 +02:00
49f703f4d4 Use software renderer 2022-05-22 22:25:27 +02:00
8affda125e Log FPS 2022-05-22 22:25:20 +02:00
2e76523bf4 AMI -> MissionSelect 2022-05-22 14:30:42 +02:00
654c71f3ee Select a mission 2022-05-22 14:25:21 +02:00
882a645926 Draw missions at their native height 2022-05-20 15:05:40 +02:00
0b34bac5bc Use Chinat font for mission list 2022-05-20 14:52:15 +02:00
c0b56f9f76 Recalculate widget position after changing width 2022-05-20 14:49:52 +02:00
21 changed files with 923 additions and 90 deletions

View File

@ -68,7 +68,14 @@ ForEachMacros:
- BOOST_FOREACH
IncludeBlocks: Regroup
IncludeCategories:
- Regex: '^[<"](fmt|SDL2)/'
# This will hopefully only catch stdlib headers
- Regex: "^<[^/]*>"
Priority: 5
SortPriority: 0
- Regex: '^[<"]SDL2/'
Priority: 2
SortPriority: 0
- Regex: '^[<"](fmt|spdlog)/'
Priority: 3
SortPriority: 0
- Regex: "^<"

View File

@ -1,4 +1,4 @@
cmake_minimum_required(VERSION 3.10)
cmake_minimum_required(VERSION 3.16)
project("FT22 Steering Wheel Display")
@ -11,6 +11,12 @@ set(CMAKE_CXX_STANDARD 17)
find_package(SDL2 REQUIRED)
find_package(SDL2TTF REQUIRED)
find_package(SDL2IMAGE REQUIRED)
find_library(I2C_LIBRARY i2c REQUIRED)
if ("${I2C_LIBRARY}" STREQUAL "I2C_LIBRARY-NOTFOUND")
message(FATAL_ERROR "libi2c not found!")
else()
message("libi2c found at ${I2C_LIBRARY}")
endif()
include(FetchContent)
@ -28,7 +34,17 @@ endif()
if (NOT DEFINED fmt_INCLUDE_DIR)
set(fmt_INCLUDE_DIR ${fmt_SOURCE_DIR}/${FMT_INC_DIR})
endif()
set(BUILD_SHARED_LIBS ${lhotse_orig_BUILD_SHARED_LIBS})
set(BUILD_SHARED_LIBS ${stw_display_orig_BUILD_SHARED_LIBS})
# spdlog
set(CMAKE_POSITION_INDEPENDENT_CODE true)
set(SPDLOG_FMT_EXTERNAL_HO true)
FetchContent_Declare(
spdlog
GIT_REPOSITORY https://github.com/gabime/spdlog.git
GIT_TAG v1.9.2
)
FetchContent_MakeAvailable(spdlog)
# }}}
@ -38,10 +54,15 @@ add_executable(
stw-display
src/main.cpp
src/App.cpp
src/AppState.cpp
src/View.cpp
src/MissionSelect.cpp
src/AMI.cpp
src/widgets.cpp
src/util.cpp
src/DriverView.cpp
src/BaseDataSource.cpp
src/DemoDataSource.cpp
)
target_include_directories(
stw-display PUBLIC
@ -57,5 +78,7 @@ target_link_libraries(
${SDL2_LIBRARIES}
${SDL2TTF_LIBRARY}
${SDL2IMAGE_LIBRARY}
fmt)
${I2C_LIBRARY}
fmt
spdlog::spdlog)
add_dependencies(stw-display fmt)

View File

@ -1,26 +1,27 @@
#pragma once
#include "AppState.h"
#include "View.h"
#include "widgets.h"
#include <SDL2/SDL.h>
#include <SDL2/SDL_render.h>
#include <SDL2/SDL_ttf.h>
#include <memory>
#include <unordered_map>
#include <vector>
class AMI final : public View {
public:
AMI(SDL_Renderer* renderer);
~AMI();
void draw() override;
void draw(const AppState& state) override;
private:
TTF_Font* avenir;
TTF_Font* font_medium;
TTF_Font* font_large;
std::vector<Widget*> widgets;
ImageWidget ft_logo;
TextWidget choose;
ListWidget missions_widget;
std::vector<std::unique_ptr<Widget>> missions;
std::unique_ptr<TextWidget> header;
std::unordered_map<Mission, std::unique_ptr<TextWidget>> missions;
};

View File

@ -1,20 +1,28 @@
#pragma once
#include "AMI.h"
#include "AppState.h"
#include "DriverView.h"
#include "MissionSelect.h"
#include "defines.h"
#include <SDL2/SDL.h>
#include <memory>
enum class AppView { AMI, DRIVER, TESTING };
class SDLManager {
public:
SDLManager();
~SDLManager();
};
#ifndef NDEBUG
class KeyboardHandler {
public:
void handle_keyup(const SDL_Event& ev, AppState& state);
};
#endif // !NDEBUG
class App {
public:
App();
@ -24,6 +32,7 @@ public:
private:
void init_sdl();
void init_state();
void handle_events();
void render();
@ -33,11 +42,17 @@ private:
// others and its destructor is called after the others.
SDLManager sdl_manager;
std::unique_ptr<AppState> state;
std::unique_ptr<MissionSelect> mission_select;
std::unique_ptr<AMI> ami;
std::unique_ptr<DriverView> driver_view;
bool running;
AppView view;
SDL_Window* window;
SDL_Renderer* renderer;
#ifndef NDEBUG
KeyboardHandler keyboard_handler;
#endif // !NDEBUG
};

55
include/AppState.h Normal file
View File

@ -0,0 +1,55 @@
#pragma once
#include "DemoDataSource.h"
#include <fmt/ostream.h>
#include <optional>
#include <ostream>
#include <string>
constexpr int I2C_SLAVE_ADDR = 0x20;
constexpr int I2C_POLL_COMMAND = 0x00;
enum class AppView { MISSION_SELECT, AMI, DRIVER, TESTING };
std::ostream& operator<<(std::ostream& os, AppView view);
enum class Mission {
NONE,
ACCELERATION,
SKIDPAD,
AUTOCROSS,
TRACKDRIVE,
EBS_TEST,
INSPECTION,
MANUAL
};
std::ostream& operator<<(std::ostream& os, Mission mission);
class AppState {
public:
AppState(const std::string& i2c_dev_path);
~AppState();
void poll();
AppView get_view() const;
Mission get_mission() const;
DemoDataSource get_data_source() const;
private:
void unmarshal_mission_select(uint8_t* data);
void unmarshal_ami(uint8_t* data);
// This is optional so that we can still run the code on a PC without I2C
std::optional<int> i2c_dev_file;
AppView view;
Mission mission;
#ifndef NDEBUG
friend class KeyboardHandler;
DemoDataSource data_source;
#endif // !NDEBUG
};
extern AppState app_state;

34
include/BaseDataSource.h Normal file
View File

@ -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;
};

13
include/DemoDataSource.h Normal file
View File

@ -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();
};

30
include/DriverView.h Normal file
View File

@ -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<TextWidget> speed_widget;
std::unique_ptr<TextWidget> speed_hint_widget;
std::unique_ptr<TextWidget> brake_balance_widget;
std::unique_ptr<TextWidget> brake_balance_hint_widget;
std::unique_ptr<TextWidget> general_info_widget;
std::unique_ptr<TextWidget> focus_widget;
std::unique_ptr<TextWidget> focus_hint_widget;
};

31
include/MissionSelect.h Normal file
View File

@ -0,0 +1,31 @@
#pragma once
#include "AppState.h"
#include "View.h"
#include "events.h"
#include "widgets.h"
#include <SDL2/SDL.h>
#include <memory>
#include <queue>
#include <vector>
class MissionSelect final : public View {
public:
MissionSelect(SDL_Renderer* renderer);
~MissionSelect();
void draw(const AppState& state) override;
private:
TTF_Font* avenir;
TTF_Font* chinat;
std::vector<Widget*> widgets;
std::unique_ptr<ImageWidget> ft_logo;
std::unique_ptr<TextWidget> choose;
std::unique_ptr<ListWidget> missions_widget;
std::vector<std::unique_ptr<Widget>> missions;
};

View File

@ -1,13 +1,18 @@
#pragma once
#include "AppState.h"
#include "events.h"
#include <SDL2/SDL.h>
#include <queue>
class View {
public:
View(SDL_Renderer* renderer);
virtual ~View();
virtual void draw() = 0;
virtual void draw(const AppState& state) = 0;
protected:
SDL_Renderer* renderer;

3
include/events.h Normal file
View File

@ -0,0 +1,3 @@
#pragma once
enum class Event { Next, Prev, Confirm };

View File

@ -3,17 +3,30 @@
#include <SDL2/SDL.h>
#include <SDL2/SDL_ttf.h>
#include <fmt/ostream.h>
#include <optional>
#include <ostream>
#include <string>
#include <vector>
enum class Alignment { LEFT, RIGHT, CENTER };
enum class Alignment {
START,
END,
CENTER,
LEFT = START,
TOP = START,
RIGHT = END,
BOTTOM = END
};
std::ostream& operator<<(std::ostream& os, Alignment align);
struct PositionInfo {
PositionInfo();
int x;
int y;
Alignment align;
Alignment align_h;
Alignment align_v;
};
class Widget {
@ -24,7 +37,7 @@ public:
virtual void set_width(int width, bool preserve_aspect_ratio = true);
virtual void set_height(int height, bool preserve_aspect_ratio = true);
virtual void set_position(int x, int y);
virtual void set_alignment(Alignment align);
virtual void set_alignment(Alignment align_h, Alignment align_v);
int get_width();
int get_height();
@ -33,7 +46,7 @@ public:
virtual void draw() = 0;
protected:
void recalculate_rect();
void recalculate_pos();
SDL_Renderer* renderer;
SDL_Rect rect;
@ -66,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);
};
@ -85,10 +99,17 @@ public:
virtual void draw() override;
void select_next();
void select_prev();
void select(size_t n);
size_t get_selection();
protected:
int element_height;
Alignment element_alignment;
size_t selection;
std::vector<Widget*> elements;
void place_element(Widget* element, int index);

BIN
resources/CHINAT.ttf Normal file

Binary file not shown.

View File

@ -1,56 +1,64 @@
#include "AMI.h"
#include "AppState.h"
#include "SDL_render.h"
#include "SDL_ttf.h"
#include "defines.h"
#include "util.h"
#include "widgets.h"
#include <array>
#include <memory>
constexpr const char* FT_LOGO_PATH = "resources/Fasttube_Logo-white.bmp";
constexpr const char* AVENIR_FONT_PATH = "resources/Avenir-Book.ttf";
constexpr int AVENIR_PTS = 20;
constexpr int FT_LOGO_HEIGHT = 40;
constexpr int GAP = 5;
constexpr int CHOOSE_Y = FT_LOGO_HEIGHT + GAP;
constexpr int MISSION_HEIGHT = 30;
constexpr std::array MISSIONS = {"ACCELERATION", "SKIDPAD", "AUTOCROSS",
"TRACKDRIVE", "EBS TEST", "INSPECTION",
"MANUAL DRIVING"};
constexpr const char* CHINAT_FONT_PATH = "resources/CHINAT.ttf";
constexpr int CHINAT_MEDIUM_PTS = 25;
constexpr int CHINAT_LARGE_PTS = 40;
constexpr int HEADER_Y = 10;
AMI::AMI(SDL_Renderer* renderer)
: View{renderer}, avenir{util::load_font(AVENIR_FONT_PATH, AVENIR_PTS)},
ft_logo{renderer, FT_LOGO_PATH}, choose{renderer, avenir,
"Choose a mission:"},
missions_widget{renderer, MISSION_HEIGHT, Alignment::CENTER} {
ft_logo.set_height(FT_LOGO_HEIGHT);
ft_logo.set_position(SCREEN_WIDTH / 2, 0);
ft_logo.set_alignment(Alignment::CENTER);
widgets.push_back(&ft_logo);
: View{renderer}, font_medium{util::load_font(CHINAT_FONT_PATH,
CHINAT_MEDIUM_PTS)},
font_large{util::load_font(CHINAT_FONT_PATH, CHINAT_LARGE_PTS)} {
header =
std::make_unique<TextWidget>(renderer, font_medium, "Current mission:");
header->set_position(SCREEN_WIDTH / 2, HEADER_Y);
header->set_alignment(Alignment::CENTER, Alignment::TOP);
choose.set_position(SCREEN_WIDTH / 2, CHOOSE_Y);
choose.set_alignment(Alignment::CENTER);
widgets.push_back(&choose);
int choose_height = choose.get_height();
int missions_y = CHOOSE_Y + choose_height + GAP;
int missions_height = SCREEN_HEIGHT - CHOOSE_Y - choose_height - 1;
missions_widget.set_position(0, missions_y);
missions_widget.set_width(SCREEN_WIDTH, false);
missions_widget.set_height(missions_height, false);
for (const auto mission : MISSIONS) {
missions.push_back(std::make_unique<TextWidget>(renderer, avenir, mission));
missions.emplace(Mission::NONE,
std::make_unique<TextWidget>(renderer, font_large,
"NO MISSION SELECTED"));
missions.emplace(
Mission::ACCELERATION,
std::make_unique<TextWidget>(renderer, font_large, "ACCELERATION"));
missions.emplace(Mission::SKIDPAD, std::make_unique<TextWidget>(
renderer, font_large, "SKIDPAD"));
missions.emplace(Mission::AUTOCROSS, std::make_unique<TextWidget>(
renderer, font_large, "AUTOCROSS"));
missions.emplace(
Mission::TRACKDRIVE,
std::make_unique<TextWidget>(renderer, font_large, "TRACKDRIVE"));
missions.emplace(Mission::EBS_TEST, std::make_unique<TextWidget>(
renderer, font_large, "EBS TEST"));
missions.emplace(
Mission::INSPECTION,
std::make_unique<TextWidget>(renderer, font_large, "INSPECTION"));
missions.emplace(
Mission::MANUAL,
std::make_unique<TextWidget>(renderer, font_large, "MANUAL DRIVING"));
for (auto& [mission, widget] : missions) {
widget->set_position(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2);
widget->set_alignment(Alignment::CENTER, Alignment::CENTER);
}
for (const auto& mission : missions) {
missions_widget.add_element(mission.get());
}
widgets.push_back(&missions_widget);
}
void AMI::draw() {
AMI::~AMI() {
TTF_CloseFont(font_medium);
TTF_CloseFont(font_large);
}
void AMI::draw(const AppState& state) {
SDL_SetRenderDrawColor(renderer, 0x00, 0x00, 0x00, 0xFF);
SDL_RenderClear(renderer);
for (const auto& widget : widgets) {
widget->draw();
}
header->draw();
missions[state.get_mission()]->draw();
}

View File

@ -1,14 +1,31 @@
#include "App.h"
#include "AMI.h"
#include "AppState.h"
#include "DemoDataSource.h"
#include "DriverView.h"
#include "MissionSelect.h"
#include "events.h"
#include <SDL2/SDL.h>
#include <SDL2/SDL_events.h>
#include <SDL2/SDL_image.h>
#include <fmt/format.h>
#include <SDL2/SDL_keycode.h>
#include <SDL2/SDL_render.h>
#include <SDL2/SDL_timer.h>
#include <fmt/format.h>
#include <spdlog/spdlog.h>
#include <iostream>
#include <memory>
#include <queue>
#include <stdexcept>
App::App() : view{AppView::AMI} { init_sdl(); }
App::App() {
init_sdl();
init_state();
}
App::~App() {
// Destroy window
@ -26,21 +43,52 @@ void App::init_sdl() {
throw std::runtime_error(
fmt::format("Couldn't create window: {}", SDL_GetError()));
}
renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
// Software renderer is *MUCH* (~50x speedup) faster than hardware accelerated
// renderer on a Pi Zero.
renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_SOFTWARE);
if (renderer == nullptr) {
throw std::runtime_error(
fmt::format("Couldn't create renderer: {}", SDL_GetError()));
}
}
void App::init_state() {
try {
state = std::make_unique<AppState>("/dev/i2c-3");
} catch (const std::runtime_error& e) {
spdlog::error("Couldn't create AppState: {}", e.what());
}
}
int App::run() {
mission_select = std::make_unique<MissionSelect>(renderer);
ami = std::make_unique<AMI>(renderer);
driver_view = std::make_unique<DriverView>(renderer);
#ifndef NDEBUG
std::cout << "Change brake balance with W (+) and S (-)" << std::endl;
#endif
running = true;
uint32_t second_start = SDL_GetTicks();
unsigned frames = 0;
while (running) {
uint32_t now = SDL_GetTicks();
if (now - second_start > 5000) {
spdlog::info("{} FPS", frames / 5);
second_start = now;
frames = 0;
}
if (state != nullptr) {
state->poll();
}
handle_events();
render();
frames++;
SDL_Delay(1);
}
@ -48,21 +96,33 @@ int App::run() {
}
void App::handle_events() {
std::queue<Event> events;
SDL_Event e;
while (SDL_PollEvent(&e) != 0) {
if (e.type == SDL_QUIT) {
running = false;
#ifndef NDEBUG
} else if (e.type == SDL_KEYUP) {
keyboard_handler.handle_keyup(e, *state);
#endif // !NDEBUG
}
}
}
void App::render() {
AppView view = state->get_view();
switch (view) {
case AppView::MISSION_SELECT:
mission_select->draw(*state);
break;
case AppView::AMI:
ami->draw();
ami->draw(*state);
break;
case AppView::DRIVER:
driver_view->draw(*state);
break;
default:
throw std::runtime_error(fmt::format("Unknown view: {}", (int)view));
throw std::runtime_error(fmt::format("Unknown view: {}", view));
}
SDL_RenderPresent(renderer);
}
@ -88,3 +148,52 @@ SDLManager::~SDLManager() {
TTF_Quit();
SDL_Quit();
}
/*
Handle key presses
*/
void KeyboardHandler::handle_keyup(const SDL_Event& ev, AppState& state) {
auto pressed_key = ev.key.keysym.sym;
// we are in the first screen (mission select)
if (state.view == AppView::MISSION_SELECT) {
// handle change selected mission
if (pressed_key == SDLK_DOWN || pressed_key == SDLK_RIGHT) {
auto new_mission = static_cast<int>(state.mission) + 1;
if (new_mission > 7) {
new_mission = 1;
}
state.mission = static_cast<Mission>(new_mission);
} else if (pressed_key == SDLK_UP || pressed_key == SDLK_LEFT) {
auto new_mission = static_cast<int>(state.mission) - 1;
if (new_mission < 1) {
new_mission = 7;
}
state.mission = static_cast<Mission>(new_mission);
// commit mission selection
} else if (pressed_key == SDLK_RETURN) {
if (state.mission == Mission::MANUAL) {
state.view = AppView::DRIVER;
} else {
state.view = AppView::AMI;
}
}
}
// go back to mission select
if (state.view == AppView::AMI || state.view == AppView::DRIVER) {
if (pressed_key == SDLK_ESCAPE) {
state.view = AppView::MISSION_SELECT;
}
}
// handle mock brake balance change
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();
}
}

118
src/AppState.cpp Normal file
View File

@ -0,0 +1,118 @@
#include "AppState.h"
#include <fmt/format.h>
#include <spdlog/spdlog.h>
#include <fcntl.h>
#include <optional>
extern "C" {
#include <i2c/smbus.h>
}
#include <linux/i2c-dev.h>
#include <sys/ioctl.h>
#include <cstdint>
#include <stdexcept>
#include <unistd.h>
std::ostream& operator<<(std::ostream& os, AppView view) {
switch (view) {
case AppView::MISSION_SELECT:
return os << "MISSION_SELECT";
case AppView::AMI:
return os << "AMI";
case AppView::DRIVER:
return os << "DRIVER";
case AppView::TESTING:
return os << "TESTING";
default:
return os << "UNKNOWN_VIEW[" << static_cast<int>(view) << "]";
}
}
std::ostream& operator<<(std::ostream& os, Mission mission) {
switch (mission) {
case Mission::NONE:
return os << "NONE";
case Mission::ACCELERATION:
return os << "ACCELERATION";
case Mission::SKIDPAD:
return os << "SKIDPAD";
case Mission::AUTOCROSS:
return os << "AUTOCROSS";
case Mission::TRACKDRIVE:
return os << "TRACKDRIVE";
case Mission::EBS_TEST:
return os << "EBS_TEST";
case Mission::INSPECTION:
return os << "INSPECTION";
case Mission::MANUAL:
return os << "MANUAL";
default:
return os << "UNKNOWN_MISSION[" << static_cast<int>(mission) << "]";
}
}
AppState::AppState(const std::string& i2c_dev_path)
: view{AppView::MISSION_SELECT}, mission{Mission::ACCELERATION} {
i2c_dev_file = open(i2c_dev_path.c_str(), O_RDWR);
if (i2c_dev_file < 0) {
spdlog::error("Couldn't open I2C device: {}", strerror(errno));
i2c_dev_file = std::nullopt;
return;
}
if (ioctl(*i2c_dev_file, I2C_SLAVE, I2C_SLAVE_ADDR) < 0) {
spdlog::error("Couldn't set I2C slave address: {}", strerror(errno));
close(*i2c_dev_file);
i2c_dev_file = std::nullopt;
}
}
AppState::~AppState() {
if (i2c_dev_file) {
close(*i2c_dev_file);
}
}
void AppState::poll() {
data_source.poll();
if (!i2c_dev_file) {
return;
}
uint8_t data[32];
int count = i2c_smbus_read_block_data(*i2c_dev_file, I2C_POLL_COMMAND, data);
if (count < 0) {
throw std::runtime_error(
fmt::format("Couldn't poll I2C slave: {}", strerror(errno)));
}
view = static_cast<AppView>(data[0]);
spdlog::info("View after poll: {}", view);
switch (view) {
case AppView::MISSION_SELECT:
unmarshal_mission_select(data);
break;
case AppView::AMI:
unmarshal_ami(data);
break;
default:
throw std::runtime_error(fmt::format("Unknown view: {}", view));
}
}
AppView AppState::get_view() const { return view; }
Mission AppState::get_mission() const { return mission; }
void AppState::unmarshal_mission_select(uint8_t* data) {
mission = static_cast<Mission>(data[1]);
spdlog::info("Mission after poll: {}", mission);
}
void AppState::unmarshal_ami(uint8_t* data) {
mission = static_cast<Mission>(data[1]);
spdlog::info("Mission after poll: {}", mission);
}
DemoDataSource AppState::get_data_source() const { return data_source; }

35
src/BaseDataSource.cpp Normal file
View File

@ -0,0 +1,35 @@
#include "BaseDataSource.h"
#include <SDL2/SDL.h>
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;
}

33
src/DemoDataSource.cpp Normal file
View File

@ -0,0 +1,33 @@
#include "DemoDataSource.h"
#include <cmath>
#include <iostream>
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);
}

131
src/DriverView.cpp Normal file
View File

@ -0,0 +1,131 @@
#include "DriverView.h"
#include "DemoDataSource.h"
#include "defines.h"
#include <fmt/format.h>
#include <iostream>
#define LARGE_FONT_SIZE_POINTS 170
#define GIANT_FONT_SIZE_POINTS 250
#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<TextWidget>(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<TextWidget>(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<TextWidget>(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<TextWidget>(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<TextWidget>(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<TextWidget>(renderer, font_giant, "");
focus_widget->set_position(120, 50);
focus_widget->set_alignment(Alignment::LEFT, Alignment::TOP);
focus_hint_widget = std::make_unique<TextWidget>(renderer, font_medium, "");
focus_hint_widget->set_position(120, 30);
// focus_hint_widget->set_alignment(Alignment::CENTER, Alignment::CENTER);
}
DriverView::~DriverView() {
TTF_CloseFont(font_medium);
TTF_CloseFont(font_large);
TTF_CloseFont(font_detail);
TTF_CloseFont(font_tiny);
TTF_CloseFont(font_giant);
}
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();
}

74
src/MissionSelect.cpp Normal file
View File

@ -0,0 +1,74 @@
#include "MissionSelect.h"
#include "AppState.h"
#include "DriverView.h"
#include "defines.h"
#include "events.h"
#include "util.h"
#include "widgets.h"
#include <fmt/format.h>
#include <array>
#include <iostream>
#include <memory>
constexpr const char* FT_LOGO_PATH = "resources/Fasttube_Logo-white.bmp";
constexpr const char* AVENIR_FONT_PATH = "resources/Avenir-Book.ttf";
constexpr int AVENIR_PTS = 20;
constexpr const char* CHINAT_FONT_PATH = "resources/CHINAT.ttf";
constexpr int CHINAT_PTS = 24;
constexpr int FT_LOGO_HEIGHT = 40;
constexpr int GAP = 5;
constexpr int CHOOSE_Y = FT_LOGO_HEIGHT + GAP;
constexpr std::array MISSIONS = {"ACCELERATION", "SKIDPAD", "AUTOCROSS",
"TRACKDRIVE", "EBS TEST", "INSPECTION",
"MANUAL DRIVING"};
MissionSelect::MissionSelect(SDL_Renderer* renderer)
: View{renderer}, avenir{util::load_font(AVENIR_FONT_PATH, AVENIR_PTS)},
chinat{util::load_font(CHINAT_FONT_PATH, CHINAT_PTS)} {
ft_logo = std::make_unique<ImageWidget>(renderer, FT_LOGO_PATH);
ft_logo->set_height(FT_LOGO_HEIGHT);
ft_logo->set_position(SCREEN_WIDTH / 2, 0);
ft_logo->set_alignment(Alignment::CENTER, Alignment::TOP);
widgets.push_back(ft_logo.get());
choose = std::make_unique<TextWidget>(renderer, avenir, "Choose a mission:");
choose->set_position(SCREEN_WIDTH / 2, CHOOSE_Y);
choose->set_alignment(Alignment::CENTER, Alignment::TOP);
widgets.push_back(choose.get());
for (const auto mission : MISSIONS) {
missions.push_back(std::make_unique<TextWidget>(renderer, chinat, mission));
}
int choose_height = choose->get_height();
int missions_y = CHOOSE_Y + choose_height + GAP;
int missions_height = SCREEN_HEIGHT - CHOOSE_Y - choose_height - 1;
missions_widget = std::make_unique<ListWidget>(
renderer, missions[0]->get_height(), Alignment::CENTER);
missions_widget->set_position(0, missions_y);
missions_widget->set_width(SCREEN_WIDTH, false);
missions_widget->set_height(missions_height, false);
for (const auto& mission : missions) {
missions_widget->add_element(mission.get());
}
widgets.push_back(missions_widget.get());
}
MissionSelect::~MissionSelect() {
TTF_CloseFont(avenir);
TTF_CloseFont(chinat);
}
void MissionSelect::draw(const AppState& state) {
size_t n = static_cast<int>(state.get_mission()) - 1;
missions_widget->select(n);
SDL_SetRenderDrawColor(renderer, 0x00, 0x00, 0x00, 0xFF);
SDL_RenderClear(renderer);
for (const auto& widget : widgets) {
widget->draw();
}
}

View File

@ -5,11 +5,37 @@
#include <SDL2/SDL.h>
#include <SDL2/SDL_image.h>
#include <fmt/format.h>
#include <cstdint>
#include <stdexcept>
PositionInfo::PositionInfo() : x{0}, y{0}, align{Alignment::LEFT} {}
constexpr uint8_t LIST_SELECTION_BG_R = 0x77;
constexpr uint8_t LIST_SELECTION_BG_G = 0x33;
constexpr uint8_t LIST_SELECTION_BG_B = 0x33;
constexpr uint8_t LIST_OTHER_BG_R = 0x22;
constexpr uint8_t LIST_OTHER_BG_G = 0x22;
constexpr uint8_t LIST_OTHER_BG_B = 0x22;
constexpr uint8_t LIST_NORMAL_BG_R = 0x00;
constexpr uint8_t LIST_NORMAL_BG_G = 0x00;
constexpr uint8_t LIST_NORMAL_BG_B = 0x00;
std::ostream& operator<<(std::ostream& os, Alignment align) {
switch (align) {
case Alignment::START:
return os << "START";
case Alignment::CENTER:
return os << "CENTER";
case Alignment::END:
return os << "END";
default:
return os << "UNKNOWN ALIGNMENT[" << static_cast<int>(align) << "]";
}
}
PositionInfo::PositionInfo()
: x{0}, y{0}, align_h{Alignment::LEFT}, align_v{Alignment::LEFT} {}
Widget::Widget(SDL_Renderer* renderer) : renderer{renderer}, rect{0, 0, 0, 0} {}
@ -21,12 +47,14 @@ void Widget::set_width(int width, bool preserve_aspect_ratio) {
rect.h = round(rect.h * scale);
}
rect.w = width;
recalculate_pos();
}
void Widget::set_height(int height, bool preserve_aspect_ratio) {
if (preserve_aspect_ratio) {
float scale = ((float)height) / rect.h;
rect.w = round(rect.w * scale);
recalculate_pos();
}
rect.h = height;
}
@ -34,12 +62,13 @@ void Widget::set_height(int height, bool preserve_aspect_ratio) {
void Widget::set_position(int x, int y) {
pos.x = x;
pos.y = y;
recalculate_rect();
recalculate_pos();
}
void Widget::set_alignment(Alignment align) {
pos.align = align;
recalculate_rect();
void Widget::set_alignment(Alignment align_h, Alignment align_v) {
pos.align_h = align_h;
pos.align_v = align_v;
recalculate_pos();
}
int Widget::get_width() { return rect.w; }
@ -48,22 +77,35 @@ int Widget::get_height() { return rect.h; }
const PositionInfo& Widget::get_position() { return pos; }
void Widget::recalculate_rect() {
void Widget::recalculate_pos() {
int x = pos.x;
int y = pos.y;
switch (pos.align) {
case Alignment::LEFT:
switch (pos.align_h) {
case Alignment::START:
break;
case Alignment::RIGHT:
case Alignment::END:
x -= rect.w;
break;
case Alignment::CENTER:
x -= rect.w / 2;
break;
default:
throw std::runtime_error(
fmt::format("Unknown alignment: {}", (int)pos.align));
throw std::runtime_error(fmt::format("Unknown alignment: {}", pos.align_h));
}
switch (pos.align_v) {
case Alignment::START:
break;
case Alignment::END:
y -= rect.h;
break;
case Alignment::CENTER:
y -= rect.h / 2;
break;
default:
throw std::runtime_error(fmt::format("Unknown alignment: {}", pos.align_v));
}
rect.x = x;
rect.y = y;
}
@ -125,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()));
@ -139,10 +186,12 @@ 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},
element_alignment{element_alignment} {}
element_alignment{element_alignment}, selection{0} {}
ListWidget::~ListWidget() {}
@ -161,12 +210,19 @@ void ListWidget::draw() {
// Since the elements are sorted, we can stop the loop now.
break;
}
if (i % 2 == 1) {
SDL_SetRenderDrawColor(renderer, 0x11, 0x11, 0x11, 0xFF);
if (i == selection) {
SDL_SetRenderDrawColor(renderer, LIST_SELECTION_BG_R, LIST_SELECTION_BG_G,
LIST_SELECTION_BG_B, 0xFF);
} else if (i % 2 == 1) {
SDL_SetRenderDrawColor(renderer, LIST_OTHER_BG_R, LIST_OTHER_BG_G,
LIST_OTHER_BG_B, 0xFF);
} else {
SDL_SetRenderDrawColor(renderer, LIST_NORMAL_BG_R, LIST_NORMAL_BG_G,
LIST_NORMAL_BG_B, 0xFF);
}
SDL_Rect fill_rect = {
.x = pos.x, .y = element_pos.y, .w = rect.w, .h = element_height};
SDL_RenderFillRect(renderer, &fill_rect);
}
element->draw();
if (i != elements.size() - 1) {
int border_y = element_pos.y + element->get_height();
@ -176,6 +232,37 @@ void ListWidget::draw() {
}
}
void ListWidget::select_next() {
size_t n = elements.size();
if (n == 0) {
return;
}
selection = (selection + 1) % n;
}
void ListWidget::select_prev() {
size_t n = elements.size();
if (n == 0) {
return;
}
if (selection == 0) {
selection = n - 1;
} else {
selection = selection - 1;
}
}
void ListWidget::select(size_t n) {
if (n > elements.size()) {
throw std::runtime_error(fmt::format(
"Tried to select element {}, but there are only {} elements!", n,
elements.size()));
}
selection = n;
}
size_t ListWidget::get_selection() { return selection; }
void ListWidget::place_element(Widget* element, int index) {
int x = rect.x;
switch (element_alignment) {
@ -189,11 +276,11 @@ void ListWidget::place_element(Widget* element, int index) {
break;
default:
throw std::runtime_error(
fmt::format("Unknown alignment: {}", (int)pos.align));
fmt::format("Unknown alignment: {}", element_alignment));
}
// Additional pixel for border
int y = rect.y + index * (element_height + 1);
element->set_position(x, y);
element->set_alignment(element_alignment);
element->set_alignment(element_alignment, Alignment::TOP);
element->set_height(element_height);
}