Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 `.`, `<CTRL-C>` or `<CTRL-D>`.

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
Expand Down
54 changes: 53 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -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)]
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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())?;

Comment thread
tchebb marked this conversation as resolved.
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)?;
Expand Down
57 changes: 34 additions & 23 deletions src/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,51 +139,62 @@ 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<String, Error> {
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<String, Error> {
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)
.map_err(|e| Error::ProtocolError(e.into()))?
.to_owned())
}

/// 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<String, Error> {
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;
Expand Down