//! FTMS client and BLE transport. See REQUIREMENTS.md §5.1–5.2. //! //! # Layout //! //! The crate is split so that everything protocol-shaped is a pure function //! over bytes, and only [`client`] and [`scan`] touch a radio. That is what //! lets the whole wire format be tested without a trainer on the desk: //! //! | Module | Contents | Needs hardware | //! |--------|----------|----------------| //! | [`uuids`] | FTMS assigned numbers | no | //! | [`indoor_bike_data`] | `0x2AD2` decoder | no | //! | [`control_point`] | `0x2AD9` encoders and response decoding | no | //! | [`capabilities`] | `0x2ACC`/`0x2AD5`/`0x2AD6`/`0x2AD8` decoding, and the safety gate | no | //! | [`zwift`] | Zwift's proprietary protocol (§2.3.1) | no | //! | [`scan`] | discovery | yes | //! | [`client`] | the connection actor | yes | //! //! # Usage //! //! ```no_run //! use bikecontrol_ble::{FtmsClient, FtmsConfig, TrainerSelector}; //! use bikecontrol_core::types::ControlTarget; //! //! # async fn example() -> Result<(), Box> { //! let client = FtmsClient::connect(TrainerSelector::Any, FtmsConfig::default()).await?; //! //! let mut telemetry = client.telemetry(); //! tokio::spawn(async move { //! while let Ok(sample) = telemetry.recv().await { //! println!("{:?} W", sample.power_w); //! } //! }); //! //! client.set_target(ControlTarget::Gradient { percent: 4.0 }).await?; //! //! // SAF-2: always leave the trainer at zero. //! client.shutdown().await?; //! # Ok(()) //! # } //! ``` //! //! # Attribution //! //! The FTMS field ordering, scaling and control-point encodings are ported from //! [`obostjancic/smart-trainer-control`](https://github.com/obostjancic/smart-trainer-control), //! MIT licensed, Copyright (c) 2025 Ogi — a working Van Rysel D100 client //! (REQUIREMENTS.md §3.2). Per-module attribution notes mark where. pub mod capabilities; pub mod click; pub mod client; pub mod control_point; pub mod error; pub mod indoor_bike_data; pub mod scan; pub mod uuids; pub mod zwift; pub use click::{ClickClient, ClickConfig, ClickEvent, PodSelector}; pub use capabilities::{ FitnessMachineFeature, InclinationRange, PowerRange, ResistanceLevelRange, TrainerCapabilities, UnsupportedTarget, }; pub use client::{ safety_reset_commands, Backoff, ControlOutcome, FtmsClient, FtmsConfig, FtmsEvent, Procedure, }; pub use control_point::{ControlPointResponse, OpCode, ResultCode, SimulationParameters, StopOrPause}; pub use error::FtmsError; pub use indoor_bike_data::{DecodeError, IndoorBikeData}; pub use scan::{ default_adapter, scan, scan_trainers, DiscoveredDevice, ScanKind, TrainerSelector, }; pub use uuids::FITNESS_MACHINE_SERVICE; pub use zwift::{ Button, ButtonBitmask, ClickButtons, DeviceKind as ZwiftDeviceKind, MessageType as ZwiftMessageType, PodId, };