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:
@@ -0,0 +1,211 @@
|
||||
import pytest
|
||||
|
||||
import track_core as tc
|
||||
|
||||
|
||||
def running_info(color=None):
|
||||
info = tc.TrackInfo()
|
||||
info.color = color or tc.Color.green()
|
||||
info.id = 1
|
||||
info.is_running = True
|
||||
info.num_segments = 1
|
||||
return info
|
||||
|
||||
|
||||
def report(mileage):
|
||||
value = tc.TrackReport()
|
||||
value.id = 1
|
||||
value.state = tc.TrackState.run
|
||||
value.mileage_m = mileage
|
||||
value.speed_m_s = 1.0
|
||||
return value
|
||||
|
||||
|
||||
def config(draw_kind):
|
||||
value = tc.TrackConfig()
|
||||
value.draw_kind = draw_kind
|
||||
value.line_length_m = 10.0
|
||||
value.active_line_length_m = 4.0
|
||||
value.head_offset_m = 0.0
|
||||
value.line_leds_num = 20
|
||||
return value
|
||||
|
||||
|
||||
def test_circular_render_wraps():
|
||||
plan = tc.make_render_plan(
|
||||
config(tc.TrackDrawKind.circular),
|
||||
running_info(tc.Color.green()),
|
||||
report(8.5),
|
||||
)
|
||||
|
||||
assert plan.span_count == 2
|
||||
assert [(span.start_led, span.led_count, span.color) for span in plan.spans] == [
|
||||
(17, 3, tc.Color.green()),
|
||||
(0, 5, tc.Color.green()),
|
||||
]
|
||||
|
||||
|
||||
def test_linear_render_forward_pixels():
|
||||
pixels = tc.render_pixels(
|
||||
config(tc.TrackDrawKind.linear),
|
||||
running_info(tc.Color.red()),
|
||||
report(5.0),
|
||||
)
|
||||
|
||||
assert len(pixels) == 20
|
||||
assert pixels[6] == tc.Color.cyan()
|
||||
assert pixels[8] == tc.Color.red()
|
||||
assert pixels[10] == tc.Color.red()
|
||||
assert pixels[12] == tc.Color.blue()
|
||||
assert pixels[0] == tc.Color.black()
|
||||
|
||||
|
||||
def test_linear_render_reverse_spans():
|
||||
cfg = config(tc.TrackDrawKind.linear)
|
||||
cfg.active_line_length_m = 5.0
|
||||
plan = tc.make_render_plan(cfg, running_info(tc.Color.red()), report(15.0))
|
||||
|
||||
assert [(span.start_led, span.led_count, span.color) for span in plan.spans] == [
|
||||
(5, 2, tc.Color.blue()),
|
||||
(7, 3, tc.Color.red()),
|
||||
(10, 3, tc.Color.red()),
|
||||
(13, 2, tc.Color.cyan()),
|
||||
]
|
||||
|
||||
|
||||
def test_linear_render_boundary_sweep_does_not_raise():
|
||||
cfg = config(tc.TrackDrawKind.linear)
|
||||
for mileage in (
|
||||
-20.0,
|
||||
-10.1,
|
||||
-10.0,
|
||||
-9.9,
|
||||
-1.0,
|
||||
-0.1,
|
||||
0.0,
|
||||
0.1,
|
||||
1.0,
|
||||
9.9,
|
||||
10.0,
|
||||
10.1,
|
||||
19.9,
|
||||
20.0,
|
||||
20.1,
|
||||
29.9,
|
||||
30.0,
|
||||
30.1,
|
||||
40.0,
|
||||
):
|
||||
pixels = tc.render_pixels(cfg, running_info(tc.Color.red()), report(mileage))
|
||||
assert len(pixels) == cfg.line_leds_num
|
||||
|
||||
|
||||
def test_not_running_renders_black_pixels():
|
||||
info = running_info()
|
||||
info.is_running = False
|
||||
|
||||
pixels = tc.render_pixels(config(tc.TrackDrawKind.circular), info, report(8.5))
|
||||
|
||||
assert len(pixels) == 20
|
||||
assert all(pixel == tc.Color.black() for pixel in pixels)
|
||||
|
||||
|
||||
def test_invalid_config_raises_value_error():
|
||||
cfg = config(tc.TrackDrawKind.circular)
|
||||
cfg.line_leds_num = 0
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
tc.render_pixels(cfg, running_info(), report(0.0))
|
||||
|
||||
|
||||
def st_segment(speed, time_s):
|
||||
segment = tc.STSegment()
|
||||
segment.speed_m_s = speed
|
||||
segment.time_since_start_s = time_s
|
||||
return segment
|
||||
|
||||
|
||||
def sm_segment(speed, mileage):
|
||||
segment = tc.SMSegment()
|
||||
segment.speed_m_s = speed
|
||||
segment.mileage_from_start_m = mileage
|
||||
return segment
|
||||
|
||||
|
||||
def mt_segment(mileage, time_s):
|
||||
segment = tc.MTSegment()
|
||||
segment.mileage_to_travel_this_segment_m = mileage
|
||||
segment.time_since_start_s = time_s
|
||||
return segment
|
||||
|
||||
|
||||
def test_pure_speed_time_runtime_ticks():
|
||||
scheme = tc.make_speed_time_scheme(
|
||||
7,
|
||||
tc.Color.green(),
|
||||
tc.AccelerationProfile.instant,
|
||||
[st_segment(1.0, 0), st_segment(3.0, 5), st_segment(1.0, 10)],
|
||||
)
|
||||
runtime = tc.start_scheme_track(tc.make_scheme_track_runtime(scheme))
|
||||
runtime = tc.tick_scheme_track(config(tc.TrackDrawKind.circular), runtime, 6.0)
|
||||
state = runtime.state
|
||||
report = runtime.report()
|
||||
|
||||
assert state.primary_segment_index == 1
|
||||
assert report.state == tc.TrackState.run
|
||||
assert report.speed_m_s == pytest.approx(3.0)
|
||||
assert report.mileage_m == pytest.approx(6.0)
|
||||
|
||||
|
||||
def test_training_runtime_accepts_all_scheme_kinds_and_renders():
|
||||
runtime = tc.SchemeTrainingRuntime()
|
||||
|
||||
runtime.add_scheme(
|
||||
tc.make_speed_mileage_scheme(
|
||||
1,
|
||||
tc.Color.red(),
|
||||
tc.AccelerationProfile.instant,
|
||||
[sm_segment(1.0, 0), sm_segment(2.0, 5), sm_segment(1.0, 10)],
|
||||
)
|
||||
)
|
||||
runtime.add_scheme(
|
||||
tc.make_mileage_time_scheme(
|
||||
2,
|
||||
tc.Color.green(),
|
||||
tc.AccelerationProfile.instant,
|
||||
[mt_segment(10, 0), mt_segment(20, 5), mt_segment(1, 15)],
|
||||
)
|
||||
)
|
||||
runtime.add_scheme(
|
||||
tc.make_speed_time_scheme(
|
||||
3,
|
||||
tc.Color.blue(),
|
||||
tc.AccelerationProfile.instant,
|
||||
[st_segment(1.0, 0), st_segment(1.0, 10)],
|
||||
)
|
||||
)
|
||||
|
||||
repeated = tc.RepeatedSMSegment()
|
||||
repeated.time_since_start_s = 0
|
||||
repeated.speed_mileage_segments = [sm_segment(1.0, 0), sm_segment(2.0, 5)]
|
||||
repeated_end = tc.RepeatedSMSegment()
|
||||
repeated_end.time_since_start_s = 20
|
||||
repeated_end.speed_mileage_segments = [sm_segment(1.0, 0), sm_segment(1.0, 5)]
|
||||
runtime.add_scheme(
|
||||
tc.make_repeated_speed_mileage_time_scheme(
|
||||
4,
|
||||
tc.Color.white(),
|
||||
tc.AccelerationProfile.instant,
|
||||
[repeated, repeated_end],
|
||||
)
|
||||
)
|
||||
|
||||
runtime.start()
|
||||
runtime.tick(config(tc.TrackDrawKind.circular), 1.0)
|
||||
pixels = runtime.render_pixels(config(tc.TrackDrawKind.circular))
|
||||
|
||||
assert runtime.has_program()
|
||||
assert not runtime.all_stopped()
|
||||
assert len(runtime.state_collection()) == 4
|
||||
assert len(runtime.scheme_status()) == 4
|
||||
assert len(pixels) == 20
|
||||
Reference in New Issue
Block a user