Scaffold workspace, shared types and requirements spec
Cargo workspace with core/ble/fit/probe crates. crates/core/src/types.rs is the fixed contract between the BLE layer, ride engine and UI. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
//! The ride state machine: ties telemetry, physics and the active profile
|
||||
//! together and decides what to command the trainer.
|
||||
//!
|
||||
//! This is the piece the Tauri layer drives. It takes telemetry in, produces
|
||||
//! snapshots and control targets out, and knows nothing about BLE or the UI.
|
||||
|
||||
use crate::physics::PhysicsState;
|
||||
use crate::profile::{Position, Profile};
|
||||
use crate::types::{
|
||||
ControlMode, ControlTarget, RideSnapshot, RiderConfig, SafetyLimits, Telemetry,
|
||||
};
|
||||
|
||||
/// Something the session wants the outside world to do or know about.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum SessionEvent {
|
||||
/// Send this target to the trainer. Already clamped (SAF-3).
|
||||
Command(ControlTarget),
|
||||
/// A new snapshot is available for the UI.
|
||||
Snapshot(RideSnapshot),
|
||||
/// A non-looping profile reached its end.
|
||||
ProfileFinished,
|
||||
/// The rider crossed into a new lap.
|
||||
Lap { index: u32 },
|
||||
}
|
||||
|
||||
/// Ride lifecycle.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum RideStatus {
|
||||
Idle,
|
||||
Running,
|
||||
Paused,
|
||||
Finished,
|
||||
}
|
||||
|
||||
pub struct RideSession {
|
||||
pub config: RiderConfig,
|
||||
pub limits: SafetyLimits,
|
||||
pub mode: ControlMode,
|
||||
pub status: RideStatus,
|
||||
physics: PhysicsState,
|
||||
profile: Option<Profile>,
|
||||
/// Manual gradient trim applied on top of the profile's gradient.
|
||||
gradient_offset_pct: f32,
|
||||
elapsed_ms: u64,
|
||||
last_target: Option<ControlTarget>,
|
||||
}
|
||||
|
||||
impl RideSession {
|
||||
pub fn new(config: RiderConfig, limits: SafetyLimits) -> Self {
|
||||
Self {
|
||||
config,
|
||||
limits,
|
||||
mode: ControlMode::ManualGrade,
|
||||
status: RideStatus::Idle,
|
||||
physics: PhysicsState::default(),
|
||||
profile: None,
|
||||
gradient_offset_pct: 0.0,
|
||||
elapsed_ms: 0,
|
||||
last_target: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_profile(&mut self, profile: Profile) {
|
||||
self.profile = Some(profile);
|
||||
self.mode = ControlMode::Profile;
|
||||
}
|
||||
|
||||
pub fn profile(&self) -> Option<&Profile> {
|
||||
self.profile.as_ref()
|
||||
}
|
||||
|
||||
pub fn position(&self) -> Position {
|
||||
Position {
|
||||
elapsed_s: self.elapsed_ms as f64 / 1000.0,
|
||||
distance_m: self.physics.distance_m,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start(&mut self) {
|
||||
self.status = RideStatus::Running;
|
||||
}
|
||||
|
||||
pub fn pause(&mut self) {
|
||||
self.status = RideStatus::Paused;
|
||||
}
|
||||
|
||||
/// Adjust the manual gradient trim by `delta` percent (FR-4.2).
|
||||
pub fn nudge_gradient(&mut self, delta_pct: f32) {
|
||||
self.gradient_offset_pct += delta_pct;
|
||||
}
|
||||
|
||||
pub fn reset_gradient_offset(&mut self) {
|
||||
self.gradient_offset_pct = 0.0;
|
||||
}
|
||||
|
||||
/// Advance the ride by one tick.
|
||||
///
|
||||
/// Feeds telemetry into the physics model, advances the profile, and
|
||||
/// returns whatever the outside world needs to act on. Must be safe to call
|
||||
/// when paused (no distance accrues) and when telemetry is missing power
|
||||
/// (treat as zero rather than panicking).
|
||||
pub fn tick(&mut self, telemetry: Telemetry, dt_s: f32) -> Vec<SessionEvent> {
|
||||
let _ = (telemetry, dt_s);
|
||||
todo!("implemented in crates/core/src/session.rs — see AGENT task A")
|
||||
}
|
||||
|
||||
/// Build the snapshot the UI renders.
|
||||
pub fn snapshot(&self, telemetry: Telemetry) -> RideSnapshot {
|
||||
let _ = telemetry;
|
||||
todo!("implemented in crates/core/src/session.rs — see AGENT task A")
|
||||
}
|
||||
|
||||
/// The target that should be in force right now, before clamping.
|
||||
fn desired_target(&self) -> Option<ControlTarget> {
|
||||
todo!("implemented in crates/core/src/session.rs — see AGENT task A")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user