refactor(track-core): remove app-facing track adapters

Keep track-core focused on portable track state, scheme runtime, PID runtime, memory strip, and render planning.

Remove the ESP-IDF/nanopb Track adapter headers and sources from the submodule so app/protobuf ownership can live in a separate firmware component.

The standalone CMake and Python binding surface remains unchanged, including the emulator-facing APIs.
This commit is contained in:
2026-06-18 17:53:14 +08:00
parent 28327ed875
commit cd5c2f64e0
8 changed files with 7 additions and 1710 deletions
+2 -12
View File
@@ -9,21 +9,11 @@ set(TRACK_CORE_SOURCES
) )
if(DEFINED IDF_TARGET) if(DEFINED IDF_TARGET)
set(TRACK_CORE_IDF_SOURCES
src/esp/app_track_decoder.cpp
src/esp/app_track_drawer.cpp
src/esp/app_track_model.cpp
)
idf_component_register( idf_component_register(
SRCS ${TRACK_CORE_SOURCES} ${TRACK_CORE_IDF_SOURCES} SRCS ${TRACK_CORE_SOURCES}
INCLUDE_DIRS include INCLUDE_DIRS include
REQUIRES
app_proto
app_strip_if
app_utils
app_utils_clock
) )
target_compile_features(${COMPONENT_LIB} PUBLIC cxx_std_23)
else() else()
cmake_minimum_required(VERSION 3.20) cmake_minimum_required(VERSION 3.20)
project(track_core VERSION 0.1.0 LANGUAGES CXX) project(track_core VERSION 0.1.0 LANGUAGES CXX)
+5 -1
View File
@@ -5,6 +5,10 @@ Standalone C++ core for TrackBackFwd track simulation and strip rendering.
This library is intentionally platform-neutral. It does not depend on ESP-IDF, This library is intentionally platform-neutral. It does not depend on ESP-IDF,
FreeRTOS, protobuf/nanopb, BLE, or hardware LED drivers. FreeRTOS, protobuf/nanopb, BLE, or hardware LED drivers.
Firmware protobuf/ESP-IDF adapter types for the Track service live in
`components/app_track_model` in the parent project. Keep this package focused on
portable track state, scheme decoding/runtime, PID runtime, and render planning.
## Build ## Build
```bash ```bash
@@ -14,7 +18,7 @@ ctest --test-dir build --output-on-failure
``` ```
When used inside an ESP-IDF project under `components/track-core`, the same When used inside an ESP-IDF project under `components/track-core`, the same
`CMakeLists.txt` registers an IDF component. `CMakeLists.txt` registers an IDF component for the portable core only.
## Python bindings ## Python bindings
-66
View File
@@ -1,66 +0,0 @@
#pragma once
#include <variant>
#include <vector>
#include "app_track_model.hpp"
#include "track_core/scheme_decoder.hpp"
namespace app::track {
struct TrackSchemeDecoder {
using proto_type = track_app_TrackScheme;
static inline const pb_msgdesc_t *pb_fields = &track_app_TrackScheme_msg;
TrackSchemeDecoder() = default;
[[nodiscard]]
static TrackSchemeDecoder from_proto(const proto_type &proto);
[[nodiscard]]
proto_type to_proto() const;
[[nodiscard]]
expected<track_core::DecodedScheme, error_t> decode_core() const;
uint8_t id = 0;
std::vector<uint8_t> binary;
Color color;
};
struct TrackSchemeMgr {
using proto_type = track_app_TrackSchemeMgr;
static inline const pb_msgdesc_t *pb_fields = &track_app_TrackSchemeMgr_msg;
struct Add {
using proto_type = track_app_TrackSchemeMgrAdd;
static inline const pb_msgdesc_t *pb_fields = &track_app_TrackSchemeMgrAdd_msg;
TrackSchemeDecoder scheme_decoder;
error_t err{ESP_OK};
static Add from_proto(const proto_type &proto);
[[nodiscard]]
proto_type to_proto() const;
};
struct Clear {};
using Unknown = std::monostate;
enum class MessageType : uint8_t {
NONE = 0,
ADD = 1,
CLEAR = 2
};
explicit TrackSchemeMgr() = default;
static TrackSchemeMgr from_proto(const proto_type &proto);
[[nodiscard]]
proto_type to_proto() const;
std::variant<Unknown, Add, Clear> choice{Unknown{}};
};
} // namespace app::track
-40
View File
@@ -1,40 +0,0 @@
#ifndef D2AF35D4_5EEF_406F_A3F6_F7E6F2AA24C4
#define D2AF35D4_5EEF_406F_A3F6_F7E6F2AA24C4
#include "app_strip_if.hpp"
#include "app_track_model.hpp"
#include "track_core/render.hpp"
#include <array>
#include <cstdint>
namespace app::track {
struct TrackRenderSpan {
uint16_t start_led{};
uint16_t led_count{};
Color color;
};
struct TrackRenderPlan {
static constexpr size_t MAX_SPANS = 4;
void add_fill(uint16_t start_led, uint16_t led_count, Color color);
[[nodiscard]]
bool empty() const {
return span_count == 0;
}
std::array<TrackRenderSpan, MAX_SPANS> spans{};
size_t span_count{};
};
[[nodiscard]]
TrackRenderPlan make_track_render_plan(const TrackConfig &config, const TrackInfo &info, const report &rep);
void apply_render_plan(app::strip::StripView strip, const TrackRenderPlan &plan);
[[nodiscard]]
track_core::TrackRenderSink make_track_render_sink(app::strip::StripView &strip);
}
#endif /* D2AF35D4_5EEF_406F_A3F6_F7E6F2AA24C4 */
File diff suppressed because it is too large Load Diff
-121
View File
@@ -1,121 +0,0 @@
#include "app_track_decoder.hpp"
#include <algorithm>
#include <cstddef>
#include <cstdint>
namespace app::track {
namespace {
track_core::Color to_core(const Color &color) {
return {
color.inner.r,
color.inner.g,
color.inner.b,
};
}
error_t from_core(track_core::TrackError error) {
switch (error) {
case track_core::TrackError::ok:
return ESP_OK;
case track_core::TrackError::invalid_arg:
return ESP_ERR_INVALID_ARG;
case track_core::TrackError::invalid_size:
return ESP_ERR_INVALID_SIZE;
case track_core::TrackError::invalid_state:
return ESP_ERR_INVALID_STATE;
case track_core::TrackError::not_supported:
return ESP_ERR_NOT_SUPPORTED;
case track_core::TrackError::range:
return ESP_ERR_INVALID_SIZE;
}
return ESP_FAIL;
}
} // namespace
TrackSchemeDecoder TrackSchemeDecoder::from_proto(const proto_type &proto) {
TrackSchemeDecoder scheme;
scheme.id = proto.id;
if (proto.has_color) {
scheme.color = Color::from_proto(proto.color);
} else {
scheme.color = Color::white();
}
auto data = std::span<const uint8_t>(proto.data.bytes, proto.data.size);
std::ranges::copy(data, std::back_inserter(scheme.binary));
return scheme;
}
TrackSchemeDecoder::proto_type TrackSchemeDecoder::to_proto() const {
proto_type proto = track_app_TrackScheme_init_default;
proto.id = id;
proto.has_color = true;
proto.color = color.to_proto();
const auto data = std::span<uint8_t>(proto.data.bytes);
const auto count = std::min(data.size(), binary.size());
std::ranges::copy(binary.begin(), binary.begin() + static_cast<std::ptrdiff_t>(count), data.begin());
proto.data.size = count;
return proto;
}
expected<track_core::DecodedScheme, error_t> TrackSchemeDecoder::decode_core() const {
using ue = unexpected<error_t>;
if (binary.empty()) {
return ue{ESP_ERR_INVALID_ARG};
}
const auto decoded = track_core::decode_scheme(id, to_core(color), binary);
if (!decoded) {
return ue{from_core(decoded.error())};
}
return *decoded;
}
TrackSchemeMgr::Add TrackSchemeMgr::Add::from_proto(const proto_type &proto) {
Add add;
assert(proto.has_scheme);
add.scheme_decoder = TrackSchemeDecoder::from_proto(proto.scheme);
return add;
}
TrackSchemeMgr::Add::proto_type TrackSchemeMgr::Add::to_proto() const {
proto_type proto = track_app_TrackSchemeMgrAdd_init_default;
proto.has_scheme = true;
proto.scheme = scheme_decoder.to_proto();
return proto;
}
TrackSchemeMgr TrackSchemeMgr::from_proto(const proto_type &proto) {
TrackSchemeMgr mgmt;
switch (proto.which_msg) {
case track_app_TrackSchemeMgr_add_tag:
mgmt.choice = Add::from_proto(proto.msg.add);
break;
case track_app_TrackSchemeMgr_clear_tag:
mgmt.choice = Clear{};
break;
default:
break;
}
return mgmt;
}
TrackSchemeMgr::proto_type TrackSchemeMgr::to_proto() const {
proto_type proto = track_app_TrackSchemeMgr_init_default;
std::visit(app::utils::overloads{
[](Unknown) {},
[&](const Add &add) {
proto.which_msg = track_app_TrackSchemeMgr_add_tag;
proto.msg.add = add.to_proto();
},
[&](Clear) {
proto.which_msg = track_app_TrackSchemeMgr_clear_tag;
proto.msg.clear = track_app_TrackSchemeMgrClear_init_default;
}},
choice);
return proto;
}
} // namespace app::track
-177
View File
@@ -1,177 +0,0 @@
#include "app_track_drawer.hpp"
#include <cassert>
#include <cstddef>
#include "track_core/render.hpp"
namespace {
track_core::Color to_core(const app::track::Color &color) {
return {
color.inner.r,
color.inner.g,
color.inner.b,
};
}
app::track::Color from_core(const track_core::Color &color) {
return {
color.r,
color.g,
color.b,
};
}
track_core::TrackDrawKind to_core(track_app_TrackDrawKind draw_kind) {
switch (draw_kind) {
case track_app_TrackDrawKind_CIRCULAR:
return track_core::TrackDrawKind::circular;
case track_app_TrackDrawKind_LINEAR:
return track_core::TrackDrawKind::linear;
}
return track_core::TrackDrawKind::circular;
}
track_core::SchemeKind to_core(app::track::SchemeKind kind) {
switch (kind) {
case track_app_TrackSchemeKind_SPEED_INPUT_MILEAGE_SEGMENTED_TIME_FREE:
return track_core::SchemeKind::speed_input_mileage_segmented_time_free;
case track_app_TrackSchemeKind_MILEAGE_INPUT_TIME_SEGMENTED_SPEED_FREE:
return track_core::SchemeKind::mileage_input_time_segmented_speed_free;
case track_app_TrackSchemeKind_SPEED_INPUT_TIME_SEGMENTED_MILEAGE_FREE:
return track_core::SchemeKind::speed_input_time_segmented_mileage_free;
case track_app_TrackSchemeKind_REPEATED_SPEED_INPUT_MILEAGE_SEGMENTATION_INPUT_TIME_SEGMENTED:
return track_core::SchemeKind::repeated_speed_input_mileage_segmentation_input_time_segmented;
}
return track_core::SchemeKind::speed_input_time_segmented_mileage_free;
}
track_core::TrackState to_core(app::track::TrackState state) {
switch (state) {
case track_app_TrackState_STOP:
return track_core::TrackState::stop;
case track_app_TrackState_RUN:
return track_core::TrackState::run;
case track_app_TrackState_TEST_DISPLAY:
return track_core::TrackState::test_display;
}
return track_core::TrackState::stop;
}
track_core::TrackConfig to_core(const app::track::TrackConfig &config) {
return {
.draw_kind = to_core(config.draw_kind),
.line_length_m = config.line_length_m,
.active_line_length_m = config.active_line_length_m,
.head_offset_m = config.head_offset_m,
.line_leds_num = config.line_leds_num,
};
}
track_core::TrackInfo to_core(const app::track::TrackInfo &info) {
return {
.kind = to_core(info.kind),
.color = to_core(info.color),
.id = info.id,
.is_running = info.is_running,
.num_segments = info.num_segments,
};
}
track_core::TrackReport to_core(const app::track::report &report) {
return {
.id = report.id,
.state = to_core(report.state),
.mileage_m = report.mileage_m,
.speed_m_s = report.speed_m_s,
.time_elapsed_ms = report.time_elapsed_ms,
};
}
track_core::TrackError to_core(error_t error) {
if (error == ESP_OK) {
return track_core::TrackError::ok;
}
return track_core::TrackError::invalid_state;
}
app::strip::StripView *strip_from_context(void *context) {
return static_cast<app::strip::StripView *>(context);
}
track_core::TrackError strip_clear(void *context) {
auto *strip = strip_from_context(context);
if (strip == nullptr) {
return track_core::TrackError::invalid_arg;
}
return to_core((*strip)->clear());
}
track_core::TrackError strip_fill(
void *context,
std::uint16_t start_led,
std::uint16_t led_count,
track_core::Color color) {
auto *strip = strip_from_context(context);
if (strip == nullptr) {
return track_core::TrackError::invalid_arg;
}
return to_core((*strip)->fill(start_led, led_count, static_cast<std::uint32_t>(from_core(color))));
}
track_core::TrackError strip_show(void *context) {
auto *strip = strip_from_context(context);
if (strip == nullptr) {
return track_core::TrackError::invalid_arg;
}
return to_core((*strip)->show());
}
} // namespace
namespace app::track {
void TrackRenderPlan::add_fill(uint16_t start_led, uint16_t led_count, Color color) {
if (led_count == 0) {
return;
}
assert(span_count < spans.size() && "TrackRenderPlan capacity exceeded");
spans[span_count++] = TrackRenderSpan{
.start_led = start_led,
.led_count = led_count,
.color = color,
};
}
TrackRenderPlan make_track_render_plan(const TrackConfig &config, const TrackInfo &info, const report &rep) {
const auto core_plan = track_core::make_track_render_plan(
to_core(config),
to_core(info),
to_core(rep));
TrackRenderPlan plan{};
for (size_t i = 0; i < core_plan.span_count; ++i) {
const auto &span = core_plan.spans[i];
plan.add_fill(span.start_led, span.led_count, from_core(span.color));
}
return plan;
}
void apply_render_plan(app::strip::StripView strip, const TrackRenderPlan &plan) {
for (size_t i = 0; i < plan.span_count; ++i) {
const auto &span = plan.spans[i];
strip->fill(span.start_led, span.led_count, span.color);
}
}
track_core::TrackRenderSink make_track_render_sink(app::strip::StripView &strip) {
return track_core::TrackRenderSink{
.context = &strip,
.clear = strip_clear,
.fill = strip_fill,
.show = strip_show,
};
}
} // namespace app::track
-107
View File
@@ -1,107 +0,0 @@
#include "app_track_model.hpp"
#include "app_track.pb.h"
#include "esp_log.h"
namespace {
app::track::config_getter global_config_getter{nullptr};
constexpr auto TAG = "app::track";
}
namespace app::track {
const char *to_str(TrackState status) {
using enum TrackState;
switch (status) {
case track_app_TrackState_STOP:
return "STOP";
case track_app_TrackState_RUN:
return "RUN";
case track_app_TrackState_TEST_DISPLAY:
return "TEST_DISPLAY";
}
return "UNKNOWN";
}
const char *to_str(TrackControllerMode mode) {
switch (mode) {
case track_app_TrackControllerMode_SCHEME:
return "SCHEME";
case track_app_TrackControllerMode_PID_HR:
return "PID_HR";
}
return "UNKNOWN";
}
const char *to_str(TrackPidStageKind kind) {
switch (kind) {
case track_app_TrackPidStageKind_NONE:
return "NONE";
case track_app_TrackPidStageKind_CONSTANT:
return "CONSTANT";
case track_app_TrackPidStageKind_PID:
return "PID";
}
return "UNKNOWN";
}
const char *to_str(track_app_TrackDrawKind draw_kind) {
switch (draw_kind) {
case track_app_TrackDrawKind_CIRCULAR:
return "CIRCULAR";
case track_app_TrackDrawKind_LINEAR:
return "LINEAR";
}
return "UNKNOWN";
}
const char *to_str(AccelerationProfile profile) {
switch (profile) {
case track_app_TrackAccelerationProfile_SMOOTH:
return "SMOOTH";
case track_app_TrackAccelerationProfile_INSTANT:
return "INSTANT";
}
return "UNKNOWN";
}
void set_global_config_getter(config_getter getter) {
global_config_getter = std::move(getter);
}
const TrackConfig &global_config() {
if (not global_config_getter) {
static const auto DEFAULT_CONFIG = TrackConfig::Default();
ESP_LOGW(TAG, "unset global config getter");
return DEFAULT_CONFIG;
}
return global_config_getter();
}
void TrackConfig::log(const char *tag, esp_log_level_t level) const {
ESP_LOG_LEVEL(level, tag,
"TrackConfig{.draw_kind=%s, "
".line_length_m=%.2f, "
".active_line_length_m=%.2f, "
".head_offset_m=%.2f, "
".line_leds_num=%" PRIu16
"}",
to_str(draw_kind),
line_length_m,
active_line_length_m,
head_offset_m,
line_leds_num);
}
void TrackStateReportCollection::log(const char *tag, esp_log_level_t level) const {
for (size_t i = 0; i < states.size(); ++i) {
const auto &report = states[i];
ESP_LOG_LEVEL(level, tag,
"[%zu] Report{.id=%" PRIu8 ", .state=%s, .mileage_m=%.2f, .speed_m_s=%.2f, .time_elapsed_ms=%" PRIu32 "}",
i,
report.id,
to_str(report.state),
report.mileage_m,
report.speed_m_s,
report.time_elapsed_ms);
}
}
}