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
5 changes: 5 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@

## Engineering workflow

- Start every planned phase with a detailed execution plan in `docs/phases/`.
- Divide implementation into small, independently reviewable merge requests;
each MR states scope, verification, and any hardware action needed.
- Never edit, commit, or push `main` directly. Work on a dedicated branch and
merge through a reviewed MR. Do not push any branch unless explicitly asked.
- Run mock/unit tests before hardware tests.
- Hardware validation runs on target Linux hardware; QEMU cannot validate USB
control behavior.
Expand Down
3 changes: 3 additions & 0 deletions crates/focusrited/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,8 @@ edition.workspace = true
rust-version.workspace = true
license.workspace = true

[features]
hardware-write-tests = []

[dependencies]
alsa = "0.12.0"
48 changes: 37 additions & 11 deletions crates/focusrited/tests/scarlett2_alsa.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
#[cfg(feature = "hardware-write-tests")]
use std::collections::BTreeSet;
use std::sync::Mutex;

#[cfg(feature = "hardware-write-tests")]
use focusrited::Device;
use focusrited::{
ControlId, Device, DeviceError, ServiceError, Value,
ControlId, DeviceError, ServiceError, Value,
scarlett2_alsa::{Scarlett2Alsa, ValueType, discover},
worker::{DeviceWorker, WorkerError},
};

static HARDWARE_LOCK: Mutex<()> = Mutex::new(());

fn hardware_card() -> String {
std::env::var("FOCUSRITED_HARDWARE_CARD")
.expect("FOCUSRITED_HARDWARE_CARD must identify the attached Solo")
}

#[test]
fn fixture_records_solo_controls() {
let fixture = include_str!("fixtures/scarlett-solo-4th-gen.md");
Expand All @@ -17,14 +28,18 @@ fn fixture_records_solo_controls() {
#[test]
#[ignore = "requires an attached Scarlett Solo ALSA card"]
fn discovers_attached_solo() {
let discovery = discover("0").unwrap();
let _hardware_guard = HARDWARE_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let card = hardware_card();
let discovery = discover(&card).unwrap();

assert!(discovery.controls.len() >= 56);
assert!(discovery.controls.iter().any(|control| {
control.name == "Level Meter" && control.value_type == ValueType::Integer
}));

let worker = DeviceWorker::start(Scarlett2Alsa::new("0")).unwrap();
let worker = DeviceWorker::start(Scarlett2Alsa::new(card)).unwrap();
let snapshot = worker.state().unwrap().snapshot;
assert_eq!(snapshot.capabilities.len(), discovery.controls.len());
assert!(
Expand All @@ -50,8 +65,11 @@ fn discovers_attached_solo() {
#[test]
#[ignore = "toggle the Solo Direct Monitor control during this 30-second read-only check"]
fn reconciles_external_direct_monitor_change() {
let _hardware_guard = HARDWARE_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let control = ControlId("alsa-numid:48".into());
let worker = DeviceWorker::start(Scarlett2Alsa::new("0")).unwrap();
let worker = DeviceWorker::start(Scarlett2Alsa::new(hardware_card())).unwrap();
let before = worker.state().unwrap();
let previous = before.snapshot.values[&control].clone();
println!("Direct Monitor before: {previous:?}. Toggle Direct now.");
Expand Down Expand Up @@ -80,12 +98,15 @@ fn reconciles_external_direct_monitor_change() {
}

#[test]
#[ignore = "detach then re-attach the Solo with usbipd during this 60-second read-only check"]
fn reconnects_after_usbip_detach() {
let worker = DeviceWorker::start(Scarlett2Alsa::new("0")).unwrap();
#[ignore = "disconnect then reconnect the Solo during this 60-second read-only check"]
fn reconnects_after_solo_disconnect() {
let _hardware_guard = HARDWARE_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let worker = DeviceWorker::start(Scarlett2Alsa::new(hardware_card())).unwrap();
let initial = worker.state().unwrap();
let mut saw_offline = false;
println!("Detach Solo with usbipd now, then attach it again.");
println!("Disconnect Solo now, then reconnect it.");

for _ in 0..240 {
std::thread::sleep(std::time::Duration::from_millis(250));
Expand Down Expand Up @@ -113,13 +134,18 @@ fn reconnects_after_usbip_detach() {
}

worker.stop().unwrap();
panic!("did not observe both USB/IP detach and reconnect within 60 seconds");
panic!("did not observe both Solo disconnect and reconnect within 60 seconds");
}

#[cfg(feature = "hardware-write-tests")]
#[test]
#[ignore = "requires explicit approval; toggles Direct Monitor once and restores it"]
fn toggles_direct_monitor_and_restores_it() {
let control = discover("0")
let _hardware_guard = HARDWARE_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let card = hardware_card();
let control = discover(&card)
.unwrap()
.controls
.into_iter()
Expand All @@ -132,7 +158,7 @@ fn toggles_direct_monitor_and_restores_it() {
focusrited::scarlett2_alsa::ObservedValue::Boolean(value) => value,
_ => unreachable!("Direct Monitor must be boolean"),
};
let mut device = Scarlett2Alsa::with_writable_controls("0", BTreeSet::from([id.clone()]));
let mut device = Scarlett2Alsa::with_writable_controls(card, BTreeSet::from([id.clone()]));
let target = !before;

let changed = device.write(&id, Value::Bool(target));
Expand Down
167 changes: 167 additions & 0 deletions docs/phases/phase-3-pi-compatibility.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# Phase 3: Pi Compatibility Verification

## Status

In progress. MR 1 complete; MR 2 is next. Target is prepared Pi OS ARM64 with
Scarlett Solo 4th Gen connected directly by USB. Phase 2 WSL evidence remains
valid only for its WSL scope.

## Goal

Prove current Solo service works on target Pi Linux without changing device
state by default. Record target prerequisites and fix only Pi-specific defects
found by that evidence.

## Guardrails

- Run mock/unit checks before every hardware check.
- Begin every hardware session with bounded, read-only discovery.
- Hardware writes, routing/clock changes, resets, firmware work, profile apply,
and physical unplug/replug require explicit approval for that session.
- Redact serials, usernames, LAN addresses, tokens, and unrelated logs before
adding fixtures or documentation.
- Use a named ALSA card or stable card selector; never assume Solo is card 0.
- Each MR uses its own branch, is independently reviewable, and never directly
edits, commits, or pushes `main`.

## Merge-request plan

### MR 1: Safe native hardware-test entry points — complete

**Scope**

- Separate existing Solo tests into read-only discovery/reconciliation/reconnect
coverage and write-capable coverage.
- Gate write-capable coverage behind an explicit Cargo feature.
- Make the selected hardware card explicit for hardware tests and commands.
- Document exact read-only and write-capable invocation commands.

**Verification**

- `cargo fmt --check`
- `cargo clippy --workspace --all-targets -- -D warnings`
- `cargo test --workspace`
- Confirm an ignored-test run without the write feature cannot compile or run a
device-writing test.

**Hardware action**

None. Do not run ignored tests in this MR.

**Commands after MR 1**

Select Solo explicitly with its ALSA card ID rather than relying on card order:

```text
FOCUSRITED_HARDWARE_CARD=Gen cargo test -p focusrited --test scarlett2_alsa -- \
--ignored --test-threads=1
```

That command runs only ignored read-only hardware tests. A write-capable test
is excluded unless `--features hardware-write-tests` is passed. Even then, run
only after explicit session approval and filter to the intended test:

```text
FOCUSRITED_HARDWARE_CARD=Gen cargo test -p focusrited --test scarlett2_alsa \
--features hardware-write-tests toggles_direct_monitor_and_restores_it -- \
--ignored --test-threads=1
```

### MR 2: Pi read-only discovery and capability evidence

**Scope**

- Run bounded Pi read-only probes: kernel, USB identity, ALSA card/device list,
controls, and bounded control contents.
- Run Solo discovery against selected Pi ALSA card.
- Compare discovery shape with Phase 2 fixture; update sanitized fixture or
adapter parsing only when Pi evidence proves a difference.
- Record Pi OS/kernel, required packages, device access/group requirements, and
stable card-selection guidance.

**Verification**

- MR 1 checks.
- Read-only hardware discovery test.
- Read-only daemon startup using `focusrited --card CARD`, then clean stop.

**Hardware action**

Read-only only. No control write or stored-profile apply.

### MR 3: Native reconciliation and controlled write metadata

**Scope**

- Verify external Direct Monitor/front-panel change reaches service state and
advances revision without daemon write.
- Capture integer ranges and enum items exposed by Pi ALSA.
- Make only evidence-backed metadata/validation changes; unsupported domains
remain explicit and non-writable.

**Verification**

- MR 1 checks.
- Read-only external-change test with operator interaction.
- Add or update mock tests for each parsing/validation defect found.

**Hardware action**

Read-only daemon behavior. Operator may change front-panel Direct Monitor;
daemon must not write any control.

### MR 4: Native lifecycle recovery

**Scope**

- Verify service behavior through physical Solo unplug/replug and Pi reboot.
- Diagnose USB power, ALSA-node, service-startup, and recovery ordering issues.
- Add the smallest code or deployment change proven necessary.
- Record sanitized lifecycle evidence and known limits.

**Verification**

- MR 1 checks.
- Read-only physical disconnect/reconnect test.
- Read-only daemon start after reboot and recovery after reattach.

**Hardware action**

Physical cable unplug/replug and Pi reboot require session approval. No ALSA
write, routing, clock, reset, firmware, or profile apply.

### MR 5: Phase 3 closeout

**Scope**

- Consolidate Pi prerequisites and verified commands in deployment/hardware
documentation.
- Mark Phase 3 evidence and unresolved limits.
- Add only regression tests for defects actually found in MRs 1–4.

**Verification**

- Full Rust verification: format, Clippy with warnings denied, and workspace
tests.
- Repeat one bounded read-only discovery and daemon-start check on Pi.

**Hardware action**

Read-only only.

## Exit checks

- [x] Read-only and write-capable hardware tests are structurally separated;
write tests require explicit feature plus session approval.
- [ ] Solo service builds and starts natively on Pi against selected ALSA card.
- [ ] Sanitized Pi discovery proves capabilities and documented prerequisites.
- [ ] External/front-panel change reconciles into authoritative service state.
- [ ] Physical unplug/replug recovers to a fresh online snapshot.
- [ ] Pi reboot returns daemon and Solo readiness without device-state mutation.
- [ ] Target-specific limits, commands, and unresolved issues are documented.

## Update rule

After every MR, record completed checks, operator approvals, sanitized evidence,
defects found, and any changed prerequisite. Do not expand scope into touchscreen,
LAN, packaging, or 16i16/FCP work; those belong to later phases.
2 changes: 2 additions & 0 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ disconnect/reconnect, and persistence; unsupported controls are explicit.

## Phase 3: Pi compatibility verification — planned

Execution plan: [Phase 3 Pi compatibility plan](phases/phase-3-pi-compatibility.md).

Develop directly on the Pi over SSH, including Zed Remote Development from a
laptop, then validate current Solo service natively on Pi Linux. Find and fix
target-only build, ALSA, USB, system-service, reboot, and unplug/replug issues.
Expand Down
Loading