Files
BikeControl/crates/probe/src/main.rs
T
dtourolleandClaude Opus 5 be046f341c Make the BLE layer say what it actually received
Chasing a paddle that shifted on one platform and not the other cost
several rebuilds, and most of that was spent unable to tell two very
different situations apart: a pod that was sending nothing, and a pod
whose frames we were receiving and quietly discarding. The logs looked
identical because every decoder in this path fails by dropping.

`click.rs` now logs every notification with its length and bytes before
anything tries to interpret it, and the unhandled-frame line in
`controller.rs` carries the payload rather than just the type byte —
which is the least useful part of a frame you could not parse, since it
is usually the framing that is wrong and not the content.

The default log filter gains `bikecontrol_ble=debug`. It was `info`,
which silenced the entire crate that owns every BLE conversation —
subscribe failures included. Survivable on desktop where RUST_LOG can
override it; not on Android, which has no environment to set and is
exactly where the subscribe was failing.

Adds `probe listen`, which decodes nothing on purpose: the GATT tree with
descriptors (the CCCD value is where a subscribe goes wrong, and
`inspect` stops short of it), then a subscribe to every notifying
characteristic across all services, reporting each as ok or FAILED. Each
notification prints characteristic, length and raw bytes, and the summary
names the characteristics that stayed silent — the difference between a
quiet device and listening in the wrong place.

That last part is what settled this one: it recorded 304 button frames
from a pod the app was reporting as dead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 19:16:26 +02:00

82 lines
2.6 KiB
Rust

//! `probe` — BLE protocol discovery against real hardware.
//!
//! This is the Phase 0 tool from REQUIREMENTS.md §9: TASK-1 (enumerate the
//! D100's services, dump its capability characteristics, log decoded Indoor
//! Bike Data, resolve whether op code `0x11` works) and TASK-2 (write a control
//! command and confirm a physical resistance change).
//!
//! It is intentionally separate from the app: it prints raw bytes next to
//! decoded values (NFR-8) so that a decoder bug shows up as a disagreement on
//! screen rather than as a strange number in a chart.
mod cli;
mod commands;
use std::time::Duration;
use anyhow::Result;
use tracing_subscriber::EnvFilter;
/// How long to look for the device before giving up.
const SCAN_TIMEOUT: Duration = Duration::from_secs(20);
#[tokio::main]
async fn main() -> Result<()> {
let args = match cli::parse(std::env::args().skip(1)) {
Ok(args) => args,
Err(e) => {
eprintln!("error: {e}\n");
eprint!("{}", cli::USAGE);
std::process::exit(2);
}
};
init_logging(args.verbose);
match args.command {
cli::Command::Help => {
print!("{}", cli::USAGE);
Ok(())
}
cli::Command::Scan { duration, all } => commands::scan_cmd(duration, all).await,
cli::Command::Inspect { device } => commands::inspect(&device, SCAN_TIMEOUT).await,
cli::Command::Monitor { device, duration } => {
commands::monitor(&device, duration, SCAN_TIMEOUT).await
}
cli::Command::Set {
device,
target,
simulation,
hold,
} => commands::set(&device, target, simulation, hold, SCAN_TIMEOUT).await,
cli::Command::Listen {
device,
duration,
handshake,
} => commands::listen(&device, duration, handshake, SCAN_TIMEOUT).await,
cli::Command::Zwift {
device,
duration,
no_handshake,
buttons_only,
} => {
commands::zwift_cmd(&device, duration, no_handshake, buttons_only, SCAN_TIMEOUT).await
}
}
}
fn init_logging(verbose: bool) {
// `-v` turns on the raw-frame logging required by NFR-8. RUST_LOG still
// wins, so `RUST_LOG=trace` gets every notification.
let default = if verbose {
"bikecontrol_ble=debug,probe=debug,info"
} else {
"warn"
};
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::try_from_default_env().unwrap_or_else(|_| default.into()))
.with_target(false)
.without_time()
.init();
}