Initial standalone track core

This commit is contained in:
2026-05-15 16:02:44 +08:00
commit 84598cad20
15 changed files with 1733 additions and 0 deletions
+49
View File
@@ -0,0 +1,49 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <vector>
#include "track_core/model.hpp"
#include "track_core/render.hpp"
namespace track_core {
class MemoryStrip {
public:
explicit MemoryStrip(std::size_t led_count = 0);
[[nodiscard]]
TrackError begin();
[[nodiscard]]
TrackError clear();
[[nodiscard]]
TrackError fill(std::size_t start, std::size_t count, Color color);
[[nodiscard]]
TrackError show();
[[nodiscard]]
TrackError set_leds_count(std::size_t count);
[[nodiscard]]
std::size_t leds_count() const;
[[nodiscard]]
std::uint64_t frame_sequence() const;
[[nodiscard]]
std::span<const Color> pixels() const;
private:
std::vector<Color> pixels_;
std::uint64_t frame_sequence_{};
bool begun_{};
};
[[nodiscard]]
TrackError apply_render_plan(MemoryStrip &strip, const TrackRenderPlan &plan);
} // namespace track_core
+330
View File
@@ -0,0 +1,330 @@
#pragma once
#include <algorithm>
#include <chrono>
#include <cstdint>
#include <expected>
#include <limits>
#include <optional>
#include <span>
#include <string>
#include <variant>
#include <vector>
namespace track_core {
template <typename T, typename E>
using expected = std::expected<T, E>;
template <typename E>
using unexpected = std::unexpected<E>;
using unit = std::monostate;
enum class TrackError : int {
ok = 0,
invalid_arg,
invalid_size,
invalid_state,
not_supported,
range,
};
enum class TrackDrawKind : std::uint8_t {
circular = 0,
linear = 1,
};
enum class SchemeKind : std::uint8_t {
speed_input_mileage_segmented_time_free = 0,
mileage_input_time_segmented_speed_free = 1,
speed_input_time_segmented_mileage_free = 2,
repeated_speed_input_mileage_segmentation_input_time_segmented = 3,
};
enum class AccelerationProfile : std::uint8_t {
instant = 0,
smooth = 1,
};
enum class TrackState : std::uint8_t {
stop = 0,
run = 1,
test_rainbow = 2,
test_blink = 3,
};
enum class TrackControllerMode : std::uint8_t {
scheme = 0,
pid_hr = 1,
};
enum class TrackPidStageKind : std::uint8_t {
none = 0,
constant = 1,
pid = 2,
};
struct Color {
std::uint8_t r{};
std::uint8_t g{};
std::uint8_t b{};
constexpr Color() = default;
constexpr Color(std::uint8_t red, std::uint8_t green, std::uint8_t blue)
: r(red), g(green), b(blue) {}
constexpr explicit Color(std::uint32_t value)
: r(static_cast<std::uint8_t>((value >> 16U) & 0xFFU)),
g(static_cast<std::uint8_t>((value >> 8U) & 0xFFU)),
b(static_cast<std::uint8_t>(value & 0xFFU)) {}
[[nodiscard]]
constexpr bool operator==(const Color &) const = default;
[[nodiscard]]
constexpr explicit operator std::uint32_t() const {
return (static_cast<std::uint32_t>(r) << 16U) |
(static_cast<std::uint32_t>(g) << 8U) |
static_cast<std::uint32_t>(b);
}
[[nodiscard]]
std::string hex() const;
[[nodiscard]]
static constexpr Color black() { return {0, 0, 0}; }
[[nodiscard]]
static constexpr Color red() { return {255, 0, 0}; }
[[nodiscard]]
static constexpr Color orange() { return {255, 165, 0}; }
[[nodiscard]]
static constexpr Color yellow() { return {255, 255, 0}; }
[[nodiscard]]
static constexpr Color green() { return {0, 255, 0}; }
[[nodiscard]]
static constexpr Color cyan() { return {0, 255, 255}; }
[[nodiscard]]
static constexpr Color blue() { return {0, 0, 255}; }
[[nodiscard]]
static constexpr Color indigo() { return {75, 0, 130}; }
[[nodiscard]]
static constexpr Color violet() { return {238, 130, 238}; }
[[nodiscard]]
static constexpr Color white() { return {255, 255, 255}; }
};
struct TrackConfig {
TrackDrawKind draw_kind{TrackDrawKind::circular};
float line_length_m{0.0F};
float active_line_length_m{0.0F};
float head_offset_m{0.0F};
std::uint16_t line_leds_num{0};
[[nodiscard]]
float led_distance() const {
return line_leds_num > 0 ? line_length_m / static_cast<float>(line_leds_num) : 0.0F;
}
[[nodiscard]]
bool verify() const {
if (line_length_m <= 0.0F || active_line_length_m <= 0.0F) {
return false;
}
if (active_line_length_m > line_length_m) {
return false;
}
if (head_offset_m >= line_length_m) {
return false;
}
return true;
}
[[nodiscard]]
static TrackConfig default_config() {
return {
.draw_kind = TrackDrawKind::circular,
.line_length_m = 400.0F,
.active_line_length_m = 10.0F,
.head_offset_m = 0.0F,
.line_leds_num = 400,
};
}
};
struct TrackInfo {
SchemeKind kind{SchemeKind::speed_input_time_segmented_mileage_free};
Color color{Color::white()};
std::uint8_t id{};
bool is_running{};
std::uint8_t num_segments{};
};
struct TrackReport {
std::uint8_t id{};
TrackState state{TrackState::stop};
float mileage_m{};
float speed_m_s{};
std::uint32_t time_elapsed_ms{};
};
struct TrackStateReportCollection {
std::vector<TrackReport> states;
};
struct TrackSchemeMgrRead {
struct Status {
std::uint8_t id{};
std::uint8_t segment_count{};
SchemeKind kind{SchemeKind::speed_input_time_segmented_mileage_free};
};
std::vector<Status> scheme_status;
};
struct SMSegment {
static constexpr float speed_lsb = 10.0F / 255.0F;
static constexpr std::size_t raw_size = 3;
float speed_m_s{};
std::uint16_t mileage_from_start_m{};
};
struct MTSegment {
std::uint16_t mileage_to_travel_this_segment_m{};
std::uint16_t time_since_start_s{};
};
struct STSegment {
static constexpr float speed_lsb = 10.0F / 255.0F;
static constexpr std::size_t raw_size = 3;
float speed_m_s{};
std::uint16_t time_since_start_s{};
};
struct RepeatedSMSegment {
std::vector<SMSegment> speed_mileage_segments;
std::uint16_t time_since_start_s{};
};
struct TrackPidFineTune {
std::uint8_t band_plus{};
std::uint8_t band_minus{};
float gain_scale{0.0F};
};
struct TrackPidSpeedSuppression {
float ratio_min{0.1F};
float sigma_m{1.0F};
};
struct TrackPidSegment {
static constexpr float default_kp = 0.0457F;
static constexpr float default_ki = 0.001464F;
static constexpr float default_kd = 0.0F;
static constexpr float default_slew_rate_limit_m_s = 0.225F;
std::uint16_t duration_s{0};
float min_speed_m_s{0.0F};
float max_speed_m_s{0.0F};
float kp{0.0F};
float ki{1.0F};
float kd{0.0F};
float slew_rate_limit{0.0F};
std::optional<TrackPidFineTune> fine_tune;
[[nodiscard]]
static TrackPidSegment default_segment() {
return {
.duration_s = 300,
.min_speed_m_s = 0.0F,
.max_speed_m_s = 4.0F,
.kp = default_kp,
.ki = default_ki,
.kd = default_kd,
.slew_rate_limit = default_slew_rate_limit_m_s,
.fine_tune = std::nullopt,
};
}
};
struct TrackConstantSegment {
std::uint16_t duration_s{0};
float speed_m_s{0.0F};
};
struct TrackPidSchema {
using variant_type = std::variant<TrackPidSegment, TrackConstantSegment>;
variant_type segment{TrackConstantSegment{}};
[[nodiscard]]
TrackPidStageKind kind() const {
return std::holds_alternative<TrackPidSegment>(segment)
? TrackPidStageKind::pid
: TrackPidStageKind::constant;
}
};
struct TrackPidConfig {
std::uint8_t band_id{0};
std::uint8_t target_hr_bpm{120};
std::uint8_t deadzone_bpm{3};
bool preemptive_pid_activation{false};
std::optional<TrackPidSpeedSuppression> speed_suppression;
std::vector<TrackPidSchema> schemas;
[[nodiscard]]
bool empty() const {
return schemas.empty();
}
[[nodiscard]]
static TrackPidConfig default_config() {
TrackPidConfig config;
config.schemas.push_back(TrackPidSchema{TrackPidSegment::default_segment()});
return config;
}
[[nodiscard]]
expected<unit, TrackError> validate(bool allow_empty = false) const;
};
struct TrackPidSetTargetHr {
std::uint8_t target_hr_bpm{120};
};
struct TrackPidSetTuning {
float min_speed_m_s{0.0F};
float max_speed_m_s{0.0F};
float kp{0.0F};
float ki{1.0F};
float kd{0.0F};
float slew_rate_limit{0.0F};
std::optional<TrackPidFineTune> fine_tune;
[[nodiscard]]
expected<unit, TrackError> validate() const;
void apply_to(TrackPidSegment &segment) const;
};
struct TrackPidRuntimeCommand {
std::variant<unit, TrackPidSetTargetHr, TrackPidSetTuning> command{unit{}};
};
struct TrackPidStatus {
std::uint8_t band_id{};
bool band_is_active{};
bool is_heart_rate_valid{};
std::uint8_t heart_rate_bpm{};
std::uint16_t step_count{};
std::uint8_t active_segment_index{};
TrackPidStageKind active_segment_kind{TrackPidStageKind::none};
float effective_speed_m_s{};
std::uint32_t remaining_program_ms{};
float base_speed_m_s{};
};
using clock = std::chrono::steady_clock;
} // namespace track_core
+116
View File
@@ -0,0 +1,116 @@
#pragma once
#include "track_core/model.hpp"
namespace track_core {
class TrackPidProgramState {
public:
using time_point = clock::time_point;
struct HrSample {
float heart_rate_bpm{0.0F};
float sample_interval_s{0.0F};
};
TrackPidProgramState() = delete;
TrackPidProgramState(time_point now, TrackPidConfig config);
[[nodiscard]]
float next(time_point now, std::optional<HrSample> fresh_hr_sample);
[[nodiscard]]
bool is_finished(time_point now) const;
[[nodiscard]]
TrackPidStageKind active_stage_kind() const;
[[nodiscard]]
std::uint8_t active_stage_index() const;
[[nodiscard]]
float commanded_speed_m_s() const;
[[nodiscard]]
std::uint32_t remaining_program_ms(time_point now) const;
void update_target_hr_bpm(std::uint8_t target_hr_bpm);
[[nodiscard]]
expected<unit, TrackError> update_tuning(const TrackPidSetTuning &tuning);
private:
struct pid_state {
struct fine_tune_state {
std::uint8_t band_plus{};
std::uint8_t band_minus{};
float gain_scale{1.0F};
};
float kp{0.0F};
float ki{1.0F};
float kd{0.0F};
std::optional<fine_tune_state> fine_tune;
float slew_rate_limit{0.0F};
float e_t_1{0.0F};
float e_t_2{0.0F};
float u_t_1{0.0F};
float min_speed_m_s{0.0F};
float max_speed_m_s{0.0F};
bool should_use_nominal_sample_interval{true};
void from_segment(const TrackPidSegment &segment);
void reset_with(float speed_m_s);
[[nodiscard]]
float effective_gain_scale(float target_hr, float current_hr) const;
[[nodiscard]]
float clamp_commanded_speed(float speed_m_s) const;
void apply_tuning(const TrackPidSetTuning &tuning);
};
[[nodiscard]]
const TrackPidSchema &schema() const;
[[nodiscard]]
static std::uint16_t schema_duration_s(const TrackPidSchema &schema);
[[nodiscard]]
bool sync_schema_for_time(time_point now);
void advance_to_schema_index(std::size_t target_index);
[[nodiscard]]
std::size_t resolve_schema_index(time_point now) const;
[[nodiscard]]
bool maybe_jump_to_final_pid(time_point now, std::optional<HrSample> fresh_hr_sample);
[[nodiscard]]
bool is_in_deadzone(float heart_rate) const;
[[nodiscard]]
float do_pid(float current_hr, float sample_interval_s);
void apply_schema(const TrackPidSchema &schema);
void finish();
[[nodiscard]]
static bool validate_preemptive_schema(const std::vector<TrackPidSchema> &schemas);
[[nodiscard]]
static time_point safe_add_seconds(time_point now, std::uint32_t duration_s);
TrackPidConfig config_;
time_point program_start_timestamp_{};
time_point program_end_timestamp_{};
std::size_t schema_index_{};
pid_state pid_{};
bool preemptive_enabled_{};
bool forced_final_pid_{};
bool finished_{};
};
} // namespace track_core
+36
View File
@@ -0,0 +1,36 @@
#pragma once
#include <array>
#include <cstdint>
#include "track_core/model.hpp"
namespace track_core {
struct TrackRenderSpan {
std::uint16_t start_led{};
std::uint16_t led_count{};
Color color{};
};
struct TrackRenderPlan {
static constexpr std::size_t max_spans = 4;
void add_fill(std::uint16_t start_led, std::uint16_t led_count, Color color);
[[nodiscard]]
bool empty() const {
return span_count == 0;
}
std::array<TrackRenderSpan, max_spans> spans{};
std::size_t span_count{};
};
[[nodiscard]]
TrackRenderPlan make_track_render_plan(
const TrackConfig &config,
const TrackInfo &info,
const TrackReport &report);
} // namespace track_core
+31
View File
@@ -0,0 +1,31 @@
#pragma once
#include <cstdint>
#include <span>
#include <variant>
#include <vector>
#include "track_core/model.hpp"
namespace track_core {
struct DecodedScheme {
std::uint8_t id{};
Color color{Color::white()};
SchemeKind kind{SchemeKind::speed_input_time_segmented_mileage_free};
AccelerationProfile acceleration_profile{AccelerationProfile::smooth};
std::variant<
std::vector<SMSegment>,
std::vector<MTSegment>,
std::vector<STSegment>,
std::vector<RepeatedSMSegment>>
segments;
};
[[nodiscard]]
expected<DecodedScheme, TrackError> decode_scheme(
std::uint8_t id,
Color color,
std::span<const std::uint8_t> binary);
} // namespace track_core