1005e50be0
Move scheme and PID training runtime behavior into the pure track_core layer and expose render sinks for injected strip application. Add ESP compatibility adapters, Python bindings/test scaffolding, and in-memory render support so app_track_bt can consume core render and runtime logic without duplicating it. Cover circular/linear rendering boundaries, all scheme runtime types, scheme render_to parity, PID sample de-duplication, speed suppression, and live tuning in track-core tests.
523 lines
22 KiB
C++
523 lines
22 KiB
C++
#include <cmath>
|
|
#include <cstdlib>
|
|
#include <iostream>
|
|
#include <vector>
|
|
|
|
#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<std::uint32_t>(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<std::uint32_t>(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<std::uint32_t>(plan.span_count), 4, "reverse span count");
|
|
require_eq_u32(plan.spans[0].start_led, 5, "reverse span 0 start");
|
|
require_eq_u32(plan.spans[0].led_count, 2, "reverse span 0 count");
|
|
require(plan.spans[0].color == track_core::Color::blue(), "reverse span 0 color");
|
|
require_eq_u32(plan.spans[1].start_led, 7, "reverse span 1 start");
|
|
require_eq_u32(plan.spans[1].led_count, 3, "reverse span 1 count");
|
|
require(plan.spans[1].color == track_core::Color::red(), "reverse span 1 color");
|
|
require_eq_u32(plan.spans[2].start_led, 10, "reverse span 2 start");
|
|
require_eq_u32(plan.spans[2].led_count, 3, "reverse span 2 count");
|
|
require(plan.spans[2].color == track_core::Color::red(), "reverse span 2 color");
|
|
require_eq_u32(plan.spans[3].start_led, 13, "reverse span 3 start");
|
|
require_eq_u32(plan.spans[3].led_count, 2, "reverse span 3 count");
|
|
require(plan.spans[3].color == track_core::Color::cyan(), "reverse span 3 color");
|
|
}
|
|
|
|
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<std::uint8_t> data{
|
|
static_cast<std::uint8_t>(track_core::SchemeKind::speed_input_time_segmented_mileage_free),
|
|
static_cast<std::uint8_t>(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<std::vector<track_core::STSegment>>(&decoded->segments);
|
|
require(segments != nullptr, "decoded ST segment vector");
|
|
require_eq_u32(static_cast<std::uint32_t>(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<track_core::TrackPidSegment>(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<std::uint32_t>(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<std::uint32_t>(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<std::uint32_t>(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_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;
|
|
}
|