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>
This commit is contained in:
+25
-1
@@ -21,6 +21,9 @@ SUBCOMMANDS:
|
||||
monitor <ADDR> Stream Indoor Bike Data as raw hex alongside decoded fields
|
||||
set <ADDR> <TARGET> Take control and apply a target, then reset the trainer to zero
|
||||
zwift <ADDR> Talk to Zwift's custom service: handshake, then log every frame
|
||||
listen <ADDR> Raw GATT: dump services, characteristics and descriptors, then
|
||||
subscribe to everything and print each notification's
|
||||
characteristic, length and bytes, undecoded
|
||||
|
||||
TARGET (for `set`):
|
||||
gradient=<PCT> SetTargetInclination (0x03), e.g. gradient=4.5
|
||||
@@ -33,7 +36,9 @@ OPTIONS:
|
||||
scan 6, monitor 30, set 15, zwift 60)
|
||||
--all `scan`: list every peripheral, not just fitness machines
|
||||
--name <SUBSTR> use in place of <ADDR> to match on advertised name
|
||||
--no-handshake `zwift`: subscribe and listen without writing RideOn
|
||||
--no-handshake `zwift`, `listen`: subscribe and listen without writing RideOn.
|
||||
`listen` greets by default, because a Click that has not been
|
||||
greeted sends nothing and an empty capture proves nothing
|
||||
--buttons `zwift`: collapse the ~10 Hz button stream to one line per
|
||||
press and release, for mapping bits to physical buttons
|
||||
-v, --verbose debug-level logging, including every raw BLE frame (NFR-8)
|
||||
@@ -70,6 +75,20 @@ pub enum Command {
|
||||
simulation: bool,
|
||||
hold: Duration,
|
||||
},
|
||||
/// Raw GATT interrogation: the whole tree including descriptors, then every
|
||||
/// notifying characteristic dumped as unparsed bytes.
|
||||
///
|
||||
/// Deliberately knows nothing about FTMS or Zwift. When a device works on
|
||||
/// one platform and not another, the question is which characteristic
|
||||
/// carried what and how long it was — and any decoding layer is exactly
|
||||
/// what you cannot trust while asking it.
|
||||
Listen {
|
||||
device: Device,
|
||||
duration: Duration,
|
||||
/// Write the Zwift `RideOn` handshake first. A Click streams nothing
|
||||
/// until it is greeted, so listening alone hears silence from one.
|
||||
handshake: bool,
|
||||
},
|
||||
/// Phase 3 / TASK-0: exercise Zwift's custom service on whatever advertises
|
||||
/// it — a Click, or the trainer itself.
|
||||
Zwift {
|
||||
@@ -190,6 +209,11 @@ pub fn parse<I: IntoIterator<Item = String>>(argv: I) -> Result<Args> {
|
||||
hold: Duration::from_secs(secs.unwrap_or(15)),
|
||||
}
|
||||
}
|
||||
"listen" => Command::Listen {
|
||||
device: device(&positional, 1)?,
|
||||
duration: Duration::from_secs(secs.unwrap_or(60)),
|
||||
handshake: !no_handshake,
|
||||
},
|
||||
"zwift" => Command::Zwift {
|
||||
device: device(&positional, 1)?,
|
||||
duration: Duration::from_secs(secs.unwrap_or(60)),
|
||||
|
||||
Reference in New Issue
Block a user