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:
2026-08-05 13:11:32 +02:00
co-authored by Claude Opus 5
commit eb216dd9e6
21 changed files with 2910 additions and 0 deletions
+117
View File
@@ -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")
}
}