diff --git a/Cargo.lock b/Cargo.lock index 7e6043d..9e010d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -76,6 +76,7 @@ dependencies = [ "hidapi", "log", "num_enum", + "rustyline", "thiserror", ] @@ -100,6 +101,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "clap" version = "4.5.36" @@ -141,6 +148,15 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + [[package]] name = "colorchoice" version = "1.0.3" @@ -194,6 +210,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + [[package]] name = "hashbrown" version = "0.15.2" @@ -283,6 +305,18 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "num_enum" version = "0.7.3" @@ -371,6 +405,25 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "rustyline" +version = "16.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62fd9ca5ebc709e8535e8ef7c658eb51457987e48c98ead2be482172accc408d" +dependencies = [ + "bitflags", + "cfg-if", + "clipboard-win", + "libc", + "log", + "memchr", + "nix", + "unicode-segmentation", + "unicode-width", + "utf8parse", + "windows-sys 0.59.0", +] + [[package]] name = "serde" version = "1.0.219" @@ -467,6 +520,18 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-width" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" + [[package]] name = "utf8parse" version = "0.2.2" diff --git a/Cargo.toml b/Cargo.toml index a041bfb..1155626 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ crc32fast = "1.2" anyhow = "1.0" clap = { version = "4.0", features = ["derive", "wrap_help"] } env_logger = { version = "0.11", default-features = false, features = ["auto-color", "humantime"] } +rustyline = { version = "16.0.0", default-features = false } [profile.release] strip = "symbols" diff --git a/README.md b/README.md index 3303fa0..784c4fe 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,7 @@ OPTIONS: SUBCOMMANDS: list List all connected Bose HID devices (vendor ID 0x05a7) info Get information about a specific device not in DFU mode + tap Run TAP commands on a specific device not in DFU mode enter-dfu Put a device into DFU mode leave-dfu Take a device out of DFU mode download Write firmware to a device in DFU mode @@ -128,6 +129,12 @@ download`, and `bose-dfu leave-dfu`, in that order. The other subcommands help you inspect the current state of devices and firmware files. Notable is `info`, which tells you the current firmware version a device is running. +The `tap` subcommand can be used to start an interactive shell with the device +allowing you to send maintenance commands to the device, useful for servicing +purposes (like putting the device into shipmode when changing the battery). +Refer to your product's service manual for available commands. To exit the shell +you may use a single `.`, `` or ``. + Subcommands that perform an operation on a device support arguments for selecting which device to talk to. You can use `-p` to select by USB product ID, `-s` to select by USB serial number, or both together. Additionally, the diff --git a/src/main.rs b/src/main.rs index d425e86..133919e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,13 +2,17 @@ use anyhow::{Context, Result, bail}; use clap::Parser; use hidapi::{DeviceInfo, HidApi, HidDevice}; use log::{info, warn}; +use rustyline::DefaultEditor; +use rustyline::error::ReadlineError; use std::io::Read; use std::path::Path; use thiserror::Error; use bose_dfu::device_ids::{DeviceCompat, DeviceMode, UsbId, identify_device}; use bose_dfu::dfu_file::parse as parse_dfu_file; -use bose_dfu::protocol::{download, ensure_idle, enter_dfu, leave_dfu, read_info_field}; +use bose_dfu::protocol::{ + download, ensure_idle, enter_dfu, leave_dfu, read_info_field, run_tap_command, +}; #[derive(Parser, Debug)] #[command(version, about)] @@ -22,6 +26,12 @@ enum Opt { spec: DeviceSpec, }, + /// Run TAP commands on a specific device not in DFU mode + Tap { + #[command(flatten)] + spec: DeviceSpec, + }, + /// Put a device into DFU mode EnterDfu { #[command(flatten)] @@ -103,6 +113,13 @@ fn main() -> Result<()> { read_info_field(&dev, CurrentFirmware)? ); } + Opt::Tap { spec } => { + let spec = DeviceSpec { + required_mode: Some(DeviceMode::Normal), + ..spec + }; + tap_command_loop(&spec.get_device(&api)?.0)?; + } Opt::EnterDfu { spec } => { let spec = DeviceSpec { required_mode: Some(DeviceMode::Normal), @@ -175,6 +192,41 @@ fn list_cmd(hidapi: &HidApi) { } } +fn tap_command_loop(device: &HidDevice) -> Result<()> { + let mut rl = DefaultEditor::new()?; + + loop { + let readline = rl.readline("> "); + match readline { + Ok(line) => { + if line.is_empty() { + continue; + } else if line == "." { + break; + } + rl.add_history_entry(line.as_str())?; + + let result = run_tap_command(device, line.as_bytes()); + println!("{result:?}"); + } + Err(ReadlineError::Interrupted) => { + println!("CTRL-C"); + break; + } + Err(ReadlineError::Eof) => { + println!("CTRL-D"); + break; + } + Err(err) => { + println!("Error: {err:?}"); + break; + } + } + } + + Ok(()) +} + fn download_cmd(dev: &HidDevice, info: &DeviceInfo, path: &Path, wildcard_fw: bool) -> Result<()> { let mut file = std::fs::File::open(path)?; let suffix = parse_dfu_file(&mut file)?; diff --git a/src/protocol.rs b/src/protocol.rs index c64c070..b986206 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -139,44 +139,38 @@ pub enum InfoField { CurrentFirmware, } -/// Read an information field (as listed in [InfoField]) from the normal firmware. `device` must -/// NOT be in DFU mode. -pub fn read_info_field(device: &HidDevice, field: InfoField) -> Result { - const INFO_REPORT_ID: u8 = 2; - const INFO_REPORT_LEN: usize = 126; - - use InfoField::*; +/// Run a "TAP command" on the device. This is the general way to communicate with Bose devices. +/// 'device' must NOT be in DFU mode. +pub fn run_tap_command(device: &HidDevice, tap_bytes: &[u8]) -> Result { + const TAP_REPORT_ID: u8 = 2; + const TAP_REPORT_LEN: usize = 126; - // 1 byte report ID + 2 bytes field ID + 1 byte NUL - let mut request_report = [0u8; 1 + 2 + 1]; + // 1 byte report ID + command length + 1 byte NUL + let mut request_report = vec![0u8; 1 + tap_bytes.len() + 1].into_boxed_slice(); - // Packet captures indicate that "lc" is also a valid field type for some devices, but on mine - // it always returns a bus error (both when I send it and when the official updater does). - request_report[0] = INFO_REPORT_ID; - request_report[1..3].copy_from_slice(match field { - DeviceModel => b"pl", - SerialNumber => b"sn", - CurrentFirmware => b"vr", - }); + request_report[0] = TAP_REPORT_ID; + request_report[1..tap_bytes.len() + 1].copy_from_slice(tap_bytes); device .send_feature_report(&request_report) .map_err(|e| Error::DeviceIoError { source: e, - action: "requesting info field", + action: "running TAP command", })?; - let mut response_report = [0u8; 1 + INFO_REPORT_LEN]; - response_report[0] = INFO_REPORT_ID; + let mut response_report = [0u8; 1 + TAP_REPORT_LEN]; + response_report[0] = TAP_REPORT_ID; map_gfr( device.get_feature_report(&mut response_report), 1, - "reading info field", + "reading TAP command response", )?; - trace!("Raw {field:?} info field: {response_report:02x?}"); + trace!( + "Raw {:?} TAP command response: {response_report:02x?}", + tap_bytes.escape_ascii() + ); - // Result is all the bytes after the report ID and before the first NUL. let result = response_report[1..].split(|&x| x == 0).next().unwrap(); Ok(std::str::from_utf8(result) @@ -184,6 +178,23 @@ pub fn read_info_field(device: &HidDevice, field: InfoField) -> Result Result { + use InfoField::*; + + // Packet captures indicate that "lc" is also a valid field type for some devices, but on mine + // it always returns a bus error (both when I send it and when the official updater does). + run_tap_command( + device, + match field { + DeviceModel => b"pl", + SerialNumber => b"sn", + CurrentFirmware => b"vr", + }, + ) +} + /// Put a device running the normal firmware into DFU mode. `device` must NOT be in DFU mode. pub fn enter_dfu(device: &HidDevice) -> Result<(), Error> { const ENTER_DFU_REPORT_ID: u8 = 1;