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

### Added

- a **0-X** mode: the time from a standing start to a target speed, counting up from
`0.00`, stopping the instant the target is reached and turning green. Coming back to rest
re-arms it; a tap resets it sooner. The target is chosen in `SETUP` -> `0-X TARGET`
- both ends of a 0-X run are **interpolated rather than quantised**. A stationary receiver
reports a few centimetres per second of noise, not zero, so the start is estimated by
extrapolating back along the acceleration between the first two moving fixes to where
speed would have been zero; the finish is interpolated between the fixes straddling the
target. At 25 Hz a fix lands every 40 ms, so without both the second decimal would be
decoration. A 0-60 at 4 m/s2 measures 6.71 s against a true 6.7056
- a 0-X run whose fixes dry up is **abandoned, not finished**. The number stops climbing
either way, which is the whole problem: a stalled run looks exactly like an arrival, so
it says `LOST FIX` and is never green
- the settings record is at **version 8**, carrying the 0-X target. Unlike appending a
mode, a new field changes a fixed-width record's length, so this needed a migration
rather than only a wider validation bound. A v7 device keeps every setting it had and
takes the default target, because it never had an opinion about one

- a **Compass** mode: a fixed card with north up, a marker travelling its rim, and the
bearing in degrees at the centre. The heading was already decoded - `headMot` and
`headAcc` have been reaching `GnssFix` every epoch - so this needed no parser work at all
Expand Down
14 changes: 12 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
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
TIME_ROLLER_TEST_BINARY := build/host/time_roller_test
Expand All @@ -66,9 +67,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 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 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 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 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 @@ -586,6 +587,15 @@ imu-calibration-test:
tests/cpp/test_imu_calibration.cpp -o $(IMU_CALIBRATION_TEST_BINARY)
$(IMU_CALIBRATION_TEST_BINARY)

acceleration-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/acceleration.cpp \
tests/cpp/test_acceleration.cpp -o $(ACCELERATION_TEST_BINARY)
$(ACCELERATION_TEST_BINARY)

compass-test:
mkdir -p build/host
$(CXX) -std=c++17 -Wall -Wextra -Werror -pedantic \
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ device loads them and runs timer-only. #184's test loop is the route to a first
With nothing attached GPS Only reports "NO RECEIVER", which is the truth rather than a
fault.

Three modes are built on those fixes. **Compass** shows course over ground on a fixed card,
Four modes are built on those fixes. **0-X** times a standing start to a target speed,
interpolating both ends rather than quantising them to the fix rate. **Compass** shows course over ground on a fixed card,
and hides its marker entirely when the car is too slow for the receiver to know which way it
is going - a parked vehicle has no heading, and the board has no magnetometer to supply one.
**Speed Only** shows road speed in mph at 248 px -
Expand Down
5 changes: 3 additions & 2 deletions docs/SETTINGS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ file or NVS operations.

## Current schema

Version 4 stores:
Version 8 stores:

- session and rest durations
- launch sensitivity
- average lap time and lower-display choice
- day and night brightness presets
- operating mode (timer, G-meter, GPS Only, Clock, Speed Only or Compass)
- operating mode (timer, G-meter, GPS Only, Clock, Speed Only, Compass or 0-X)
- the 0-X target speed
- Trackday Mode enable/disable state
- repeating lap boundary (`START LINE` or `FINISH LINE`)
- independent Pit Exit Auto-Start and Pit Entry Auto-Stop enable/disable states
Expand Down
53 changes: 47 additions & 6 deletions firmware/components/settings/component.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ constexpr std::size_t kLegacyV5PayloadSize = 74;
// v6 appends the session trigger, which nothing before it had.
// v7 changed what the launch field may hold, not the shape of the payload.
constexpr std::size_t kLegacyV6PayloadSize = 75;
constexpr std::size_t kCurrentPayloadSize = 75;
constexpr std::size_t kLegacyV7PayloadSize = 75;
constexpr std::size_t kCurrentPayloadSize = 76;

inline constexpr std::uint32_t kMaximumDurationSeconds = 24U * 60U * 60U;
inline constexpr std::uint32_t kMinimumSessionSeconds = 60U;
Expand Down Expand Up @@ -157,6 +158,18 @@ std::uint16_t nearest_launch_sensitivity(const std::uint16_t milli_g) noexcept
return best;
}

[[nodiscard]] bool valid_acceleration_target(const std::uint8_t mph) noexcept
{
// One of the offered speeds, not merely a plausible number. A record carrying a target
// the picker cannot reach is a record this firmware did not write.
for (const auto offered : kAccelerationTargetsMph) {
if (offered == mph) {
return true;
}
}
return false;
}

bool valid_settings(const DeviceSettings& settings) noexcept
{
return settings.session_duration_seconds >= kMinimumSessionSeconds &&
Expand All @@ -166,7 +179,8 @@ 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::compass &&
settings.operating_mode <= OperatingMode::acceleration &&
valid_acceleration_target(settings.acceleration_target_mph) &&
settings.orientation <= OrientationMode::automatic &&
settings.lower_display <= LowerDisplayMode::laps_remaining &&
(settings.average_lap_seconds > 0 ||
Expand Down Expand Up @@ -271,14 +285,26 @@ SettingsBlob encode_legacy_settings_v6(const DeviceSettings& settings) noexcept
return make_blob(6, payload.data(), payload.size());
}

SettingsBlob encode_settings(const DeviceSettings& settings) noexcept
SettingsBlob encode_legacy_settings_v7(const DeviceSettings& settings) noexcept
{
const auto legacy = encode_legacy_settings_v6(settings);
if (legacy.size == 0) {
return {};
}
std::array<std::uint8_t, kCurrentPayloadSize> payload{};
std::array<std::uint8_t, kLegacyV7PayloadSize> payload{};
std::copy_n(legacy.bytes.data() + kHeaderSize, kLegacyV6PayloadSize, payload.data());
return make_blob(7, payload.data(), payload.size());
}

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

Expand Down Expand Up @@ -322,7 +348,7 @@ DecodeResult decode_settings(const SettingsBlob& blob, DeviceSettings& settings)
return DecodeResult::migrated_v1;
}
if (version != 2 && version != 3 && version != 4 && version != 5 && version != 6 &&
version != kCurrentSettingsVersion) {
version != 7 && version != kCurrentSettingsVersion) {
return DecodeResult::unsupported_version;
}
const auto expected_payload_size =
Expand All @@ -331,6 +357,7 @@ DecodeResult decode_settings(const SettingsBlob& blob, DeviceSettings& settings)
: version == 4 ? kLegacyV4PayloadSize
: version == 5 ? kLegacyV5PayloadSize
: version == 6 ? kLegacyV6PayloadSize
: version == 7 ? kLegacyV7PayloadSize
: kCurrentPayloadSize;
if (payload_size != expected_payload_size) {
return DecodeResult::corrupt;
Expand Down Expand Up @@ -365,10 +392,20 @@ DecodeResult decode_settings(const SettingsBlob& blob, DeviceSettings& settings)
if (version >= 6) {
candidate.session_trigger = static_cast<SessionTrigger>(payload[74]);
}
if (version < kCurrentSettingsVersion) {
if (version >= 8) {
candidate.acceleration_target_mph = payload[75];
}
// Anything older predates 0-X entirely, so it keeps the default rather than being
// treated as a device that chose it. A migrated record is a record that never had an
// opinion about this, not one whose opinion was zero.
if (version < 7) {
// Before v7 the ladder ran to 4 g and offered zero as "off". Snapping keeps the
// rest of the blob rather than failing validation over one field and taking every
// unrelated setting back to defaults.
//
// Pinned to 7 rather than to kCurrentSettingsVersion: this corrects a v6-and-older
// ladder, and a record written by v7 already has a snapped value. Leaving it as
// "older than current" would have silently re-snapped every future version too.
candidate.launch_sensitivity_milli_g =
nearest_launch_sensitivity(candidate.launch_sensitivity_milli_g);
}
Expand All @@ -389,6 +426,7 @@ DecodeResult decode_settings(const SettingsBlob& blob, DeviceSettings& settings)
: version == 4 ? DecodeResult::migrated_v4
: version == 5 ? DecodeResult::migrated_v5
: version == 6 ? DecodeResult::migrated_v6
: version == 7 ? DecodeResult::migrated_v7
: DecodeResult::current;
}

Expand Down Expand Up @@ -445,6 +483,9 @@ SettingsLoadReport SettingsManager::load() noexcept
case DecodeResult::migrated_v6:
current_ = decoded;
return {SettingsSource::migrated_v6, persist(current_)};
case DecodeResult::migrated_v7:
current_ = decoded;
return {SettingsSource::migrated_v7, 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 = 7;
inline constexpr std::uint16_t kCurrentSettingsVersion = 8;
inline constexpr std::size_t kSettingsBlobCapacity = 128;
inline constexpr std::size_t kTrackIdentifierCapacity = 48;

Expand All @@ -28,6 +28,8 @@ enum class OperatingMode : std::uint8_t {
// A compass card from the receiver's course over ground. Appended again: the order of
// this enum is the stored format, whatever order the modes are offered in.
compass,
// Standing-start acceleration timing, to a target speed chosen in Setup.
acceleration,
};

enum class OrientationMode : std::uint8_t {
Expand Down Expand Up @@ -67,6 +69,11 @@ inline constexpr std::array<std::uint16_t, 10> kLaunchSensitivityMilliG{
// A deliberate pit exit, without tripping on a bump.
inline constexpr std::uint16_t kDefaultLaunchSensitivityMilliG = 300;

// The speeds a 0-X run can be timed to. Sixty is the one everybody means, which is why it
// is the default; thirty is reachable in a car park and a hundred is a straight.
inline constexpr std::array<std::uint8_t, 6> kAccelerationTargetsMph{30, 40, 50, 60, 70, 100};
inline constexpr std::uint8_t kDefaultAccelerationTargetMph = 60;

// Snaps any stored value onto the ladder. Settings written before v7 hold values that are
// no longer offered, and rejecting them would fail the whole blob and take every unrelated
// setting back to defaults with it.
Expand Down Expand Up @@ -95,6 +102,11 @@ struct DeviceSettings {
// 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};
// The speed a 0-X run is timed to, in mph. Appended, like every field before it - but
// unlike appending a mode this is a new byte in a fixed-width record, so it is what
// took the format to version 8 and needed a migration path rather than only a wider
// validation bound.
std::uint8_t acceleration_target_mph{kDefaultAccelerationTargetMph};
};

struct LegacySettingsV1 {
Expand Down Expand Up @@ -131,6 +143,7 @@ enum class DecodeResult : std::uint8_t {
migrated_v4,
migrated_v5,
migrated_v6,
migrated_v7,
corrupt,
unsupported_version,
};
Expand All @@ -143,6 +156,7 @@ enum class SettingsSource : std::uint8_t {
migrated_v4,
migrated_v5,
migrated_v6,
migrated_v7,
defaults_missing,
defaults_corrupt,
defaults_unsupported,
Expand Down Expand Up @@ -193,6 +207,8 @@ struct FeatureAvailability {
const LegacySettingsV1& settings) noexcept;
[[nodiscard]] SettingsBlob encode_legacy_settings_v2(
const DeviceSettings& settings) noexcept;
[[nodiscard]] SettingsBlob encode_legacy_settings_v7(
const DeviceSettings& settings) noexcept;
[[nodiscard]] SettingsBlob encode_legacy_settings_v6(
const DeviceSettings& settings) noexcept;
[[nodiscard]] SettingsBlob encode_legacy_settings_v5(
Expand Down
2 changes: 1 addition & 1 deletion firmware/components/ui/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
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" "g_meter_screen.cpp" "g_radar_screen.cpp" "imu_meter.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" "imu_meter.cpp"
"lvgl_visual_system.cpp" "navigation.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"
Expand Down
Loading