#include #include #include #include #include "track_core/memory_strip.hpp" #include "track_core/pid_program.hpp" #include "track_core/pid_runtime.hpp" #include "track_core/scheme_decoder.hpp" #include "track_core/scheme_runtime.hpp" namespace { void require(bool condition, const char *message) { if (!condition) { std::cerr << "FAIL: " << message << '\n'; std::exit(1); } } void require_eq_u32(std::uint32_t actual, std::uint32_t expected, const char *message) { if (actual != expected) { std::cerr << "FAIL: " << message << ": actual=" << actual << " expected=" << expected << '\n'; std::exit(1); } } void require_near(float actual, float expected, float tolerance, const char *message) { if (std::fabs(actual - expected) > tolerance) { std::cerr << "FAIL: " << message << ": actual=" << actual << " expected=" << expected << '\n'; std::exit(1); } } track_core::TrackInfo running_info(track_core::Color color = track_core::Color::green()) { return { .kind = track_core::SchemeKind::speed_input_time_segmented_mileage_free, .color = color, .id = 1, .is_running = true, .num_segments = 1, }; } void test_circular_render_wraps() { track_core::TrackConfig config{ .draw_kind = track_core::TrackDrawKind::circular, .line_length_m = 10.0F, .active_line_length_m = 4.0F, .head_offset_m = 0.0F, .line_leds_num = 20, }; const track_core::TrackReport report{ .id = 1, .state = track_core::TrackState::run, .mileage_m = 8.5F, .speed_m_s = 1.0F, }; const auto plan = track_core::make_track_render_plan(config, running_info(), report); require_eq_u32(static_cast(plan.span_count), 2, "circular span count"); require_eq_u32(plan.spans[0].start_led, 17, "circular span 0 start"); require_eq_u32(plan.spans[0].led_count, 3, "circular span 0 count"); require(plan.spans[0].color == track_core::Color::green(), "circular span 0 color"); require_eq_u32(plan.spans[1].start_led, 0, "circular span 1 start"); require_eq_u32(plan.spans[1].led_count, 5, "circular span 1 count"); } void test_linear_render_forward_and_memory_strip() { track_core::TrackConfig config{ .draw_kind = track_core::TrackDrawKind::linear, .line_length_m = 10.0F, .active_line_length_m = 4.0F, .head_offset_m = 0.0F, .line_leds_num = 20, }; const track_core::TrackReport report{ .id = 2, .state = track_core::TrackState::run, .mileage_m = 5.0F, .speed_m_s = 2.0F, }; track_core::MemoryStrip strip(config.line_leds_num); const auto plan = track_core::make_track_render_plan(config, running_info(track_core::Color::red()), report); const auto applied = track_core::apply_render_plan(strip, plan); require(applied == track_core::TrackError::ok, "linear plan applies"); require_eq_u32(static_cast(plan.span_count), 4, "linear span count"); require_eq_u32(plan.spans[0].start_led, 10, "linear span 0 start"); require_eq_u32(plan.spans[0].led_count, 2, "linear span 0 count"); require(plan.spans[0].color == track_core::Color::red(), "linear span 0 color"); require_eq_u32(plan.spans[1].start_led, 12, "linear span 1 start"); require(plan.spans[1].color == track_core::Color::blue(), "linear span 1 color"); require_eq_u32(plan.spans[2].start_led, 6, "linear span 2 start"); require(plan.spans[2].color == track_core::Color::cyan(), "linear span 2 color"); require_eq_u32(plan.spans[3].start_led, 8, "linear span 3 start"); require(plan.spans[3].color == track_core::Color::red(), "linear span 3 color"); } void test_linear_render_reverse() { track_core::TrackConfig config{ .draw_kind = track_core::TrackDrawKind::linear, .line_length_m = 10.0F, .active_line_length_m = 5.0F, .head_offset_m = 0.0F, .line_leds_num = 20, }; const track_core::TrackReport report{ .id = 3, .state = track_core::TrackState::run, .mileage_m = 15.0F, .speed_m_s = 2.0F, }; const auto plan = track_core::make_track_render_plan(config, running_info(track_core::Color::red()), report); require_eq_u32(static_cast(plan.span_count), 4, "reverse span count"); require_eq_u32(plan.spans[0].start_led, 10, "reverse span 0 start"); require_eq_u32(plan.spans[0].led_count, 3, "reverse span 0 count"); require(plan.spans[0].color == track_core::Color::red(), "reverse span 0 color"); require_eq_u32(plan.spans[1].start_led, 13, "reverse span 1 start"); require_eq_u32(plan.spans[1].led_count, 2, "reverse span 1 count"); require(plan.spans[1].color == track_core::Color::blue(), "reverse span 1 color"); require_eq_u32(plan.spans[2].start_led, 5, "reverse span 2 start"); require_eq_u32(plan.spans[2].led_count, 2, "reverse span 2 count"); require(plan.spans[2].color == track_core::Color::cyan(), "reverse span 2 color"); require_eq_u32(plan.spans[3].start_led, 7, "reverse span 3 start"); require_eq_u32(plan.spans[3].led_count, 3, "reverse span 3 count"); require(plan.spans[3].color == track_core::Color::red(), "reverse span 3 color"); } void test_linear_render_prefix_postfix_colors_do_not_reverse() { track_core::TrackConfig config{ .draw_kind = track_core::TrackDrawKind::linear, .line_length_m = 10.0F, .active_line_length_m = 5.0F, .head_offset_m = 0.0F, .line_leds_num = 20, }; const track_core::TrackReport forward_report{ .id = 3, .state = track_core::TrackState::run, .mileage_m = 5.0F, .speed_m_s = 2.0F, }; const track_core::TrackReport reverse_report{ .id = 3, .state = track_core::TrackState::run, .mileage_m = 15.0F, .speed_m_s = 2.0F, }; const auto forward_plan = track_core::make_track_render_plan(config, running_info(track_core::Color::red()), forward_report); const auto reverse_plan = track_core::make_track_render_plan(config, running_info(track_core::Color::red()), reverse_report); require_eq_u32(static_cast(forward_plan.span_count), static_cast(reverse_plan.span_count), "linear same-position span count"); for (std::size_t i = 0; i < forward_plan.span_count; ++i) { require_eq_u32(forward_plan.spans[i].start_led, reverse_plan.spans[i].start_led, "linear same-position span start"); require_eq_u32(forward_plan.spans[i].led_count, reverse_plan.spans[i].led_count, "linear same-position span count"); require(forward_plan.spans[i].color == reverse_plan.spans[i].color, "linear same-position span color"); } } void test_linear_render_reverse_near_ends_stays_visible() { track_core::TrackConfig config{ .draw_kind = track_core::TrackDrawKind::linear, .line_length_m = 10.0F, .active_line_length_m = 4.0F, .head_offset_m = 0.0F, .line_leds_num = 20, }; const auto info = running_info(track_core::Color::red()); const auto require_visible = [&](float mileage_m, const char *message) { const track_core::TrackReport report{ .id = 4, .state = track_core::TrackState::run, .mileage_m = mileage_m, .speed_m_s = 2.0F, }; const auto plan = track_core::make_track_render_plan(config, info, report); std::uint32_t total_leds = 0; for (std::size_t i = 0; i < plan.span_count; ++i) { total_leds += plan.spans[i].led_count; } require(total_leds > 0, message); }; require_visible(10.1F, "reverse render visible just after right-end bounce"); require_visible(19.9F, "reverse render visible just before left-end return"); } void test_linear_render_boundaries_apply() { track_core::TrackConfig config{ .draw_kind = track_core::TrackDrawKind::linear, .line_length_m = 10.0F, .active_line_length_m = 4.0F, .head_offset_m = 0.0F, .line_leds_num = 20, }; const auto info = running_info(track_core::Color::red()); for (const float mileage : { -20.0F, -10.1F, -10.0F, -9.9F, -1.0F, -0.1F, 0.0F, 0.1F, 1.0F, 9.9F, 10.0F, 10.1F, 19.9F, 20.0F, 20.1F, 29.9F, 30.0F, 30.1F, 40.0F, }) { const track_core::TrackReport report{ .id = 4, .state = track_core::TrackState::run, .mileage_m = mileage, .speed_m_s = 2.0F, }; track_core::MemoryStrip strip(config.line_leds_num); const auto plan = track_core::make_track_render_plan(config, info, report); require(plan.span_count <= track_core::TrackRenderPlan::max_spans, "linear boundary span capacity"); for (std::size_t i = 0; i < plan.span_count; ++i) { const auto &span = plan.spans[i]; require(span.start_led < config.line_leds_num, "linear boundary span start in range"); require(span.start_led + span.led_count <= config.line_leds_num, "linear boundary span end in range"); } require( track_core::apply_render_plan(strip, plan) == track_core::TrackError::ok, "linear boundary plan applies"); } } void test_memory_strip_bounds() { track_core::MemoryStrip strip(4); require(strip.fill(0, 0, track_core::Color::red()) == track_core::TrackError::ok, "zero fill"); require(strip.fill(2, 8, track_core::Color::blue()) == track_core::TrackError::ok, "clamped fill"); require(strip.pixels()[2] == track_core::Color::blue(), "clamped fill pixel 2"); require(strip.pixels()[3] == track_core::Color::blue(), "clamped fill pixel 3"); require(strip.fill(4, 1, track_core::Color::red()) == track_core::TrackError::range, "out-of-range fill"); } void test_scheme_decoder() { const std::vector data{ static_cast(track_core::SchemeKind::speed_input_time_segmented_mileage_free), static_cast(track_core::AccelerationProfile::instant), 2, 0, 0, 0, 51, 10, 0, }; const auto decoded = track_core::decode_scheme(7, track_core::Color::white(), data); require(decoded.has_value(), "decode ST scheme"); require(decoded->id == 7, "decoded id"); require(decoded->kind == track_core::SchemeKind::speed_input_time_segmented_mileage_free, "decoded kind"); const auto *segments = std::get_if>(&decoded->segments); require(segments != nullptr, "decoded ST segment vector"); require_eq_u32(static_cast(segments->size()), 2, "decoded ST segment count"); require_near((*segments)[1].speed_m_s, 2.0F, 0.01F, "decoded ST speed"); require_eq_u32((*segments)[1].time_since_start_s, 10, "decoded ST time"); } void test_pid_program_constant() { track_core::TrackPidConfig config; config.schemas = { track_core::TrackPidSchema{track_core::TrackConstantSegment{ .duration_s = 2, .speed_m_s = 1.5F, }}, }; const auto start = track_core::clock::time_point(std::chrono::seconds(0)); track_core::TrackPidProgramState state(start, config); require_near(state.next(start + std::chrono::seconds(1), std::nullopt), 1.5F, 0.0001F, "constant PID speed"); require(!state.is_finished(start + std::chrono::seconds(1)), "constant PID not finished"); require(state.is_finished(start + std::chrono::seconds(2)), "constant PID finished"); } track_core::TrackPidConfig pid_runtime_config() { track_core::TrackPidConfig config; config.band_id = 5; config.target_hr_bpm = 148; config.deadzone_bpm = 3; config.schemas = { track_core::TrackPidSchema{track_core::TrackPidSegment{ .duration_s = 45, .min_speed_m_s = 0.0F, .max_speed_m_s = 4.0F, .kp = 1.0F, .ki = 2.0F, .kd = 0.0F, .slew_rate_limit = 0.25F, .fine_tune = std::nullopt, }}, }; return config; } track_core::TrackPidConfig constant_pid_runtime_config(float speed_m_s) { track_core::TrackPidConfig config; config.band_id = 5; config.target_hr_bpm = 148; config.deadzone_bpm = 3; config.schemas = { track_core::TrackPidSchema{track_core::TrackConstantSegment{ .duration_s = 2, .speed_m_s = speed_m_s, }}, }; return config; } track_core::TrackPidBandSnapshot band_sample(std::uint32_t seq, std::uint8_t heart_rate = 140) { return { .band_id = 5, .heart_rate = heart_rate, .heart_rate_sample_seq = seq, .has_heart_rate = true, .hr_is_fresh = true, .step_count = 0, .has_step_count = false, .band_is_active = true, }; } void test_pid_runtime_consumes_each_hr_sample_once() { track_core::PidHrRuntime runtime(pid_runtime_config()); const auto start = track_core::clock::time_point(std::chrono::seconds(0)); runtime.start(start); runtime.tick(nullptr, band_sample(1), start + std::chrono::milliseconds(1)); const auto first_status = runtime.pid_status(band_sample(1), start + std::chrono::milliseconds(1)); require_near(first_status.effective_speed_m_s, 0.25F, 0.0001F, "pid runtime first effective speed"); require_near(first_status.base_speed_m_s, 0.25F, 0.0001F, "pid runtime first base speed"); runtime.tick(nullptr, band_sample(1), start + std::chrono::milliseconds(2)); const auto repeated_status = runtime.pid_status(band_sample(1), start + std::chrono::milliseconds(2)); require_near( repeated_status.effective_speed_m_s, first_status.effective_speed_m_s, 0.0001F, "pid runtime ignores repeated sample effective speed"); require_near( repeated_status.base_speed_m_s, first_status.base_speed_m_s, 0.0001F, "pid runtime ignores repeated sample base speed"); runtime.tick(nullptr, band_sample(2), start + std::chrono::milliseconds(3)); const auto second_status = runtime.pid_status(band_sample(2), start + std::chrono::milliseconds(3)); require(second_status.effective_speed_m_s > repeated_status.effective_speed_m_s, "pid runtime consumes new sample"); require(second_status.base_speed_m_s > repeated_status.base_speed_m_s, "pid runtime updates base speed on new sample"); } void test_pid_runtime_suppression_is_explicitly_configured() { auto config = constant_pid_runtime_config(1.0F); config.speed_suppression = track_core::TrackPidSpeedSuppression{ .ratio_min = 0.25F, .sigma_m = 0.01F, }; const track_core::TrackConfig track_config{ .draw_kind = track_core::TrackDrawKind::circular, .line_length_m = 0.1F, .active_line_length_m = 0.025F, .head_offset_m = 0.0F, .line_leds_num = 100, }; const auto start = track_core::clock::time_point(std::chrono::seconds(0)); track_core::PidHrRuntime without_config(config); without_config.start(start); without_config.tick(nullptr, {}, start + std::chrono::milliseconds(1)); auto status = without_config.pid_status({}, start + std::chrono::milliseconds(1)); require_near(status.effective_speed_m_s, 1.0F, 0.0001F, "pid runtime skips suppression without track config"); require_near(status.base_speed_m_s, 1.0F, 0.0001F, "pid runtime base speed without track config"); track_core::PidHrRuntime with_config(config); with_config.start(start); with_config.tick(&track_config, {}, start + std::chrono::milliseconds(1)); status = with_config.pid_status({}, start + std::chrono::milliseconds(1)); require(status.active_segment_kind == track_core::TrackPidStageKind::constant, "pid runtime constant stage kind"); require_near(status.effective_speed_m_s, 0.25F, 0.0001F, "pid runtime suppresses at seam"); require_near(status.base_speed_m_s, 1.0F, 0.0001F, "pid runtime base speed at seam"); } void test_pid_runtime_applies_live_tuning() { track_core::PidHrRuntime runtime(pid_runtime_config()); const auto start = track_core::clock::time_point(std::chrono::seconds(0)); runtime.start(start); runtime.tick(nullptr, band_sample(1), start + std::chrono::milliseconds(1)); const track_core::TrackPidRuntimeCommand command{ .command = track_core::TrackPidSetTuning{ .min_speed_m_s = 1.2F, .max_speed_m_s = 3.5F, .kp = 0.5F, .ki = 0.25F, .kd = 0.0F, .slew_rate_limit = 0.25F, .fine_tune = std::nullopt, }, }; const auto applied = runtime.apply_pid_runtime_command(command, nullptr); require(applied.has_value(), "pid runtime applies tuning"); require_near( std::get(runtime.pid_config().schemas.front().segment).min_speed_m_s, 1.2F, 0.0001F, "pid runtime config tracks tuning"); const auto status = runtime.pid_status(band_sample(1), start + std::chrono::milliseconds(1)); require_near(status.effective_speed_m_s, 1.2F, 0.0001F, "pid runtime tuned effective speed"); require_near(status.base_speed_m_s, 1.2F, 0.0001F, "pid runtime tuned base speed"); } track_core::TrackConfig runtime_config() { return { .draw_kind = track_core::TrackDrawKind::circular, .line_length_m = 5.0F, .active_line_length_m = 1.0F, .head_offset_m = 0.0F, .line_leds_num = 50, }; } void test_speed_mileage_runtime_ticks() { auto runtime = track_core::make_scheme_track_runtime(track_core::make_speed_mileage_scheme( 11, track_core::Color::red(), track_core::AccelerationProfile::instant, { track_core::SMSegment{.speed_m_s = 1.0F, .mileage_from_start_m = 0}, track_core::SMSegment{.speed_m_s = 2.0F, .mileage_from_start_m = 5}, track_core::SMSegment{.speed_m_s = 1.0F, .mileage_from_start_m = 10}, })); require(runtime.has_value(), "SM runtime builds"); auto track = track_core::start_scheme_track(std::move(*runtime)); track = track_core::tick_scheme_track(runtime_config(), std::move(track), 6.0F); const auto report = track_core::scheme_track_report(track); require(report.state == track_core::TrackState::run, "SM still running before end"); require_near(report.mileage_m, 6.0F, 0.001F, "SM mileage after tick"); require_near(report.speed_m_s, 2.0F, 0.001F, "SM speed after boundary"); } void test_mileage_time_runtime_ticks() { auto runtime = track_core::make_scheme_track_runtime(track_core::make_mileage_time_scheme( 12, track_core::Color::green(), track_core::AccelerationProfile::instant, { track_core::MTSegment{.mileage_to_travel_this_segment_m = 10, .time_since_start_s = 0}, track_core::MTSegment{.mileage_to_travel_this_segment_m = 20, .time_since_start_s = 5}, track_core::MTSegment{.mileage_to_travel_this_segment_m = 1, .time_since_start_s = 15}, })); require(runtime.has_value(), "MT runtime builds"); auto track = track_core::start_scheme_track(std::move(*runtime)); track = track_core::tick_scheme_track(runtime_config(), std::move(track), 2.0F); auto report = track_core::scheme_track_report(track); require_near(report.speed_m_s, 2.0F, 0.001F, "MT initial derived speed"); require_near(report.mileage_m, 4.0F, 0.001F, "MT mileage after first tick"); track = track_core::tick_scheme_track(runtime_config(), std::move(track), 4.0F); report = track_core::scheme_track_report(track); require_near(report.speed_m_s, 2.0F, 0.001F, "MT speed after first time boundary"); require_near(report.mileage_m, 12.0F, 0.001F, "MT mileage after boundary tick"); } void test_speed_time_runtime_ticks() { auto runtime = track_core::make_scheme_track_runtime(track_core::make_speed_time_scheme( 13, track_core::Color::blue(), track_core::AccelerationProfile::instant, { track_core::STSegment{.speed_m_s = 1.0F, .time_since_start_s = 0}, track_core::STSegment{.speed_m_s = 3.0F, .time_since_start_s = 5}, track_core::STSegment{.speed_m_s = 1.0F, .time_since_start_s = 10}, })); require(runtime.has_value(), "ST runtime builds"); auto track = track_core::start_scheme_track(std::move(*runtime)); track = track_core::tick_scheme_track(runtime_config(), std::move(track), 6.0F); const auto report = track_core::scheme_track_report(track); require(report.state == track_core::TrackState::run, "ST still running before end"); require_near(report.mileage_m, 6.0F, 0.001F, "ST mileage after tick"); require_near(report.speed_m_s, 3.0F, 0.001F, "ST speed after time boundary"); } void test_repeated_speed_mileage_time_runtime_ticks() { auto runtime = track_core::make_scheme_track_runtime(track_core::make_repeated_speed_mileage_time_scheme( 14, track_core::Color::white(), track_core::AccelerationProfile::instant, { track_core::RepeatedSMSegment{ .speed_mileage_segments = { track_core::SMSegment{.speed_m_s = 1.0F, .mileage_from_start_m = 0}, track_core::SMSegment{.speed_m_s = 2.0F, .mileage_from_start_m = 5}, }, .time_since_start_s = 0, }, track_core::RepeatedSMSegment{ .speed_mileage_segments = { track_core::SMSegment{.speed_m_s = 3.0F, .mileage_from_start_m = 0}, track_core::SMSegment{.speed_m_s = 1.0F, .mileage_from_start_m = 5}, }, .time_since_start_s = 4, }, track_core::RepeatedSMSegment{ .speed_mileage_segments = { track_core::SMSegment{.speed_m_s = 1.0F, .mileage_from_start_m = 0}, track_core::SMSegment{.speed_m_s = 1.0F, .mileage_from_start_m = 5}, }, .time_since_start_s = 20, }, })); require(runtime.has_value(), "RSMT runtime builds"); auto track = track_core::start_scheme_track(std::move(*runtime)); track = track_core::tick_scheme_track(runtime_config(), std::move(track), 3.0F); track = track_core::tick_scheme_track(runtime_config(), std::move(track), 2.0F); require_eq_u32(static_cast(track.state.primary_segment_index), 0, "RSMT waits for line alignment"); track = track_core::tick_scheme_track(runtime_config(), std::move(track), 1.0F); require_eq_u32(static_cast(track.state.primary_segment_index), 1, "RSMT switches on alignment"); require_near(track.state.speed_m_s, 3.0F, 0.001F, "RSMT switched speed"); } void test_scheme_training_runtime_renders() { track_core::SchemeTrainingRuntime runtime; const auto added = runtime.add_scheme(track_core::make_speed_time_scheme( 15, track_core::Color::green(), track_core::AccelerationProfile::instant, { track_core::STSegment{.speed_m_s = 1.0F, .time_since_start_s = 0}, track_core::STSegment{.speed_m_s = 1.0F, .time_since_start_s = 10}, })); require(added.has_value(), "training runtime accepts scheme"); runtime.start(); runtime.tick(runtime_config(), 1.0F); const auto pixels = runtime.render_pixels(runtime_config()); require(pixels.has_value(), "training runtime renders pixels"); require_eq_u32(static_cast(pixels->size()), runtime_config().line_leds_num, "training runtime pixel count"); track_core::MemoryStrip strip(runtime_config().line_leds_num); const auto rendered = runtime.render_to(runtime_config(), track_core::make_memory_strip_sink(strip)); require(rendered.has_value(), "training runtime renders to sink"); for (std::size_t i = 0; i < pixels->size(); ++i) { require((*pixels)[i] == strip.pixels()[i], "render_to matches render_pixels"); } require(!runtime.all_stopped(), "training runtime running"); require_eq_u32(runtime.state_collection().states[0].id, 15, "training runtime report id"); } } // namespace int main() { test_circular_render_wraps(); test_linear_render_forward_and_memory_strip(); test_linear_render_reverse(); test_linear_render_prefix_postfix_colors_do_not_reverse(); test_linear_render_reverse_near_ends_stays_visible(); test_linear_render_boundaries_apply(); test_memory_strip_bounds(); test_scheme_decoder(); test_pid_program_constant(); test_pid_runtime_consumes_each_hr_sample_once(); test_pid_runtime_suppression_is_explicitly_configured(); test_pid_runtime_applies_live_tuning(); test_speed_mileage_runtime_ticks(); test_mileage_time_runtime_ticks(); test_speed_time_runtime_ticks(); test_repeated_speed_mileage_time_runtime_ticks(); test_scheme_training_runtime_renders(); std::cout << "track-core tests passed\n"; return 0; }