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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ The project follows Semantic Versioning once the first firmware release is tagge

### Fixed

- section menus opened on their first item rather than the one in force, so Mode, Track and
Trigger could not tell the driver what was set, only let them change it
- the menu carousel was told there were four items while the shell offered five, leaving
Diagnostics selectable but never drawn

- the start page kept the durations it read at boot, so a session or rest period changed in
the menu was obeyed by the timer but still shown at its old value until a restart
- durations were written two ways at once, so a one minute session read "1 MIN SESSION"
Expand Down Expand Up @@ -58,6 +63,8 @@ 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
- top-level TRIGGER selection: MANUAL starts on the button, IMU on a launch, GPS at the line
- pending phase on the running screen for a session armed and waiting for its trigger
- overrun timer counting up in deep purple once a session reaches 00:00, with the lap
estimate replaced by OVER RUN
- rest period shown on the running screen, counting down on the same ramp as a session
Expand Down
17 changes: 15 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,16 @@ DEVICE_MODE_TEST_BINARY := build/host/device_mode_test
IMU_CALIBRATION_TEST_BINARY := build/host/imu_calibration_test
SESSION_URGENCY_TEST_BINARY := build/host/session_urgency_test
TIME_ROLLER_TEST_BINARY := build/host/time_roller_test
SESSION_TRIGGER_TEST_BINARY := build/host/session_trigger_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 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 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 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 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 @@ -423,6 +424,18 @@ gate-session-automation-test:
-o $(GATE_SESSION_AUTOMATION_TEST_BINARY)
$(GATE_SESSION_AUTOMATION_TEST_BINARY)

session-trigger-test:
mkdir -p build/host
$(CXX) -std=c++17 -Wall -Wextra -Werror -pedantic \
-Ifirmware/components/settings/include \
-Ifirmware/components/ui/include \
firmware/components/settings/component.cpp \
firmware/components/ui/session_trigger.cpp \
firmware/components/ui/value_picker.cpp \
firmware/components/ui/time_roller.cpp \
tests/cpp/test_session_trigger.cpp -o $(SESSION_TRIGGER_TEST_BINARY)
$(SESSION_TRIGGER_TEST_BINARY)

time-roller-test:
mkdir -p build/host
$(CXX) -std=c++17 -Wall -Wextra -Werror -pedantic \
Expand Down
37 changes: 32 additions & 5 deletions firmware/components/settings/component.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ constexpr std::size_t kLegacyV4PayloadSize = 66;
// v5 appends the durations as seconds. The minutes still written at offsets 0-3 are
// vestigial: they keep the shared prefix that every earlier version's decoder reads, and
// v5 ignores them in favour of the appended values.
constexpr std::size_t kCurrentPayloadSize = 74;
constexpr std::size_t kLegacyV5PayloadSize = 74;
// v6 appends the session trigger, which nothing before it had.
constexpr std::size_t kCurrentPayloadSize = 75;

inline constexpr std::uint32_t kMaximumDurationSeconds = 24U * 60U * 60U;
inline constexpr std::uint32_t kMinimumSessionSeconds = 60U;
Expand Down Expand Up @@ -149,6 +151,7 @@ bool valid_settings(const DeviceSettings& settings) noexcept
(settings.average_lap_seconds > 0 ||
settings.lower_display == LowerDisplayMode::elapsed) &&
settings.lap_boundary <= LapBoundaryMode::finish &&
settings.session_trigger <= SessionTrigger::gps &&
valid_track_identifier(settings.selected_track_id);
}

Expand Down Expand Up @@ -222,16 +225,28 @@ SettingsBlob encode_legacy_settings_v4(const DeviceSettings& settings) noexcept
return make_blob(4, payload.data(), payload.size());
}

SettingsBlob encode_settings(const DeviceSettings& settings) noexcept
SettingsBlob encode_legacy_settings_v5(const DeviceSettings& settings) noexcept
{
const auto legacy = encode_legacy_settings_v4(settings);
if (legacy.size == 0) {
return {};
}
std::array<std::uint8_t, kCurrentPayloadSize> payload{};
std::array<std::uint8_t, kLegacyV5PayloadSize> payload{};
std::copy_n(legacy.bytes.data() + kHeaderSize, kLegacyV4PayloadSize, payload.data());
put_u32(payload.data() + 66, settings.session_duration_seconds);
put_u32(payload.data() + 70, settings.rest_duration_seconds);
return make_blob(5, payload.data(), payload.size());
}

SettingsBlob encode_settings(const DeviceSettings& settings) noexcept
{
const auto legacy = encode_legacy_settings_v5(settings);
if (legacy.size == 0) {
return {};
}
std::array<std::uint8_t, kCurrentPayloadSize> payload{};
std::copy_n(legacy.bytes.data() + kHeaderSize, kLegacyV5PayloadSize, payload.data());
payload[74] = static_cast<std::uint8_t>(settings.session_trigger);
return make_blob(kCurrentSettingsVersion, payload.data(), payload.size());
}

Expand Down Expand Up @@ -274,14 +289,15 @@ DecodeResult decode_settings(const SettingsBlob& blob, DeviceSettings& settings)
settings = candidate;
return DecodeResult::migrated_v1;
}
if (version != 2 && version != 3 && version != 4 &&
if (version != 2 && version != 3 && version != 4 && version != 5 &&
version != kCurrentSettingsVersion) {
return DecodeResult::unsupported_version;
}
const auto expected_payload_size =
version == 2 ? kLegacyV2PayloadSize
: version == 3 ? kLegacyV3PayloadSize
: version == 4 ? kLegacyV4PayloadSize
: version == 5 ? kLegacyV5PayloadSize
: kCurrentPayloadSize;
if (payload_size != expected_payload_size) {
return DecodeResult::corrupt;
Expand All @@ -308,23 +324,31 @@ DecodeResult decode_settings(const SettingsBlob& blob, DeviceSettings& settings)
candidate.pit_exit_auto_start_enabled = payload[64] != 0;
candidate.pit_entry_auto_stop_enabled = payload[65] != 0;
}
if (version == kCurrentSettingsVersion) {
if (version >= 5) {
// The authoritative durations, which the vestigial minutes above cannot express.
candidate.session_duration_seconds = get_u32(payload + 66);
candidate.rest_duration_seconds = get_u32(payload + 70);
}
if (version == kCurrentSettingsVersion) {
candidate.session_trigger = static_cast<SessionTrigger>(payload[74]);
}
// Everything before v6 predates the trigger, so it defaults to manual, which is what
// those devices were doing.
if (payload[12] > 1 ||
(version >= 3 && payload[62] > 1) ||
(version >= 4 &&
(payload[63] > static_cast<std::uint8_t>(LapBoundaryMode::finish) ||
payload[64] > 1 || payload[65] > 1)) ||
(version == kCurrentSettingsVersion &&
payload[74] > static_cast<std::uint8_t>(SessionTrigger::gps)) ||
!valid_settings(candidate)) {
return DecodeResult::corrupt;
}
settings = candidate;
return version == 2 ? DecodeResult::migrated_v2
: version == 3 ? DecodeResult::migrated_v3
: version == 4 ? DecodeResult::migrated_v4
: version == 5 ? DecodeResult::migrated_v5
: DecodeResult::current;
}

Expand Down Expand Up @@ -375,6 +399,9 @@ SettingsLoadReport SettingsManager::load() noexcept
case DecodeResult::migrated_v4:
current_ = decoded;
return {SettingsSource::migrated_v4, persist(current_)};
case DecodeResult::migrated_v5:
current_ = decoded;
return {SettingsSource::migrated_v5, persist(current_)};
case DecodeResult::corrupt:
current_ = {};
return {SettingsSource::defaults_corrupt, persist(current_)};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

namespace track_timer::settings {

inline constexpr std::uint16_t kCurrentSettingsVersion = 5;
inline constexpr std::uint16_t kCurrentSettingsVersion = 6;
inline constexpr std::size_t kSettingsBlobCapacity = 128;
inline constexpr std::size_t kTrackIdentifierCapacity = 48;

Expand All @@ -29,6 +29,13 @@ enum class LowerDisplayMode : std::uint8_t {
laps_remaining,
};

// What starts a session. Manual is the button; the other two arm the timer and wait.
enum class SessionTrigger : std::uint8_t {
manual,
imu,
gps,
};

enum class LapBoundaryMode : std::uint8_t {
start,
finish,
Expand All @@ -52,6 +59,10 @@ struct DeviceSettings {
LapBoundaryMode lap_boundary{LapBoundaryMode::finish};
bool pit_exit_auto_start_enabled{false};
bool pit_entry_auto_stop_enabled{false};
// Appended, like every field before it: this struct is aggregate-initialised at call
// sites. Owns pit_exit_auto_start_enabled, which is derived from it rather than
// edited separately, so the two cannot disagree.
SessionTrigger session_trigger{SessionTrigger::manual};
};

struct LegacySettingsV1 {
Expand Down Expand Up @@ -86,6 +97,7 @@ enum class DecodeResult : std::uint8_t {
migrated_v2,
migrated_v3,
migrated_v4,
migrated_v5,
corrupt,
unsupported_version,
};
Expand All @@ -96,6 +108,7 @@ enum class SettingsSource : std::uint8_t {
migrated_v2,
migrated_v3,
migrated_v4,
migrated_v5,
defaults_missing,
defaults_corrupt,
defaults_unsupported,
Expand Down Expand Up @@ -146,6 +159,8 @@ struct FeatureAvailability {
const LegacySettingsV1& settings) noexcept;
[[nodiscard]] SettingsBlob encode_legacy_settings_v2(
const DeviceSettings& settings) noexcept;
[[nodiscard]] SettingsBlob encode_legacy_settings_v5(
const DeviceSettings& settings) noexcept;
[[nodiscard]] SettingsBlob encode_legacy_settings_v4(
const DeviceSettings& settings) noexcept;
[[nodiscard]] SettingsBlob encode_legacy_settings_v3(
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_review.cpp" "session_review_screen.cpp"
"presenter.cpp" "ready_screen.cpp" "rest_session.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
28 changes: 27 additions & 1 deletion firmware/components/ui/active_session.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ std::uint32_t urgency_rgb(const SessionUrgency urgency) noexcept
const char* running_phase_caption(const RunningPhase phase) noexcept
{
switch (phase) {
case RunningPhase::pending:
return "PENDING";
case RunningPhase::overrun:
return "OVER RUN";
case RunningPhase::rest:
Expand All @@ -168,7 +170,16 @@ const char* running_phase_caption(const RunningPhase phase) noexcept

std::uint32_t trackday_rgb(const RunningPhase phase, const SessionUrgency urgency) noexcept
{
return phase == RunningPhase::overrun ? kOverrunRgb : urgency_rgb(urgency);
switch (phase) {
case RunningPhase::pending:
return kPendingRgb;
case RunningPhase::overrun:
return kOverrunRgb;
case RunningPhase::session:
case RunningPhase::rest:
break;
}
return urgency_rgb(urgency);
}

const char* session_urgency_name(const SessionUrgency urgency) noexcept
Expand Down Expand Up @@ -215,7 +226,22 @@ void ActiveSessionController::update(const domain::UiSnapshot& snapshot,
view_.feedback = {};
view_.trackday.visible = true;
view_.trackday.phase = display.phase;
view_.trackday.detail[0] = '\0';
switch (display.phase) {
case RunningPhase::pending: {
// The whole session is still ahead, so the bar is full and the clock holds at
// the configured duration rather than counting anything.
format_running_clock(
view_.trackday.countdown,
static_cast<std::int64_t>(display.session_duration_seconds) * 1'000);
format_estimated_laps(view_.trackday, domain::kUnavailableTime,
display.average_lap_seconds);
view_.trackday.remaining_ratio = 1.0F;
view_.trackday.urgency = SessionUrgency::ample;
std::snprintf(view_.trackday.detail.data(), view_.trackday.detail.size(), "%s",
display.pending_detail == nullptr ? "" : display.pending_detail);
break;
}
case RunningPhase::overrun: {
// Counting up, so there is no proportion left to show and the bar is spent.
format_running_clock(view_.trackday.countdown, display.overrun_ms);
Expand Down
11 changes: 11 additions & 0 deletions firmware/components/ui/include/track_timer/ui/active_session.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ struct StopControlViewModel {
// Which clock the running screen is showing. The session counts down, the overrun counts
// up past zero, and rest counts down again through the configured break.
enum class RunningPhase : std::uint8_t {
// Armed and waiting for a trigger. The clock shows the configured duration and does
// not move, because nothing has started yet.
pending,
session,
overrun,
rest,
Expand All @@ -68,6 +71,9 @@ struct ActiveSessionDisplayConfig {
std::int64_t overrun_ms{0};
std::int64_t rest_remaining_ms{0};
std::uint32_t rest_duration_seconds{0};
// What a pending session is waiting for. Owned by the caller, which is the only thing
// that knows whether the IMU has calibrated or a receiver exists.
const char* pending_detail{""};
};

// Urgency bands for the countdown. Named rather than raw colours so the thresholds are
Expand All @@ -84,6 +90,8 @@ enum class SessionUrgency : std::uint8_t {
struct TrackdayModeViewModel {
std::array<char, 32> countdown{};
RunningPhase phase{RunningPhase::session};
// What a pending session is waiting for, or why it cannot arm.
std::array<char, 32> detail{};
std::array<char, 24> estimated_laps{};
// 1.0 at the start of a session falling to 0.0 at its end, for the decaying bar.
float remaining_ratio{0.0F};
Expand Down Expand Up @@ -112,6 +120,9 @@ struct TrackdayModeViewModel {
// literal "deep purple" #6A0DAD manages 2.27:1, where every other state here sits between
// 5.9 and 10.5, and a 10 mm numeral at 2.27:1 is hard work in daylight.
inline constexpr std::uint32_t kOverrunRgb = 0x9A4DFF;
// Pending is not a countdown, so it does not take a point on the ramp: green would read as
// "plenty of time left" on a clock that is not running.
inline constexpr std::uint32_t kPendingRgb = 0x39B6FF;
[[nodiscard]] std::uint32_t trackday_rgb(RunningPhase phase, SessionUrgency urgency) noexcept;
[[nodiscard]] const char* session_urgency_name(SessionUrgency urgency) noexcept;

Expand Down
Loading