diff --git a/Cargo.lock b/Cargo.lock
index 785c721..f567e69 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -253,6 +253,22 @@ dependencies = [
"memchr",
]
+[[package]]
+name = "core-foundation"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
@@ -397,7 +413,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef033ed5e9bad94e55838ca0ca906db0e043f517adda0c8b79c7a8c66c93c1b5"
dependencies = [
"cfg-if",
- "rustix",
+ "rustix 0.38.44",
"windows-sys 0.48.0",
]
@@ -529,15 +545,25 @@ dependencies = [
"hashbrown 0.17.0",
]
+[[package]]
+name = "io-kit-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06d3a048d09fbb6597dbf7c69f40d14df4a49487db1487191618c893fc3b1c26"
+dependencies = [
+ "core-foundation-sys",
+ "mach2",
+]
+
[[package]]
name = "iqos"
version = "1.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8d642c991ed9c909971bbc6e0679ae20ba5f9c063e92aa0b1227a657860db51f"
+source = "git+https://github.com/okhsunrog/iqos?branch=agent%2Fusb-transport#e02a9991992eded1b4256aa3d4a06bf8ee842631"
dependencies = [
"async-trait",
"btleplug",
"futures",
+ "nusb",
"thiserror 2.0.12",
"tokio",
"uuid",
@@ -651,6 +677,12 @@ version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
+[[package]]
+name = "linux-raw-sys"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
+
[[package]]
name = "lock_api"
version = "0.4.12"
@@ -667,6 +699,15 @@ version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
+[[package]]
+name = "mach2"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a1b95cd5421ec55b445b5ae102f5ea0e768de1f82bd3001e11f426c269c3aea"
+dependencies = [
+ "libc",
+]
+
[[package]]
name = "memchr"
version = "2.7.4"
@@ -704,6 +745,25 @@ dependencies = [
"libc",
]
+[[package]]
+name = "nusb"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "215a49c10e8cff30a0da93b258b21846c4e9242f7683748cfa8474fdc2e4d22b"
+dependencies = [
+ "core-foundation",
+ "core-foundation-sys",
+ "futures-core",
+ "io-kit-sys",
+ "linux-raw-sys 0.12.1",
+ "log",
+ "once_cell",
+ "rustix 1.1.4",
+ "slab",
+ "tokio",
+ "windows-sys 0.61.2",
+]
+
[[package]]
name = "objc-sys"
version = "0.3.5"
@@ -865,7 +925,20 @@ dependencies = [
"bitflags 2.9.0",
"errno",
"libc",
- "linux-raw-sys",
+ "linux-raw-sys 0.4.15",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "rustix"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
+dependencies = [
+ "bitflags 2.9.0",
+ "errno",
+ "libc",
+ "linux-raw-sys 0.12.1",
"windows-sys 0.59.0",
]
diff --git a/Cargo.toml b/Cargo.toml
index 9116578..a513e61 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,7 +11,7 @@ dirs = "5"
btleplug = "0.11"
colored = "3.0"
futures = "0.3"
-iqos = { version = "1.1.1", features = ["btleplug-support"] }
+iqos = { git = "https://github.com/okhsunrog/iqos", branch = "agent/usb-transport", features = ["btleplug-support", "usb-support"] }
rustyline = "11.0"
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.0", features = ["full"] }
diff --git a/README.md b/README.md
index 8f7a0c2..9b63ab3 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
-**A command-line interface for controlling IQOS devices via Bluetooth Low Energy, built on [V-VX/iqos](https://github.com/V-VX/iqos)**
+**A command-line interface for controlling IQOS devices over Bluetooth Low Energy or USB, built on [V-VX/iqos](https://github.com/V-VX/iqos)**
[](https://www.rust-lang.org/)
[](LICENSE)
@@ -32,15 +32,16 @@
## Overview
-IQOS CLI is a Rust-based command-line tool for controlling IQOS devices over Bluetooth Low Energy, built on top of [V-VX/iqos](https://github.com/V-VX/iqos). It supports both an interactive REPL and one-shot command execution, so you can either connect once and work from the `iqos>` prompt or run a single command directly from your shell.
+IQOS CLI is a Rust-based command-line tool for controlling IQOS devices over Bluetooth Low Energy or USB, built on top of [V-VX/iqos](https://github.com/V-VX/iqos). It supports both an interactive REPL and one-shot command execution, so you can either connect once and work from the `iqos>` prompt or run a single command directly from your shell.
## Architecture
-All device protocol logic — BLE framing, capability negotiation, command encoding, response parsing — lives in the [iqos crate (V-VX/iqos)](https://github.com/V-VX/iqos). This repository is a thin CLI layer: it handles device discovery, user interaction, and argument parsing, then delegates every device operation to the crate's high-level API.
+All device protocol logic — BLE/USB framing, capability negotiation, command encoding, and response parsing — lives in the [iqos crate (V-VX/iqos)](https://github.com/V-VX/iqos). This repository is a thin CLI layer: it handles device discovery, user interaction, and argument parsing, then delegates every device operation to the crate's high-level API.
## Features
- **Automatic Device Discovery** — Scans and connects to IQOS devices via Bluetooth
+- **Direct USB Connection** — Select the attached IQOS with `--usb` without BLE scanning
- **Interactive Console** — REPL with command history (`iqos>` prompt)
- **One-Shot CLI Commands** — Run device commands directly, for example `iqos --model iluma battery`
- **Saved Device Labels** — Remember a connected device and target it later with `--model `
@@ -76,7 +77,8 @@ All device protocol logic — BLE framing, capability negotiation, command encod
## Prerequisites
-- **Bluetooth adapter** — A working Bluetooth adapter on your system
+- **BLE use** — A working Bluetooth adapter
+- **USB use** — Access to an attached IQOS USB device (Linux may require a udev rule or `sudo`)
- **Platform-specific dependencies**:
**Linux:**
@@ -172,6 +174,16 @@ iqos brightness high --model iluma
iqos vibration heating on --model iluma-i --timeout 5
```
+Use the experimental USB transport instead of scanning over BLE:
+
+```bash
+iqos --usb info
+iqos --usb battery
+iqos --usb diagnosis
+```
+
+These commands, plus read-only brightness, vibration, FlexPuff, FlexBattery, and AutoStart queries, have been validated on an IQOS ILUMA i and compared with BLE results from the same device. The shared library validates both ordinary SCP CRC-8 frames and the CRC-16 format used by diagnosis telemetry. Stateful USB commands are not yet hardware-validated.
+
`--model` accepts either a built-in model selector or a saved device label. Global options can be placed before the command, after the command, or between command arguments; the CLI normalizes them before execution.
If you pass only a target option and no subcommand, IQOS CLI connects to that target and then starts interactive mode:
@@ -195,6 +207,7 @@ This is useful after saving a device label, because it skips the manual "Connect
| `iqos --model ` | Connect to a built-in model selector or saved label, then open interactive mode |
| `iqos --model ` | Connect to the selected target and run one command |
| `iqos --model ` | Same as above; global options may be placed after the command |
+| `iqos --usb [command]` | Connect to the attached IQOS over USB, optionally run one command |
| `iqos --timeout ...` | Override the BLE scan timeout |
Built-in model selectors include `iluma`, `iluma-one`, `iluma-prime`, `iluma-i`, `iluma-i-one`, and `iluma-i-prime`. Saved labels are managed with the `device` command.
@@ -336,6 +349,8 @@ sudo usermod -a -G bluetooth $USER
# Log out and log back in
```
+For USB, run the command once with `sudo` to confirm that permissions are the issue, then add a udev rule for vendor/product `2759:0003` rather than routinely running the CLI as root.
+
### Command Not Available
- Check the compatibility table — some features are model-specific
@@ -348,7 +363,7 @@ sudo usermod -a -G bluetooth $USER
```
iqos_cli/
├── src/
-│ ├── main.rs # Entry point and BLE device discovery
+│ ├── main.rs # Entry point and BLE/USB connection setup
│ └── loader/ # CLI interface
│ ├── mod.rs # Console runner (run_console)
│ ├── parser.rs # IQOSConsole REPL and command dispatch
@@ -417,7 +432,7 @@ GNU General Public License v3.0 — see [LICENSE](LICENSE) for details.
## Acknowledgments
-Built with [btleplug](https://github.com/deviceplug/btleplug) for Bluetooth Low Energy support.
+Built with [btleplug](https://github.com/deviceplug/btleplug) for Bluetooth Low Energy and [nusb](https://github.com/kevinmehall/nusb) for USB support.
---
diff --git a/src/cli.rs b/src/cli.rs
index 96938c4..a189c81 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -7,7 +7,7 @@ pub const VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Debug, Parser)]
#[command(
name = "iqos",
- about = "Control IQOS devices over BLE",
+ about = "Control IQOS devices over BLE or USB",
disable_version_flag = true
)]
pub struct Cli {
@@ -23,6 +23,10 @@ pub struct Cli {
#[arg(long, value_name = "secs")]
pub timeout: Option,
+ /// Connect through USB HID instead of scanning over BLE.
+ #[arg(long, action = ArgAction::SetTrue)]
+ pub usb: bool,
+
#[command(subcommand)]
pub command: Option,
}
@@ -236,6 +240,11 @@ pub fn normalize_global_options(args: Vec) -> Vec {
continue;
}
+ if arg == "--usb" {
+ global_options.push(arg.clone());
+ continue;
+ }
+
remaining.push(arg.clone());
}
@@ -332,6 +341,17 @@ mod tests {
assert!(matches!(cli.command, Some(CliCommand::Battery)));
}
+ #[test]
+ fn normalizes_usb_after_command() {
+ let args = normalize_global_options(strings(["iqos", "info", "--usb"]));
+
+ assert_eq!(args, strings(["iqos", "--usb", "info"]));
+
+ let cli = Cli::try_parse_from(args).unwrap();
+ assert!(cli.usb);
+ assert!(matches!(cli.command, Some(CliCommand::Info)));
+ }
+
#[test]
fn normalizes_global_options_between_command_args() {
let args = normalize_global_options(strings([
diff --git a/src/loader/cmds/autostart.rs b/src/loader/cmds/autostart.rs
index 55af460..ec0593c 100644
--- a/src/loader/cmds/autostart.rs
+++ b/src/loader/cmds/autostart.rs
@@ -1,7 +1,7 @@
use std::sync::Arc;
use anyhow::Result;
-use iqos::{DeviceCapability, Iqos, IqosBle};
+use iqos::{DeviceCapability, Iqos, IqosTransport};
use tokio::sync::Mutex;
use crate::loader::parser::{invalid_arguments, IQOSConsole};
@@ -20,7 +20,7 @@ enum AutostartAction {
Status,
}
-async fn execute(iqos: Arc>>, args: Vec) -> Result<()> {
+async fn execute(iqos: Arc>>, args: Vec) -> Result<()> {
let action = parse_action(&args)?;
let iqos = iqos.lock().await;
let model = iqos.transport().model();
diff --git a/src/loader/cmds/battery.rs b/src/loader/cmds/battery.rs
index 4ead46d..741d88c 100644
--- a/src/loader/cmds/battery.rs
+++ b/src/loader/cmds/battery.rs
@@ -1,7 +1,7 @@
use std::sync::Arc;
use anyhow::Result;
-use iqos::{Iqos, IqosBle};
+use iqos::{Iqos, IqosTransport};
use tokio::sync::Mutex;
use crate::loader::parser::IQOSConsole;
@@ -13,9 +13,17 @@ pub fn register_command(console: &mut IQOSConsole) {
);
}
-async fn execute(iqos: Arc>>) -> Result<()> {
+async fn execute(iqos: Arc>>) -> Result<()> {
let iqos = iqos.lock().await;
- let level = iqos.transport().read_battery_level().await?;
- println!("Battery: {level}%");
+ match iqos.transport() {
+ IqosTransport::Ble(transport) => {
+ let level = transport.read_battery_level().await?;
+ println!("Battery: {level}%");
+ }
+ IqosTransport::Usb(_) => {
+ let voltage = iqos.read_battery_voltage().await?;
+ println!("Battery voltage: {voltage:.3}V");
+ }
+ }
Ok(())
}
diff --git a/src/loader/cmds/brightness.rs b/src/loader/cmds/brightness.rs
index f84d4d8..2ab600b 100644
--- a/src/loader/cmds/brightness.rs
+++ b/src/loader/cmds/brightness.rs
@@ -1,7 +1,7 @@
use std::sync::Arc;
use anyhow::Result;
-use iqos::{BrightnessLevel, DeviceCapability, Iqos, IqosBle};
+use iqos::{BrightnessLevel, DeviceCapability, Iqos, IqosTransport};
use tokio::sync::Mutex;
use crate::loader::parser::{invalid_arguments, IQOSConsole};
@@ -13,7 +13,7 @@ pub fn register_command(console: &mut IQOSConsole) {
);
}
-async fn execute(iqos: Arc>>, args: Vec) -> Result<()> {
+async fn execute(iqos: Arc>>, args: Vec) -> Result<()> {
let iqos = iqos.lock().await;
if !iqos
diff --git a/src/loader/cmds/command.rs b/src/loader/cmds/command.rs
index 68641da..2991c89 100644
--- a/src/loader/cmds/command.rs
+++ b/src/loader/cmds/command.rs
@@ -4,12 +4,12 @@ use std::pin::Pin;
use std::sync::Arc;
use anyhow::Result;
-use iqos::{Iqos, IqosBle};
+use iqos::{Iqos, IqosTransport};
use tokio::sync::Mutex;
pub type CommandFn = Box<
dyn Fn(
- Arc>>,
+ Arc>>,
Vec,
) -> Pin> + Send>>
+ Send
diff --git a/src/loader/cmds/diagnosis.rs b/src/loader/cmds/diagnosis.rs
index f5064cd..21caeb8 100644
--- a/src/loader/cmds/diagnosis.rs
+++ b/src/loader/cmds/diagnosis.rs
@@ -1,7 +1,7 @@
use std::sync::Arc;
use anyhow::Result;
-use iqos::{Iqos, IqosBle};
+use iqos::{Iqos, IqosTransport};
use tokio::sync::Mutex;
use crate::loader::parser::IQOSConsole;
@@ -13,7 +13,7 @@ pub fn register_command(console: &mut IQOSConsole) {
);
}
-async fn execute(iqos: Arc>>, _args: Vec) -> Result<()> {
+async fn execute(iqos: Arc>>, _args: Vec) -> Result<()> {
let iqos = iqos.lock().await;
let data = iqos.read_diagnosis().await?;
println!("Diagnosis:");
diff --git a/src/loader/cmds/findmyiqos.rs b/src/loader/cmds/findmyiqos.rs
index d37f74a..d315e4b 100644
--- a/src/loader/cmds/findmyiqos.rs
+++ b/src/loader/cmds/findmyiqos.rs
@@ -1,7 +1,7 @@
use std::sync::Arc;
use anyhow::Result;
-use iqos::{Iqos, IqosBle};
+use iqos::{Iqos, IqosTransport};
use rustyline::DefaultEditor;
use tokio::sync::Mutex;
@@ -14,7 +14,7 @@ pub fn register_command(console: &mut IQOSConsole) {
);
}
-async fn execute(iqos: Arc>>) -> Result<()> {
+async fn execute(iqos: Arc>>) -> Result<()> {
println!("Starting Find My IQOS...");
{
diff --git a/src/loader/cmds/flexbattery.rs b/src/loader/cmds/flexbattery.rs
index 0dea919..9657109 100644
--- a/src/loader/cmds/flexbattery.rs
+++ b/src/loader/cmds/flexbattery.rs
@@ -1,7 +1,7 @@
use std::sync::Arc;
use anyhow::Result;
-use iqos::{DeviceCapability, FlexBatteryMode, FlexBatterySettings, Iqos, IqosBle};
+use iqos::{DeviceCapability, FlexBatteryMode, FlexBatterySettings, Iqos, IqosTransport};
use tokio::sync::Mutex;
use crate::loader::parser::{invalid_arguments, IQOSConsole};
@@ -13,7 +13,7 @@ pub fn register_command(console: &mut IQOSConsole) {
);
}
-async fn execute(iqos: Arc>>, args: Vec) -> Result<()> {
+async fn execute(iqos: Arc>>, args: Vec) -> Result<()> {
let iqos = iqos.lock().await;
let model = iqos.transport().model();
diff --git a/src/loader/cmds/flexpuff.rs b/src/loader/cmds/flexpuff.rs
index b871ef1..6b388d4 100644
--- a/src/loader/cmds/flexpuff.rs
+++ b/src/loader/cmds/flexpuff.rs
@@ -1,7 +1,7 @@
use std::sync::Arc;
use anyhow::Result;
-use iqos::{DeviceCapability, FlexPuffSetting, Iqos, IqosBle};
+use iqos::{DeviceCapability, FlexPuffSetting, Iqos, IqosTransport};
use tokio::sync::Mutex;
use crate::loader::parser::{invalid_arguments, IQOSConsole};
@@ -20,7 +20,7 @@ enum FlexPuffAction {
Status,
}
-async fn execute(iqos: Arc>>, args: Vec) -> Result<()> {
+async fn execute(iqos: Arc>>, args: Vec) -> Result<()> {
let action = parse_action(&args)?;
let iqos = iqos.lock().await;
let model = iqos.transport().model();
diff --git a/src/loader/cmds/help.rs b/src/loader/cmds/help.rs
index 7c5d330..e00c889 100644
--- a/src/loader/cmds/help.rs
+++ b/src/loader/cmds/help.rs
@@ -1,7 +1,7 @@
use std::sync::Arc;
use anyhow::Result;
-use iqos::{DeviceCapability, Iqos, IqosBle};
+use iqos::{DeviceCapability, Iqos, IqosTransport};
use tokio::sync::Mutex;
use crate::loader::parser::IQOSConsole;
@@ -13,7 +13,7 @@ pub fn register_command(console: &mut IQOSConsole) {
);
}
-async fn execute(iqos: Arc>>) -> Result<()> {
+async fn execute(iqos: Arc>>) -> Result<()> {
let iqos = iqos.lock().await;
let model = iqos.transport().model();
println!("Available commands:");
diff --git a/src/loader/cmds/info.rs b/src/loader/cmds/info.rs
index 0ae5622..7dab54c 100644
--- a/src/loader/cmds/info.rs
+++ b/src/loader/cmds/info.rs
@@ -1,7 +1,7 @@
use std::sync::Arc;
use anyhow::Result;
-use iqos::{DeviceStatus, Iqos, IqosBle};
+use iqos::{DeviceStatus, Iqos, IqosTransport};
use tokio::sync::Mutex;
use crate::loader::parser::{invalid_arguments, IQOSConsole};
@@ -13,7 +13,7 @@ pub fn register_command(console: &mut IQOSConsole) {
);
}
-async fn execute(iqos: Arc>>, args: Vec) -> Result<()> {
+async fn execute(iqos: Arc>>, args: Vec) -> Result<()> {
if args.len() != 1 {
return Err(invalid_arguments("Usage: info"));
}
@@ -65,7 +65,7 @@ fn print_status(status: &DeviceStatus) {
}
fn field_or_missing(value: Option<&str>) -> &str {
- value.unwrap_or("N/A")
+ value.map_or("N/A", |field| field.trim_end_matches('\0'))
}
#[cfg(test)]
@@ -81,4 +81,9 @@ mod tests {
fn present_field_is_preserved() {
assert_eq!(field_or_missing(Some("value")), "value");
}
+
+ #[test]
+ fn trailing_gatt_nul_is_not_printed() {
+ assert_eq!(field_or_missing(Some("value\0")), "value");
+ }
}
diff --git a/src/loader/cmds/lock.rs b/src/loader/cmds/lock.rs
index 5c08e15..8665a6e 100644
--- a/src/loader/cmds/lock.rs
+++ b/src/loader/cmds/lock.rs
@@ -1,7 +1,7 @@
use std::sync::Arc;
use anyhow::Result;
-use iqos::{Iqos, IqosBle};
+use iqos::{Iqos, IqosTransport};
use tokio::sync::Mutex;
use crate::loader::parser::IQOSConsole;
@@ -13,7 +13,7 @@ pub fn register_command(console: &mut IQOSConsole) {
);
}
-async fn execute(iqos: Arc>>) -> Result<()> {
+async fn execute(iqos: Arc>>) -> Result<()> {
let iqos = iqos.lock().await;
iqos.lock(iqos.transport().model()).await?;
println!("Device locked");
diff --git a/src/loader/cmds/smartgesture.rs b/src/loader/cmds/smartgesture.rs
index f1ab66f..b66241e 100644
--- a/src/loader/cmds/smartgesture.rs
+++ b/src/loader/cmds/smartgesture.rs
@@ -1,7 +1,7 @@
use std::sync::Arc;
use anyhow::Result;
-use iqos::{DeviceCapability, Iqos, IqosBle};
+use iqos::{DeviceCapability, Iqos, IqosTransport};
use tokio::sync::Mutex;
use crate::loader::parser::{invalid_arguments, IQOSConsole};
@@ -19,7 +19,7 @@ enum SmartGestureAction {
Disable,
}
-async fn execute(iqos: Arc>>, args: Vec) -> Result<()> {
+async fn execute(iqos: Arc>>, args: Vec) -> Result<()> {
let action = parse_action(&args)?;
let iqos = iqos.lock().await;
let model = iqos.transport().model();
diff --git a/src/loader/cmds/unlock.rs b/src/loader/cmds/unlock.rs
index 3c61fb9..34ad974 100644
--- a/src/loader/cmds/unlock.rs
+++ b/src/loader/cmds/unlock.rs
@@ -1,7 +1,7 @@
use std::sync::Arc;
use anyhow::Result;
-use iqos::{Iqos, IqosBle};
+use iqos::{Iqos, IqosTransport};
use tokio::sync::Mutex;
use crate::loader::parser::IQOSConsole;
@@ -13,7 +13,7 @@ pub fn register_command(console: &mut IQOSConsole) {
);
}
-async fn execute(iqos: Arc>>) -> Result<()> {
+async fn execute(iqos: Arc>>) -> Result<()> {
let iqos = iqos.lock().await;
iqos.unlock(iqos.transport().model()).await?;
println!("Device unlocked");
diff --git a/src/loader/cmds/version.rs b/src/loader/cmds/version.rs
index 6354005..10a2daf 100644
--- a/src/loader/cmds/version.rs
+++ b/src/loader/cmds/version.rs
@@ -1,7 +1,7 @@
use std::sync::Arc;
use anyhow::Result;
-use iqos::{Iqos, IqosBle};
+use iqos::{Iqos, IqosTransport};
use tokio::sync::Mutex;
use crate::cli::print_version;
@@ -14,7 +14,7 @@ pub fn register_command(console: &mut IQOSConsole) {
);
}
-async fn execute(_iqos: Arc>>, args: Vec) -> Result<()> {
+async fn execute(_iqos: Arc>>, args: Vec) -> Result<()> {
if args.len() != 1 {
return Err(invalid_arguments("Usage: version"));
}
diff --git a/src/loader/cmds/vibration.rs b/src/loader/cmds/vibration.rs
index 9c594bd..2162279 100644
--- a/src/loader/cmds/vibration.rs
+++ b/src/loader/cmds/vibration.rs
@@ -1,7 +1,7 @@
use std::sync::Arc;
use anyhow::Result;
-use iqos::{Iqos, IqosBle, VibrationSettings};
+use iqos::{Iqos, IqosTransport, VibrationSettings};
use tokio::sync::Mutex;
use crate::loader::parser::{invalid_arguments, IQOSConsole};
@@ -13,7 +13,7 @@ pub fn register_command(console: &mut IQOSConsole) {
);
}
-async fn execute(iqos: Arc>>, args: Vec) -> Result<()> {
+async fn execute(iqos: Arc>>, args: Vec) -> Result<()> {
let iqos = iqos.lock().await;
let model = iqos.transport().model();
let str_args: Vec<&str> = args.iter().map(String::as_str).collect();
diff --git a/src/loader/parser.rs b/src/loader/parser.rs
index 7a77fcf..d1239c3 100644
--- a/src/loader/parser.rs
+++ b/src/loader/parser.rs
@@ -4,7 +4,7 @@ use std::path::PathBuf;
use std::sync::Arc;
use anyhow::Result;
-use iqos::{Iqos, IqosBle};
+use iqos::{Iqos, IqosTransport};
use rustyline::error::ReadlineError;
use rustyline::{Config, Editor};
use tokio::sync::Mutex;
@@ -63,17 +63,17 @@ pub fn is_invalid_argument_message(message: &str) -> bool {
pub struct IQOSConsole {
commands: CommandRegistry,
- iqos: Arc>>,
+ iqos: Arc>>,
connected_device: Option,
}
impl IQOSConsole {
- pub fn new(iqos: Iqos) -> Self {
+ pub fn new(iqos: Iqos) -> Self {
Self::with_connected_device(iqos, None)
}
pub fn with_connected_device(
- iqos: Iqos,
+ iqos: Iqos,
connected_device: Option,
) -> Self {
Self {
@@ -151,20 +151,23 @@ impl IQOSConsole {
}
#[allow(dead_code)]
-pub async fn run_console(iqos: Iqos) -> Result<()> {
+pub async fn run_console(iqos: Iqos) -> Result<()> {
let mut console = IQOSConsole::new(iqos);
register_all_commands(&mut console);
console.run().await
}
-pub async fn run_console_with_device(iqos: Iqos, device: ConnectedDevice) -> Result<()> {
+pub async fn run_console_with_device(
+ iqos: Iqos,
+ device: ConnectedDevice,
+) -> Result<()> {
let mut console = IQOSConsole::with_connected_device(iqos, Some(device));
register_all_commands(&mut console);
console.run().await
}
pub async fn run_registered_command(
- iqos: Iqos,
+ iqos: Iqos,
command: &str,
args: Vec,
) -> Result<()> {
diff --git a/src/main.rs b/src/main.rs
index 936f9fd..df4f69b 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -10,7 +10,7 @@ use btleplug::platform::{Adapter, Manager, Peripheral, PeripheralId};
use clap::Parser;
use colored::Colorize;
use futures::stream::StreamExt;
-use iqos::{DeviceModel, Iqos, IqosBle};
+use iqos::{DeviceModel, Iqos, IqosBle, IqosTransport, IqosUsb};
mod cli;
mod config;
@@ -47,6 +47,7 @@ impl ExitError {
#[derive(Debug, Clone)]
enum ScanTarget {
+ Usb,
Model(DeviceModel),
Address {
label: Option,
@@ -154,7 +155,8 @@ async fn run_cli(mut args: Vec) -> i32 {
}
let Some(command) = cli.command else {
- return match run_auto_connected_console(cli.model, scan_timeout(cli.timeout)).await {
+ return match run_auto_connected_console(cli.model, scan_timeout(cli.timeout), cli.usb).await
+ {
Ok(()) => 0,
Err(error) => {
eprintln!("Error: {:#}", error.error);
@@ -166,6 +168,7 @@ async fn run_cli(mut args: Vec) -> i32 {
match run_one_shot(
cli.model,
scan_timeout(cli.timeout),
+ cli.usb,
command.into_one_shot(),
)
.await
@@ -181,6 +184,7 @@ async fn run_cli(mut args: Vec) -> i32 {
async fn run_auto_connected_console(
model_arg: Option,
timeout: Duration,
+ use_usb: bool,
) -> std::result::Result<(), ExitError> {
print_ascii_art();
@@ -188,8 +192,8 @@ async fn run_auto_connected_console(
mut config,
target,
should_save_memory,
- } = load_config_and_resolve_target(model_arg.as_deref(), true)?;
- let (iqos, device) = connect_target(&target, timeout).await?;
+ } = load_config_and_resolve_target(model_arg.as_deref(), true, use_usb)?;
+ let (iqos, device) = connect_target(&target, timeout, use_usb).await?;
apply_connection_memory(&mut config, &target, &device);
save_connection_memory(&config, &target, should_save_memory, true)?;
@@ -202,6 +206,7 @@ async fn run_auto_connected_console(
async fn run_one_shot(
model_arg: Option,
timeout: Duration,
+ use_usb: bool,
command: OneShotCommand,
) -> std::result::Result<(), ExitError> {
match command {
@@ -233,12 +238,18 @@ async fn run_one_shot(
}
}
OneShotCommand::DeviceSave { label } => {
+ if use_usb {
+ return Err(ExitError::new(
+ EXIT_INVALID_ARGUMENTS,
+ anyhow!("Saving USB devices is not supported yet"),
+ ));
+ }
let label = validate_device_label(&label)
.map_err(|error| ExitError::new(EXIT_INVALID_ARGUMENTS, error))?;
let ResolvedTarget {
mut config, target, ..
- } = load_config_and_resolve_target(model_arg.as_deref(), false)?;
- let (iqos, device) = connect_target(&target, timeout).await?;
+ } = load_config_and_resolve_target(model_arg.as_deref(), false, false)?;
+ let (iqos, device) = connect_target(&target, timeout, false).await?;
apply_connection_memory(&mut config, &target, &device);
config
.save_device(label.clone(), &device)
@@ -255,8 +266,8 @@ async fn run_one_shot(
config: mut command_config,
target,
should_save_memory,
- } = load_config_and_resolve_target(model_arg.as_deref(), true)?;
- let (iqos, device) = connect_target(&target, timeout).await?;
+ } = load_config_and_resolve_target(model_arg.as_deref(), true, use_usb)?;
+ let (iqos, device) = connect_target(&target, timeout, use_usb).await?;
apply_connection_memory(&mut command_config, &target, &device);
save_connection_memory(&command_config, &target, should_save_memory, true)?;
@@ -270,7 +281,26 @@ async fn run_one_shot(
fn load_config_and_resolve_target(
model_arg: Option<&str>,
allow_model_config_failure: bool,
+ use_usb: bool,
) -> std::result::Result {
+ if use_usb {
+ let (config, _) = load_memory_config(true)?;
+ let target = match model_arg {
+ Some(value) => ScanTarget::Model(parse_device_model(value).ok_or_else(|| {
+ ExitError::new(
+ EXIT_INVALID_ARGUMENTS,
+ anyhow!("USB mode accepts a device model, not a saved BLE label: {value}"),
+ )
+ })?),
+ None => ScanTarget::Usb,
+ };
+ return Ok(ResolvedTarget {
+ config,
+ target,
+ should_save_memory: false,
+ });
+ }
+
if let Some(value) = model_arg {
if let Some(model) = parse_device_model(value) {
let (config, should_save_memory) = load_memory_config(allow_model_config_failure)?;
@@ -368,7 +398,12 @@ fn save_connection_memory(
async fn connect_target(
target: &ScanTarget,
timeout: Duration,
-) -> std::result::Result<(IqosBle, ConnectedDevice), ExitError> {
+ use_usb: bool,
+) -> std::result::Result<(IqosTransport, ConnectedDevice), ExitError> {
+ if use_usb {
+ return connect_usb(target).await;
+ }
+
let manager = Manager::new()
.await
.map_err(|error| ExitError::new(EXIT_CONNECTION_FAILED, error))?;
@@ -381,10 +416,38 @@ async fn connect_target(
let ble = IqosBle::connect_and_discover(peripheral)
.await
.map_err(|error| ExitError::new(EXIT_CONNECTION_FAILED, error))?;
- let device = connected_device(&ble, discovered);
+ let device = connected_ble_device(&ble, discovered);
warn_serial_mismatch(target, &device);
- Ok((ble, device))
+ Ok((ble.into(), device))
+}
+
+async fn connect_usb(
+ target: &ScanTarget,
+) -> std::result::Result<(IqosTransport, ConnectedDevice), ExitError> {
+ let usb = IqosUsb::connect()
+ .await
+ .map_err(|error| ExitError::new(EXIT_CONNECTION_FAILED, error))?;
+ let device = ConnectedDevice {
+ address: usb.location().to_string(),
+ local_name: usb.device_info().model_number.clone(),
+ model: usb.model(),
+ serial_number: usb.device_info().serial_number.clone(),
+ };
+
+ if let ScanTarget::Model(expected) = target {
+ if *expected != device.model {
+ return Err(ExitError::new(
+ EXIT_CONNECTION_FAILED,
+ anyhow!(
+ "Connected USB device is {:?}, requested {expected:?}",
+ device.model
+ ),
+ ));
+ }
+ }
+
+ Ok((usb.into(), device))
}
async fn find_matching_peripheral(
@@ -483,9 +546,9 @@ async fn run_interactive() -> Result<()> {
if prompt_for_connection(&name, &discovered.address).await? {
println!("Connecting...");
let ble = IqosBle::connect_and_discover(peripheral).await?;
- let device = connected_device(&ble, discovered);
+ let device = connected_ble_device(&ble, discovered);
remember_connected_device(&device);
- let iqos = Iqos::new(ble);
+ let iqos = Iqos::new(IqosTransport::Ble(ble));
central.stop_scan().await?;
run_console_with_device(iqos, device).await?;
return Ok(());
@@ -537,7 +600,7 @@ fn discovered_device(
}
}
-fn connected_device(ble: &IqosBle, discovered: DiscoveredDevice) -> ConnectedDevice {
+fn connected_ble_device(ble: &IqosBle, discovered: DiscoveredDevice) -> ConnectedDevice {
ConnectedDevice {
address: discovered.address,
local_name: discovered.local_name,
@@ -548,6 +611,7 @@ fn connected_device(ble: &IqosBle, discovered: DiscoveredDevice) -> ConnectedDev
fn target_matches(target: &ScanTarget, discovered: &DiscoveredDevice) -> bool {
match target {
+ ScanTarget::Usb => false,
ScanTarget::Model(model) => {
discovered
.local_name
@@ -609,6 +673,7 @@ fn classify_command_error(error: &anyhow::Error) -> i32 {
fn describe_target(target: &ScanTarget) -> String {
match target {
+ ScanTarget::Usb => "USB IQOS".to_string(),
ScanTarget::Model(model) => format!("{model:?}"),
ScanTarget::Address {
label: Some(label),