Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ The project follows Semantic Versioning once the first firmware release is tagge
- vertical G alongside the lateral and longitudinal pair, for kerbs and compressions
- gyroscope zero-rate offset measured at rest and removed, 4.4 dps on this board
- device settings persisted in NVS, so Mode survives a reboot
- Review presents a session as a carousel, one value per screen, in the same visual
language as the configuration menus
- a caption line on carousel entries, for when a label alone does not say what it is
- session records kept on the card, so a driver's sessions outlive a power cycle
- a finished session leaves a record: duration, overrun and peak G on every axis, shown in
Review
Expand Down
18 changes: 16 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,16 @@ TIME_ROLLER_TEST_BINARY := build/host/time_roller_test
SESSION_TRIGGER_TEST_BINARY := build/host/session_trigger_test
SUMMARY_STORE_TEST_BINARY := build/host/summary_store_test
SUMMARY_FILE_TEST_BINARY := build/host/summary_file_test
REVIEW_CARDS_TEST_BINARY := build/host/review_cards_test
GATE_CAPTURE_TEST_BINARY := build/host/gate_capture_test
GATE_SESSION_AUTOMATION_TEST_BINARY := build/host/gate_session_automation_test
SIMULATOR_BUILD_DIR ?= build/simulator
SIMULATOR_IMAGE ?= track-session-timer-simulator:lvgl-9.5.0
CMAKE ?= cmake

.PHONY: check test track-validate uk-track-pack track-pack-test simulator-track-catalog-test simulator-fixture-validate repo-check host-test simulator-model-test session-state-test settings-test settings-editor-test projection-test intersection-test crossing-validation-test crossing-time-test lap-state-machine-test timing-engine-test gate-event-engine-test track-definition-test track-capture-test track-matching-test track-selection-test log-format-test async-logger-test session-review-test diagnostics-test active-session-test display-policy-test imu-meter-test rest-session-test track-catalog-test shell-navigation-test device-mode-test imu-calibration-test session-urgency-test time-roller-test session-trigger-test summary-store-test summary-file-test gate-capture-test gate-session-automation-test ui-foundation-test navigation-test simulator-configure simulator-build simulator-test simulator-run simulator-container-image simulator-container-test simulator-clean firmware-build firmware-container-build firmware-container-flash firmware-container-monitor firmware-container-flash-monitor firmware-container-erase firmware-device-info firmware-clean issue-preview label-preview
.PHONY: check test track-validate uk-track-pack track-pack-test simulator-track-catalog-test simulator-fixture-validate repo-check host-test simulator-model-test session-state-test settings-test settings-editor-test projection-test intersection-test crossing-validation-test crossing-time-test lap-state-machine-test timing-engine-test gate-event-engine-test track-definition-test track-capture-test track-matching-test track-selection-test log-format-test async-logger-test session-review-test diagnostics-test active-session-test display-policy-test imu-meter-test rest-session-test track-catalog-test shell-navigation-test device-mode-test imu-calibration-test session-urgency-test time-roller-test session-trigger-test summary-store-test summary-file-test review-cards-test gate-capture-test gate-session-automation-test ui-foundation-test navigation-test simulator-configure simulator-build simulator-test simulator-run simulator-container-image simulator-container-test simulator-clean firmware-build firmware-container-build firmware-container-flash firmware-container-monitor firmware-container-flash-monitor firmware-container-erase firmware-device-info firmware-clean issue-preview label-preview

check: test track-validate track-pack-test simulator-track-catalog-test simulator-fixture-validate repo-check host-test simulator-model-test session-state-test settings-test settings-editor-test projection-test intersection-test crossing-validation-test crossing-time-test lap-state-machine-test timing-engine-test gate-event-engine-test track-definition-test track-capture-test track-matching-test track-selection-test log-format-test async-logger-test session-review-test diagnostics-test active-session-test display-policy-test imu-meter-test rest-session-test track-catalog-test shell-navigation-test device-mode-test imu-calibration-test session-urgency-test time-roller-test session-trigger-test summary-store-test summary-file-test gate-capture-test gate-session-automation-test ui-foundation-test navigation-test
check: test track-validate track-pack-test simulator-track-catalog-test simulator-fixture-validate repo-check host-test simulator-model-test session-state-test settings-test settings-editor-test projection-test intersection-test crossing-validation-test crossing-time-test lap-state-machine-test timing-engine-test gate-event-engine-test track-definition-test track-capture-test track-matching-test track-selection-test log-format-test async-logger-test session-review-test diagnostics-test active-session-test display-policy-test imu-meter-test rest-session-test track-catalog-test shell-navigation-test device-mode-test imu-calibration-test session-urgency-test time-roller-test session-trigger-test summary-store-test summary-file-test review-cards-test gate-capture-test gate-session-automation-test ui-foundation-test navigation-test

test:
$(PYTHON) -B -m unittest discover -s tests -p 'test_*.py'
Expand Down Expand Up @@ -426,6 +427,19 @@ gate-session-automation-test:
-o $(GATE_SESSION_AUTOMATION_TEST_BINARY)
$(GATE_SESSION_AUTOMATION_TEST_BINARY)

review-cards-test:
mkdir -p build/host
$(CXX) -std=c++17 -Wall -Wextra -Werror -pedantic \
-Ifirmware/components/domain/include \
-Ifirmware/components/settings/include \
-Ifirmware/components/logger/include \
-Ifirmware/components/ui/include \
firmware/components/settings/component.cpp \
firmware/components/logger/formats.cpp \
firmware/components/ui/review_cards.cpp \
tests/cpp/test_review_cards.cpp -o $(REVIEW_CARDS_TEST_BINARY)
$(REVIEW_CARDS_TEST_BINARY)

summary-file-test:
mkdir -p build/host
$(CXX) -std=c++17 -Wall -Wextra -Werror -pedantic \
Expand Down
2 changes: 1 addition & 1 deletion firmware/components/ui/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ idf_component_register(
SRCS "component.cpp" "active_session.cpp" "input.cpp" "shell_navigation.cpp" "device_mode.cpp" "value_picker.cpp" "gesture_input.cpp" "carousel_screen.cpp" "time_roller.cpp" "time_roller_screen.cpp" "trackday_screen.cpp" "timer_font.cpp" "diagnostics.cpp" "diagnostics_screen.cpp"
"display_policy.cpp" "foundation.cpp" "gate_capture.cpp" "gate_capture_screen.cpp" "g_meter_screen.cpp" "g_radar_screen.cpp" "imu_meter.cpp"
"lvgl_visual_system.cpp" "navigation.cpp"
"presenter.cpp" "ready_screen.cpp" "rest_session.cpp" "session_trigger.cpp" "session_review.cpp" "session_review_screen.cpp"
"presenter.cpp" "ready_screen.cpp" "rest_session.cpp" "review_cards.cpp" "session_trigger.cpp" "session_review.cpp" "session_review_screen.cpp"
"settings_editor.cpp" "settings_screen.cpp"
"setup_menu_screen.cpp" "track_selection.cpp" "track_selection_screen.cpp"
INCLUDE_DIRS "include"
Expand Down
7 changes: 7 additions & 0 deletions firmware/components/ui/carousel_screen.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ CarouselScreen::CarouselScreen(lv_obj_t* const root, const InputCallback callbac
lv_obj_set_style_text_font(label_, &lv_font_montserrat_36, 0);
lv_obj_align(label_, LV_ALIGN_CENTER, 0, kLabelOffsetY);

// Under the label, in the roller's arrangement: the value is what you read, and the
// name beneath tells you how to read it.
caption_ = create_label(root_, Typography::body, kMutedRgb);
lv_obj_align(caption_, LV_ALIGN_CENTER, 0, kLabelOffsetY + 44);

hint_ = create_label(root_, Typography::caption, kMutedRgb);
lv_label_set_text(hint_, "swipe to change - press to select - swipe down to go back");
lv_obj_align(hint_, LV_ALIGN_BOTTOM_MID, 0, -12);
Expand Down Expand Up @@ -142,12 +147,14 @@ void CarouselScreen::refresh() noexcept
if (count_ == 0) {
lv_label_set_text(icon_, "");
lv_label_set_text(label_, "");
lv_label_set_text(caption_, "");
return;
}
const auto& entry = entries_[index_];
lv_label_set_text(icon_, entry.icon == nullptr ? "" : entry.icon);
lv_obj_set_style_text_color(icon_, lv_color_hex(entry.icon_rgb), 0);
lv_label_set_text(label_, entry.label == nullptr ? "" : entry.label);
lv_label_set_text(caption_, entry.caption == nullptr ? "" : entry.caption);

// A long list gets a counter; dots stop conveying position once there are too many
// to distinguish at a glance.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ struct CarouselEntry {
const char* icon{nullptr}; // LV_SYMBOL_*
const char* label{nullptr};
std::uint32_t icon_rgb{0xFFFFFF}; // colour-coded by function
// A second line under the label, for when the label alone does not say what it is: a
// record value needs its name, and a choice benefits from a word on what it does.
// Appended, because this struct is aggregate-initialised at every call site and a field
// in the middle silently reassigns positional initialisers.
const char* caption{nullptr};
};

// One choice fills the panel: a large icon, a large label, pressable chevrons and a
Expand Down Expand Up @@ -53,6 +58,7 @@ class CarouselScreen {
lv_obj_t* title_{nullptr};
lv_obj_t* icon_{nullptr};
lv_obj_t* label_{nullptr};
lv_obj_t* caption_{nullptr};
lv_obj_t* hint_{nullptr};
std::array<lv_obj_t*, 2> chevrons_{};
std::array<lv_obj_t*, kCarouselMaximumDots> dots_{};
Expand Down
46 changes: 46 additions & 0 deletions firmware/components/ui/include/track_timer/ui/review_cards.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#pragma once

#include "track_timer/logger/formats.hpp"

#include <array>
#include <cstddef>
#include <cstdint>

namespace track_timer::ui {

// One value per card, cycled with a swipe, in the arrangement the rest of the device uses.
// Free of LVGL so the list a session produces can be tested without a display: what a
// record is worth reading is a question about the record, not about how it is drawn.
enum class ReviewMetric : std::uint8_t {
duration,
overrun,
peak_total,
longitudinal,
lateral,
vertical,
completion,
integrity,
};

inline constexpr std::size_t kReviewCardCapacity = 8;

struct ReviewCard {
ReviewMetric metric{ReviewMetric::duration};
// The value, large. Two figures where a metric has two sides, because braking and
// acceleration are read against each other rather than in isolation.
std::array<char, 24> value{};
};

struct ReviewCardList {
std::array<ReviewCard, kReviewCardCapacity> cards{};
std::size_t count{0};
};

[[nodiscard]] ReviewCardList review_cards(const logger::SessionSummaryV1& summary) noexcept;

// The line under the value, saying what it is.
[[nodiscard]] const char* review_metric_caption(ReviewMetric metric) noexcept;
// Colour-coded by what it measures, as the menus are by what they do.
[[nodiscard]] std::uint32_t review_metric_rgb(ReviewMetric metric) noexcept;

} // namespace track_timer::ui
137 changes: 137 additions & 0 deletions firmware/components/ui/review_cards.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
#include "track_timer/ui/review_cards.hpp"

#include <algorithm>
#include <cstdio>

namespace track_timer::ui {
namespace {

void set_clock(std::array<char, 24>& value, const std::int64_t milliseconds) noexcept
{
const auto total = std::max<std::int64_t>(0, milliseconds) / 1'000;
std::snprintf(value.data(), value.size(), "%lld:%02lld",
static_cast<long long>(total / 60), static_cast<long long>(total % 60));
}

void set_pair(std::array<char, 24>& value, const float first, const float second) noexcept
{
std::snprintf(value.data(), value.size(), "%.2f / %.2f", static_cast<double>(first),
static_cast<double>(second));
}

void append(ReviewCardList& list, const ReviewMetric metric) noexcept
{
if (list.count < list.cards.size()) {
list.cards[list.count].metric = metric;
++list.count;
}
}

const char* completion_text(const logger::SessionCompletionReason reason) noexcept
{
switch (reason) {
case logger::SessionCompletionReason::driver_stop:
return "DRIVER";
case logger::SessionCompletionReason::pit_entry:
return "PIT ENTRY";
case logger::SessionCompletionReason::reset_recovery:
return "RECOVERED";
case logger::SessionCompletionReason::none:
break;
}
return "UNKNOWN";
}

} // namespace

ReviewCardList review_cards(const logger::SessionSummaryV1& summary) noexcept
{
ReviewCardList list{};

append(list, ReviewMetric::duration);
set_clock(list.cards[list.count - 1].value, summary.session_duration_ms);

append(list, ReviewMetric::overrun);
set_clock(list.cards[list.count - 1].value, summary.session_overrun_ms);

append(list, ReviewMetric::peak_total);
std::snprintf(list.cards[list.count - 1].value.data(),
list.cards[list.count - 1].value.size(), "%.2f",
static_cast<double>(summary.peaks.total_g));

append(list, ReviewMetric::longitudinal);
set_pair(list.cards[list.count - 1].value, summary.peaks.acceleration_g,
summary.peaks.braking_g);

append(list, ReviewMetric::lateral);
set_pair(list.cards[list.count - 1].value, summary.peaks.left_g, summary.peaks.right_g);

append(list, ReviewMetric::vertical);
set_pair(list.cards[list.count - 1].value, summary.peaks.up_g, summary.peaks.down_g);

append(list, ReviewMetric::completion);
std::snprintf(list.cards[list.count - 1].value.data(),
list.cards[list.count - 1].value.size(), "%s",
completion_text(summary.completion_reason));

// Only when there is something to say. A card that always reads "fine" teaches the
// driver to swipe past the one place a problem would be reported.
if (summary.integrity == logger::SummaryIntegrity::partial_log ||
summary.degraded_subsystems != logger::degraded_none) {
append(list, ReviewMetric::integrity);
const auto* text = summary.integrity == logger::SummaryIntegrity::partial_log
? "PARTIAL LOG"
: "NO GPS";
std::snprintf(list.cards[list.count - 1].value.data(),
list.cards[list.count - 1].value.size(), "%s", text);
}
return list;
}

const char* review_metric_caption(const ReviewMetric metric) noexcept
{
switch (metric) {
case ReviewMetric::duration:
return "SESSION TIME";
case ReviewMetric::overrun:
return "OVER RUN";
case ReviewMetric::peak_total:
return "PEAK G";
case ReviewMetric::longitudinal:
return "ACCEL / BRAKE G";
case ReviewMetric::lateral:
return "LEFT / RIGHT G";
case ReviewMetric::vertical:
return "UP / DOWN G";
case ReviewMetric::completion:
return "ENDED BY";
case ReviewMetric::integrity:
return "RECORDING";
}
return "";
}

std::uint32_t review_metric_rgb(const ReviewMetric metric) noexcept
{
switch (metric) {
case ReviewMetric::duration:
return 0x39B6FF; // azure, as the timer is elsewhere
case ReviewMetric::overrun:
return 0x9A4DFF; // the overrun's own deep purple
case ReviewMetric::peak_total:
return 0xFF3B30;
case ReviewMetric::longitudinal:
return 0x2FD16D;
case ReviewMetric::lateral:
return 0xFFC02E;
case ReviewMetric::vertical:
return 0xFF8A24;
case ReviewMetric::completion:
return 0x7C8899;
case ReviewMetric::integrity:
return 0xFFC02E;
}
return 0xFFFFFF;
}

} // namespace track_timer::ui
Loading