Core ride logic, FTMS client, FIT encoder and probe CLI
Adds backing state for Resistance and Erg control modes, which had no value to hold and so could never satisfy FR-4.3/FR-4.6. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
//! Event channel from Rust to the webview.
|
||||
//!
|
||||
//! The frontend is a *view* (§4.3): it never computes ride state, it renders
|
||||
//! what arrives here. Every event name is declared once, in this module, and
|
||||
//! mirrored in `ui/src/lib/events.ts`.
|
||||
|
||||
use bikecontrol_core::types::{ConnectionState, ControlMode, ControlTarget};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::devices::DeviceInfo;
|
||||
use crate::profile_view::ProfileView;
|
||||
|
||||
/// `RideSnapshot`, pushed at [`crate::engine::TICK_HZ`].
|
||||
pub const RIDE_SNAPSHOT: &str = "ride://snapshot";
|
||||
/// Low-frequency ride state: status, mode, targets, laps, loaded profile.
|
||||
pub const RIDE_STATE: &str = "ride://state";
|
||||
/// A lap marker was inserted (FR-3.19 / FR-8.7).
|
||||
pub const RIDE_LAP: &str = "ride://lap";
|
||||
/// The full device list changed (FR-9.1).
|
||||
pub const DEVICES_UPDATED: &str = "devices://updated";
|
||||
/// One device changed connection or control state (FR-1.7, FR-9.3).
|
||||
pub const DEVICE_CONNECTION: &str = "devices://connection";
|
||||
/// User-facing message: confirmation, warning or error (FR-9.2).
|
||||
pub const APP_NOTICE: &str = "app://notice";
|
||||
/// Acknowledgement that an input registered, so the UI can flash (FR-9.9).
|
||||
pub const INPUT_ACK: &str = "app://input-ack";
|
||||
|
||||
/// Ride lifecycle, mirroring `bikecontrol_core::session::RideStatus` but
|
||||
/// serialisable across the IPC boundary.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum RideStatus {
|
||||
Idle,
|
||||
Running,
|
||||
Paused,
|
||||
Finished,
|
||||
}
|
||||
|
||||
/// Everything the ride screen needs that is *not* in a `RideSnapshot`.
|
||||
/// Emitted on change, not on a timer.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RideState {
|
||||
pub status: RideStatus,
|
||||
pub mode: ControlMode,
|
||||
pub target: Option<ControlTarget>,
|
||||
/// Manual gradient trim on top of the profile's base gradient (FR-4.2).
|
||||
pub gradient_offset_pct: f32,
|
||||
pub manual_gradient_pct: f32,
|
||||
pub resistance_level: i16,
|
||||
pub power_target_w: u16,
|
||||
pub lap: u32,
|
||||
pub laps: Vec<LapSummary>,
|
||||
pub profile: Option<ProfileView>,
|
||||
/// Which backend is driving the ride — `"mock"` until `crates/ble` lands.
|
||||
pub source: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LapSummary {
|
||||
pub index: u32,
|
||||
pub elapsed_ms: u64,
|
||||
pub distance_m: f64,
|
||||
pub avg_power_w: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ConnectionEvent {
|
||||
pub device_id: String,
|
||||
pub state: ConnectionState,
|
||||
/// FTMS control point acquired. Connected is *not* controllable (FR-9.3).
|
||||
pub control_acquired: bool,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum NoticeLevel {
|
||||
Info,
|
||||
Warn,
|
||||
Error,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Notice {
|
||||
pub level: NoticeLevel,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
impl Notice {
|
||||
pub fn info(message: impl Into<String>) -> Self {
|
||||
Self { level: NoticeLevel::Info, message: message.into() }
|
||||
}
|
||||
pub fn warn(message: impl Into<String>) -> Self {
|
||||
Self { level: NoticeLevel::Warn, message: message.into() }
|
||||
}
|
||||
pub fn error(message: impl Into<String>) -> Self {
|
||||
Self { level: NoticeLevel::Error, message: message.into() }
|
||||
}
|
||||
}
|
||||
|
||||
/// Confirms an intent was accepted, so the UI can flash the control (FR-9.9).
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct InputAck {
|
||||
pub action: String,
|
||||
pub detail: Option<String>,
|
||||
}
|
||||
|
||||
/// The full device list.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DeviceList {
|
||||
pub scanning: bool,
|
||||
pub devices: Vec<DeviceInfo>,
|
||||
}
|
||||
Reference in New Issue
Block a user