feat(track-core): add portable training runtimes
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.
This commit is contained in:
@@ -5,7 +5,9 @@
|
||||
|
||||
#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 {
|
||||
|
||||
@@ -129,6 +131,43 @@ void test_linear_render_reverse() {
|
||||
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);
|
||||
|
||||
@@ -176,15 +215,308 @@ void test_pid_program_constant() {
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user