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:
2026-08-05 13:34:27 +02:00
co-authored by Claude Opus 5
parent 3e106de2c5
commit 7c17ca6158
61 changed files with 20933 additions and 55 deletions
+93
View File
@@ -0,0 +1,93 @@
//! BikeControl desktop shell.
//!
//! This crate is *only* wiring: it owns the ride loop, exposes intents as Tauri
//! commands, and pushes state to the webview as events. The ride logic proper
//! lives in `bikecontrol-core`, and device I/O in `bikecontrol-ble` — the
//! webview reaches neither directly (§4.3).
pub mod backend;
pub mod commands;
pub mod derive;
pub mod devices;
pub mod events;
pub mod mock;
pub mod profile_view;
pub mod samples;
pub mod session_backend;
pub mod state;
use tauri::{Manager, RunEvent, WindowEvent};
use crate::state::AppState;
pub fn run() {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "info,bikecontrol_app_lib=debug".into()),
)
.init();
tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
.manage(AppState::new())
.invoke_handler(tauri::generate_handler![
// ride lifecycle
commands::ride_state,
commands::start_ride,
commands::pause_ride,
commands::resume_ride,
commands::toggle_pause,
commands::stop_ride,
commands::reset_ride,
// control modes and targets
commands::set_control_mode,
commands::cycle_control_mode,
commands::nudge_gradient,
commands::set_gradient,
commands::reset_gradient,
commands::set_target_resistance,
commands::set_target_power,
commands::mark_lap,
// configuration
commands::rider_config,
commands::set_rider_config,
commands::safety_limits,
commands::set_safety_limits,
// profiles
commands::load_profile_from_path,
commands::load_profile_from_text,
commands::preview_profile_yaml,
commands::clear_profile,
commands::sample_profiles,
// devices
commands::device_list,
commands::start_scan,
commands::stop_scan,
commands::connect_device,
commands::disconnect_device,
commands::forget_device,
commands::trainer_controllable,
])
.setup(|app| {
let handle = app.handle().clone();
// NFR-7: scanning starts immediately, not on a user click.
handle.state::<AppState>().lock().devices.start_scan();
state::spawn_ride_loop(handle.clone());
state::spawn_device_loop(handle.clone());
state::emit_devices(&handle);
state::emit_ride_state(&handle);
Ok(())
})
.build(tauri::generate_context!())
.expect("failed to start BikeControl")
.run(|app, event| {
// SAF-2 — on any exit path, hand the trainer back at zero load.
if let RunEvent::ExitRequested { .. } = &event {
state::release_trainer(app);
}
if let RunEvent::WindowEvent { event: WindowEvent::Destroyed, .. } = &event {
state::release_trainer(app);
}
});
}