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

### Added

- a **Navigation** mode: road speed low and large, the receiver's clock top left, and an
arrow top right pointing where the car is going with north at twelve o'clock. Three
readouts the device already computed in three separate modes, on one screen
- the **unit sits beside the speed rather than under it**, which is the one place this
screen departs from Speed Only and the reason its face is 208 px against Speed Only's
248. Three digits plus `MPH` and its gap have to fit 540 px, which leaves 454 px for the
digits and so caps the face at 208. Speed Only keeps 248 and its label underneath - it
has the whole panel to answer a narrower question in, and is not changed to match
- the arrow is **absent whenever the heading is not trustworthy**, with the reason in the
ring where it would have been. The board has no magnetometer, so the direction is course
over ground and a parked car has no heading at all - not a stale one, not north. The
gates are Compass mode's, reused rather than restated: the receiver's own `headAcc`
under 15 degrees, with a half-metre-per-second backstop under it
- **one cause stated once**: with no receiver all three readouts are absent for the same
reason, and printing it beside the speed, under the clock and inside the ring would read
as three faults. Every other combination keeps its three separate statuses, because a
parked car with a fix and a time and no course really is three different things to say
- the mode is appended to `OperatingMode`, so this is **one byte that already existed
changing value**: no settings format bump, no migration, and a device updating to this
firmware keeps its settings

- **Race mode has its own running-session screen**: the lap in progress in `mm:ss.sss`,
the fastest lap of the session, and the time left in it. Race previously borrowed Track
Day's countdown, which - as that screen's own header says - has nowhere to put a lap
Expand Down
16 changes: 14 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ GPS_ONLY_TEST_BINARY := build/host/gps_only_test
CLOCK_TEST_BINARY := build/host/clock_test
SPEED_ONLY_TEST_BINARY := build/host/speed_only_test
COMPASS_TEST_BINARY := build/host/compass_test
NAVIGATION_MODE_TEST_BINARY := build/host/navigation_mode_test
ACCELERATION_TEST_BINARY := build/host/acceleration_test
IMU_CALIBRATION_TEST_BINARY := build/host/imu_calibration_test
SESSION_URGENCY_TEST_BINARY := build/host/session_urgency_test
Expand All @@ -67,9 +68,9 @@ SIMULATOR_BUILD_DIR ?= build/simulator
SIMULATOR_IMAGE ?= track-session-timer-simulator:lvgl-9.5.0
CMAKE ?= cmake

.PHONY: check venv python-env test track-validate uk-track-pack test-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 gps-only-test clock-test speed-only-test compass-test acceleration-test imu-calibration-test session-urgency-test time-roller-test session-trigger-test summary-store-test summary-file-test review-cards-test ubx-parser-test fix-validation-test gnss-pipeline-test gnss-configure-test i2c-transport-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-capture-boot firmware-device-info firmware-clean issue-preview label-preview
.PHONY: check venv python-env test track-validate uk-track-pack test-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 gps-only-test clock-test speed-only-test compass-test navigation-mode-test acceleration-test imu-calibration-test session-urgency-test time-roller-test session-trigger-test summary-store-test summary-file-test review-cards-test ubx-parser-test fix-validation-test gnss-pipeline-test gnss-configure-test i2c-transport-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-capture-boot firmware-device-info firmware-clean issue-preview label-preview

check: python-env test track-validate test-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 gps-only-test clock-test speed-only-test compass-test acceleration-test imu-calibration-test session-urgency-test time-roller-test session-trigger-test summary-store-test summary-file-test review-cards-test ubx-parser-test fix-validation-test gnss-pipeline-test gnss-configure-test i2c-transport-test gate-capture-test gate-session-automation-test ui-foundation-test navigation-test
check: python-env test track-validate test-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 gps-only-test clock-test speed-only-test compass-test navigation-mode-test acceleration-test imu-calibration-test session-urgency-test time-roller-test session-trigger-test summary-store-test summary-file-test review-cards-test ubx-parser-test fix-validation-test gnss-pipeline-test gnss-configure-test i2c-transport-test gate-capture-test gate-session-automation-test ui-foundation-test navigation-test

venv:
python3 -m venv .venv
Expand Down Expand Up @@ -605,6 +606,17 @@ compass-test:
tests/cpp/test_compass.cpp -o $(COMPASS_TEST_BINARY)
$(COMPASS_TEST_BINARY)

navigation-mode-test:
mkdir -p build/host
$(CXX) -std=c++17 -Wall -Wextra -Werror -pedantic \
-Ifirmware/components/domain/include \
-Ifirmware/components/ui/include \
firmware/components/ui/gps_only.cpp firmware/components/ui/speed_only.cpp \
firmware/components/ui/compass.cpp firmware/components/ui/clock.cpp \
firmware/components/ui/navigation_mode.cpp \
tests/cpp/test_navigation_mode.cpp -o $(NAVIGATION_MODE_TEST_BINARY)
$(NAVIGATION_MODE_TEST_BINARY)

speed-only-test:
mkdir -p build/host
$(CXX) -std=c++17 -Wall -Wextra -Werror -pedantic \
Expand Down
2 changes: 1 addition & 1 deletion firmware/components/settings/component.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ bool valid_settings(const DeviceSettings& settings) noexcept
settings.average_lap_seconds <= 59 * 60 + 59 &&
valid_brightness(settings.day_brightness_percent) &&
valid_brightness(settings.night_brightness_percent) &&
settings.operating_mode <= OperatingMode::acceleration &&
settings.operating_mode <= OperatingMode::navigation &&
valid_acceleration_target(settings.acceleration_target_mph) &&
settings.orientation <= OrientationMode::automatic &&
settings.lower_display <= LowerDisplayMode::laps_remaining &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ enum class OperatingMode : std::uint8_t {
compass,
// Standing-start acceleration timing, to a target speed chosen in Setup.
acceleration,
// Road speed, the receiver's clock and its course over ground on one screen. Appended
// for the reason every mode before it was: this enum's order *is* the stored format,
// so a new mode goes on the end whatever order it is offered to the driver in.
navigation,
};

enum class OrientationMode : std::uint8_t {
Expand Down
2 changes: 1 addition & 1 deletion firmware/components/ui/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,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" "clock.cpp" "clock_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" "gps_only.cpp" "gps_only_screen.cpp" "speed_only.cpp" "speed_only_screen.cpp" "compass.cpp" "compass_screen.cpp" "acceleration.cpp" "acceleration_screen.cpp" "g_meter_screen.cpp" "g_radar_screen.cpp" "race_screen.cpp" "imu_meter.cpp"
"lvgl_visual_system.cpp" "navigation.cpp"
"lvgl_visual_system.cpp" "navigation.cpp" "navigation_mode.cpp" "navigation_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"
Expand Down
17 changes: 17 additions & 0 deletions firmware/components/ui/device_mode.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ ModeVisibility visibility_for(const DeviceMode mode) noexcept
// A compass card and nothing else. gnss_data again marks where the number comes
// from rather than what is drawn, and again this is not a session view.
return {false, false, false, false, true};
case DeviceMode::navigation:
// Speed, time and heading together. gnss_data marks where all three come from;
// like the two above it this is not a session view and must not offer a timer.
return {false, false, false, false, true};
case DeviceMode::acceleration:
// A measurement, and nothing that could be mistaken for timing a session.
return {false, false, false, false, true};
Expand Down Expand Up @@ -51,6 +55,9 @@ DeviceMode mode_from_settings(const settings::DeviceSettings& settings) noexcept
if (settings.operating_mode == settings::OperatingMode::compass) {
return DeviceMode::compass;
}
if (settings.operating_mode == settings::OperatingMode::navigation) {
return DeviceMode::navigation;
}
if (settings.operating_mode == settings::OperatingMode::acceleration) {
return DeviceMode::acceleration;
}
Expand Down Expand Up @@ -86,6 +93,10 @@ void apply_mode(const DeviceMode mode, settings::DeviceSettings& settings) noexc
settings.operating_mode = settings::OperatingMode::compass;
settings.trackday_mode_enabled = false;
break;
case DeviceMode::navigation:
settings.operating_mode = settings::OperatingMode::navigation;
settings.trackday_mode_enabled = false;
break;
case DeviceMode::acceleration:
settings.operating_mode = settings::OperatingMode::acceleration;
settings.trackday_mode_enabled = false;
Expand Down Expand Up @@ -114,6 +125,8 @@ const char* device_mode_name(const DeviceMode mode) noexcept
return "speed-only";
case DeviceMode::compass:
return "compass";
case DeviceMode::navigation:
return "navigation";
case DeviceMode::acceleration:
return "acceleration";
case DeviceMode::gps_only:
Expand All @@ -137,6 +150,8 @@ const char* device_mode_label(const DeviceMode mode) noexcept
return "SPEED";
case DeviceMode::compass:
return "COMPASS";
case DeviceMode::navigation:
return "NAV";
case DeviceMode::acceleration:
return "0-X";
case DeviceMode::gps_only:
Expand All @@ -160,6 +175,8 @@ const char* device_mode_summary(const DeviceMode mode) noexcept
return "Road speed in mph, as large as the panel allows.";
case DeviceMode::compass:
return "Course over ground. Needs movement: a parked car has no heading.";
case DeviceMode::navigation:
return "Speed, GMT and direction of travel. The arrow needs movement.";
case DeviceMode::acceleration:
return "Standing start to a target speed, set in Setup.";
case DeviceMode::gps_only:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ namespace track_timer::ui {
// G-Only operating_mode = g_meter
// Speed Only operating_mode = speed_only
// Compass operating_mode = compass
// Navigation operating_mode = navigation
// 0-X operating_mode = acceleration
// GPS Only operating_mode = gps_only
// Clock operating_mode = clock
Expand All @@ -36,11 +37,15 @@ enum class DeviceMode : std::uint8_t {
g_only,
speed_only,
compass,
// Beside Compass, because it is the same course-over-ground reading with road speed
// and the receiver's clock around it, and like Compass it is a driving display rather
// than something reached when a thing has gone wrong.
navigation,
acceleration,
gps_only,
clock,
};
inline constexpr std::size_t kDeviceModeCount = 8;
inline constexpr std::size_t kDeviceModeCount = 9;

// What each mode is permitted to show while a session is running.
struct ModeVisibility {
Expand Down
57 changes: 57 additions & 0 deletions firmware/components/ui/include/track_timer/ui/navigation_mode.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#pragma once

#include "track_timer/ui/clock.hpp"
#include "track_timer/ui/compass.hpp"
#include "track_timer/ui/speed_only.hpp"

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

namespace track_timer::ui {

// The view model behind Navigation mode: road speed, the receiver's clock and the
// direction of travel on one screen.
//
// Not `navigation.hpp`, which is the shell's screen-to-screen navigator and has nothing to
// do with this. The two should not be merged on the strength of sharing a word.
//
// This mode composes the three view builders that already exist rather than formatting
// anything itself. Speed Only, Clock and Compass have each already settled what their
// readout does when it is not known, and those rules are tested; a fourth formatter would
// be a fourth place for them to drift. What this file owns is the one question none of the
// three can answer alone - what to do when they are all absent for the same reason.

inline constexpr std::size_t kNavigationStatusCapacity = 24;

struct NavigationSnapshot {
// One receiver feeds all three readouts, so there is one GNSS snapshot. The clock gets
// its own because what it shows depends on how long ago its reading arrived, which is
// not a property of the newest fix.
GpsOnlySnapshot gnss{};
ClockSnapshot clock{};
};

struct NavigationViewModel {
SpeedOnlyViewModel speed{};
ClockViewModel time{};
CompassViewModel heading{};
// All three readouts absent, for one cause that is worth stating once.
//
// Without this the screen prints "NO RECEIVER" in three places - beside the speed,
// under the clock and inside the compass ring - which reads as three faults rather
// than one. The three per-readout statuses are still the right thing to show when the
// readouts disagree, which is the common case: a receiver that has a fix and a time
// but no course is a parked car, and that is three different things to say.
std::array<char, kNavigationStatusCapacity> shared_fault{};
std::uint32_t shared_fault_rgb{0};
bool has_shared_fault{false};
};

[[nodiscard]] NavigationViewModel navigation_view(const NavigationSnapshot& snapshot) noexcept;

static_assert(std::is_trivially_copyable_v<NavigationSnapshot>);
static_assert(std::is_trivially_copyable_v<NavigationViewModel>);

} // namespace track_timer::ui
Loading