Skip to content

feat: non-interactive (one-shot) CLI mode with auto-connect and device memory #5

@nonnil

Description

@nonnil

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

  • `iqos [args]` connects to the default stored device and executes the command.
  • `iqos --model ` scans and connects to the first matching device by BLE local name.
  • `iqos --model ` connects to the device stored under that label by BT address.
  • BT address is persisted as `[default]` after every successful connection.
  • Serial number is read post-connect and cached in config; a mismatch triggers a warning (non-fatal).
  • `iqos device save/list/remove` manages labelled device entries.
  • Interactive mode (`iqos` with no subcommand) is unaffected.
  • Exit codes match the table above.
  • `iqos --help` and `iqos --help` produce correct usage text.

Out of Scope

  • Multiple simultaneous device sessions
  • Cloud/remote config sync
  • GUI / TUI

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions