Background
The current UX is fully interactive: the user launches `iqos_cli`, follows the stdout device-selection prompt, confirms the connection, and then operates a REPL (`iqos>` prompt) to issue commands one at a time.
This works well for exploratory sessions, but makes automation, scripting, and quick one-off operations cumbersome.
Proposed Feature
Add a non-interactive (one-shot) CLI mode that lets users invoke a single command directly from their shell, bypassing both the device-selection prompt and the REPL entirely.
Proposed invocation syntax
iqos [--model <target>] <command> [command-args…]
| Example |
Effect |
| `iqos battery` |
Connect to last stored device, print battery level, exit |
| `iqos findmyiqos` |
Connect to last stored device, run Find My IQOS, exit |
| `iqos vibration heating on` |
Enable heating-start vibration and exit |
| `iqos --model iluma-i battery` |
Scan and connect to first discovered ILUMA i device |
| `iqos --model blackcat battery` |
Connect to user-labelled device "blackcat" and run command |
Key Requirements
1. Unified `--model` target resolution
The single `--model ` flag resolves a target device via two strategies:
Strategy A — DeviceModel variant
Values: `iluma`, `iluma-prime`, `iluma-one`, `iluma-i`, `iluma-i-prime`, `iluma-i-one` (case-insensitive)
During BLE scan the peripheral's BLE local name (e.g. `"IQOS ILUMA i"`) is available before connection. `DeviceModel::from_local_name()` maps this to a `DeviceModel` variant. If the variant matches the requested model, connect automatically.
iqos --model iluma-i findmyiqos
→ scan → find "IQOS ILUMA i" → DeviceModel::IlumaI → match → auto-connect
Strategy B — User label
Any value that does not match a known DeviceModel variant is treated as a user-defined label looked up in `config.toml`. Each labelled entry stores the peripheral's Bluetooth address, which is available at scan time. The tool scans until the stored address is discovered, then auto-connects — no model-name ambiguity involved.
iqos --model blackcat battery
→ look up "blackcat" in config → BT address AA:BB:CC:DD:EE:FF
→ scan → match address → auto-connect
If `--model` is omitted, the `[default]` entry in config is used (address of the most recently connected device).
2. Device memory — `config.toml`
Persisted at `/.config/iqos_cli/config.toml` (XDG) or `/.iqos_cli/config.toml` (fallback).
# Written automatically after every successful connection.
[default]
address = "AA:BB:CC:DD:EE:FF"
# Written when user runs: iqos device save blackcat
[devices.blackcat]
address = "AA:BB:CC:DD:EE:FF" # BT address — primary key, used at scan time
local_name = "IQOS ILUMA i" # BLE advertised name, stored for display
model = "IlumaI" # DeviceModel variant, cached for display
serial_number = "SN123456789" # Cached after first connection (post-connect read)
Why BT address as primary key, not serial number:
Serial number is part of the GATT Device Information Service and is only readable after `connect_and_discover()` completes service discovery. BLE advertisement data (local name + peripheral ID / BT address) is available during scan before connection. Therefore the BT address is the correct scan-time identifier.
The serial number is read on first connection and cached in config. On subsequent connections it serves as a post-connect consistency check (warn if the serial at the stored address has changed — e.g. device was replaced).
3. Device labelling CLI
iqos device save <label> # label the most recently connected device
iqos device list # list all saved devices with model and serial
iqos device remove <label> # remove a saved entry
4. Auto-connect flow (non-interactive)
resolve target
├─ --model <DeviceModel variant> → scan, match by local_name → connect
├─ --model <label> → look up BT address in config → scan, match by address → connect
└─ (no flag) → use config [default].address → scan, match → connect
on connect:
→ read DeviceInfo (serial_number, model_number, sw_revision)
→ if serial cached in config: verify matches → warn on mismatch
→ update config [default] + config [label].serial_number if new
→ execute command → print result → exit
Scan timeout: 10 s by default; configurable via `--timeout ` or `IQOS_SCAN_TIMEOUT` env var.
5. Interactive REPL remains unchanged
Running `iqos` with no subcommand preserves the existing interactive flow exactly. All changes are additive.
6. Exit codes
| Situation |
Code |
| Success |
`0` |
| BLE connection failed / device not found |
`1` |
| Unknown command or invalid arguments |
`2` |
| Device command returned an error |
`3` |
| Label not found in config |
`4` |
Implementation Notes
- Argument detection: if `std::env::args()` has a non-flag positional argument after the binary name, enter one-shot mode; otherwise enter interactive mode.
- CLI parsing: `clap` is recommended for subcommand + flag parsing. Existing per-command argument parsing in `loader/cmds/` can be reused.
- Config format: `toml` (already in Cargo ecosystem via `toml` crate); `serde` for (de)serialization.
- BT address stability: Bluetooth address randomization is possible on some platforms/adapters. If this becomes a problem, the local name + model pair can serve as a secondary fallback for matching.
Acceptance Criteria
Out of Scope
- Multiple simultaneous device sessions
- Cloud/remote config sync
- GUI / TUI
Background
The current UX is fully interactive: the user launches `iqos_cli`, follows the stdout device-selection prompt, confirms the connection, and then operates a REPL (`iqos>` prompt) to issue commands one at a time.
This works well for exploratory sessions, but makes automation, scripting, and quick one-off operations cumbersome.
Proposed Feature
Add a non-interactive (one-shot) CLI mode that lets users invoke a single command directly from their shell, bypassing both the device-selection prompt and the REPL entirely.
Proposed invocation syntax
Key Requirements
1. Unified `--model` target resolution
The single `--model ` flag resolves a target device via two strategies:
Strategy A — DeviceModel variant
Values: `iluma`, `iluma-prime`, `iluma-one`, `iluma-i`, `iluma-i-prime`, `iluma-i-one` (case-insensitive)
During BLE scan the peripheral's BLE local name (e.g. `"IQOS ILUMA i"`) is available before connection. `DeviceModel::from_local_name()` maps this to a `DeviceModel` variant. If the variant matches the requested model, connect automatically.
Strategy B — User label
Any value that does not match a known DeviceModel variant is treated as a user-defined label looked up in `config.toml`. Each labelled entry stores the peripheral's Bluetooth address, which is available at scan time. The tool scans until the stored address is discovered, then auto-connects — no model-name ambiguity involved.
If `--model` is omitted, the `[default]` entry in config is used (address of the most recently connected device).
2. Device memory — `config.toml`
Persisted at `
/.config/iqos_cli/config.toml` (XDG) or `/.iqos_cli/config.toml` (fallback).Why BT address as primary key, not serial number:
Serial number is part of the GATT Device Information Service and is only readable after `connect_and_discover()` completes service discovery. BLE advertisement data (local name + peripheral ID / BT address) is available during scan before connection. Therefore the BT address is the correct scan-time identifier.
The serial number is read on first connection and cached in config. On subsequent connections it serves as a post-connect consistency check (warn if the serial at the stored address has changed — e.g. device was replaced).
3. Device labelling CLI
4. Auto-connect flow (non-interactive)
Scan timeout: 10 s by default; configurable via `--timeout ` or `IQOS_SCAN_TIMEOUT` env var.
5. Interactive REPL remains unchanged
Running `iqos` with no subcommand preserves the existing interactive flow exactly. All changes are additive.
6. Exit codes
Implementation Notes
Acceptance Criteria
Out of Scope