main.rs (11794B)
1 //! Phase 0 feasibility spike for scarlett-tui. 2 //! 3 //! Goal: prove that on macOS we can speak the Focusrite "scarlett2" control 4 //! protocol to a Scarlett 18i20 3rd Gen over endpoint 0, WITHOUT taking the 5 //! audio streaming interfaces away from Core Audio — i.e. replicate what 6 //! Focusrite Control does, from userspace, in Rust. 7 //! 8 //! It is deliberately chatty and defensive: every step prints what happened so 9 //! we can read the real macOS behaviour rather than guess it. 10 //! 11 //! Run with Focusrite Control QUIT (only one process may own the device): 12 //! cargo run -p spike 13 //! 14 //! Protocol facts (from the GPL Linux driver sound/usb/mixer_scarlett2.c): 15 //! * control transfers on EP0, recipient = interface 16 //! TX: bmRequestType 0x21 (Class|Interface|OUT), bRequest 2 (CMD_REQ) 17 //! RX: bmRequestType 0xA1 (Class|Interface|IN), bRequest 3 (CMD_RESP) 18 //! * 16-byte little-endian header: cmd u32, size u16, seq u16, error u32, pad u32 19 //! * seq increments by 1 per request; response echoes cmd/seq, error must be 0 20 21 use std::time::Duration; 22 23 use anyhow::{anyhow, bail, Context, Result}; 24 use rusb::{Direction, Recipient, RequestType, TransferType, UsbContext}; 25 26 const VID: u16 = 0x1235; // Focusrite / Novation 27 const PID: u16 = 0x8215; // Scarlett 18i20 3rd Gen 28 29 // scarlett2 opcodes 30 const CMD_INIT_1: u32 = 0x0000_0000; 31 const CMD_INIT_2: u32 = 0x0000_0002; 32 const CMD_GET_SYNC: u32 = 0x0000_6004; 33 const CMD_GET_DATA: u32 = 0x0080_0000; 34 35 const REQ: u8 = 2; // CMD_REQ (host -> device) 36 const RESP: u8 = 3; // CMD_RESP (device -> host) 37 const CMD_INIT: u8 = 0; // bRequest for the raw "step 0" priming read 38 const HDR: usize = 16; 39 const TIMEOUT: Duration = Duration::from_millis(1000); 40 41 fn main() { 42 if let Err(e) = run() { 43 eprintln!("\n\x1b[31mSPIKE FAILED:\x1b[0m {e:#}"); 44 eprintln!("\nIf this is an access/ownership error, make sure Focusrite Control"); 45 eprintln!("is fully quit (it holds the device exclusively)."); 46 std::process::exit(1); 47 } 48 } 49 50 fn run() -> Result<()> { 51 let ctx = rusb::Context::new().context("create libusb context")?; 52 53 let device = ctx 54 .devices()? 55 .iter() 56 .find(|d| { 57 d.device_descriptor() 58 .map(|x| x.vendor_id() == VID && x.product_id() == PID) 59 .unwrap_or(false) 60 }) 61 .ok_or_else(|| anyhow!("Scarlett 18i20 3rd Gen ({VID:04x}:{PID:04x}) not found"))?; 62 63 let desc = device.device_descriptor()?; 64 println!("== Device =="); 65 println!( 66 " {:04x}:{:04x} bDeviceClass={:#04x} subclass={:#04x} configs={}", 67 desc.vendor_id(), 68 desc.product_id(), 69 desc.class_code(), 70 desc.sub_class_code(), 71 desc.num_configurations() 72 ); 73 74 // --- 1. Enumerate interfaces + endpoints; find the control interface and a 75 // candidate interrupt-in (notification) endpoint. ---------------- 76 let config = device 77 .active_config_descriptor() 78 .context("read active config descriptor")?; 79 80 let mut control_iface: Option<u8> = None; 81 let mut notify: Option<(u8, u8)> = None; // (interface number, endpoint address) 82 83 println!("\n== Interfaces =="); 84 for iface in config.interfaces() { 85 for d in iface.descriptors() { 86 let ifn = d.interface_number(); 87 let class = d.class_code(); 88 let sub = d.sub_class_code(); 89 println!( 90 " iface {ifn} alt {} class={class:#04x} subclass={sub:#04x} endpoints={}", 91 d.setting_number(), 92 d.num_endpoints() 93 ); 94 // The scarlett2 control protocol (Gen3+) lives on the VENDOR-SPECIFIC 95 // interface (class 0xFF) that carries the interrupt notify endpoint — 96 // NOT the USB-Audio control interface. wIndex must be this interface. 97 if class == 0xFF { 98 control_iface.get_or_insert(ifn); 99 } 100 for ep in d.endpoint_descriptors() { 101 let addr = ep.address(); 102 let is_int = ep.transfer_type() == TransferType::Interrupt; 103 let is_in = ep.direction() == Direction::In; 104 println!( 105 " ep {addr:#04x} {} {}", 106 if is_in { "IN " } else { "OUT" }, 107 transfer_type_name(ep.transfer_type()) 108 ); 109 if is_int && is_in { 110 notify.get_or_insert((ifn, addr)); 111 } 112 } 113 } 114 } 115 116 // Prefer the vendor-specific interface; otherwise whichever carries the 117 // interrupt-in endpoint; last resort interface 0. 118 let ctl_iface = control_iface.or(notify.map(|(i, _)| i)).unwrap_or(0); 119 println!("\n -> using control interface {ctl_iface}"); 120 if let Some((ifn, ep)) = notify { 121 println!(" -> candidate notify endpoint {ep:#04x} on interface {ifn}"); 122 } else { 123 println!(" -> no interrupt-in endpoint found (will rely on polling)"); 124 } 125 126 // --- 2. Open the device. This is the first macOS gate: can a userspace app 127 // open the device while Core Audio drives the audio interfaces? --- 128 let handle = device.open().context( 129 "open device — if this fails with 'access denied' or 'busy', quit Focusrite Control", 130 )?; 131 println!("\n\x1b[32mOK\x1b[0m device opened (Core Audio should still be streaming)"); 132 133 // --- 3. scarlett2 INIT handshake over EP0 (mirrors the kernel exactly). --- 134 // 135 // step 0: raw control-IN, bRequest = CMD_INIT (0) — primes the device. 136 // INIT_1: packet cmd 0, seq reset to 1 (the "seq sent=1, response=0" quirk). 137 // INIT_2: packet cmd 2, seq reset to 1; firmware version is u32 LE at byte 8. 138 println!("\n== INIT step 0 (raw read, bRequest=0) =="); 139 { 140 let rt_in = rusb::request_type(Direction::In, RequestType::Class, Recipient::Interface); 141 let mut buf = [0u8; 24]; 142 match handle.read_control(rt_in, CMD_INIT, 0, ctl_iface as u16, &mut buf, TIMEOUT) { 143 Ok(n) => println!(" ok, {n} bytes: {}", hex(&buf[..n])), 144 Err(e) => eprintln!(" step 0 failed (continuing): {e}"), 145 } 146 } 147 148 let mut seq: u16 = 1; 149 150 println!("\n== INIT_1 =="); 151 // seq already 1 152 match cmd(&handle, ctl_iface, &mut seq, CMD_INIT_1, &[], 0) { 153 Ok(data) => println!(" ok ({} payload bytes){}", data.len(), maybe_hex(&data)), 154 Err(e) => return Err(e).context("INIT_1 — the handshake gate"), 155 } 156 157 println!("\n== INIT_2 (device info / firmware) =="); 158 seq = 1; // reset per kernel 159 match cmd(&handle, ctl_iface, &mut seq, CMD_INIT_2, &[], 32) { 160 Ok(data) => { 161 println!(" ok, {} bytes: {}", data.len(), hex(&data)); 162 if data.len() >= 12 { 163 let fw = u32::from_le_bytes(data[8..12].try_into().unwrap()); 164 println!(" \x1b[36mfirmware version: {fw}\x1b[0m"); 165 } 166 } 167 Err(e) => eprintln!(" INIT_2 failed (non-fatal for the gate): {e:#}"), 168 } 169 170 println!("\n== GET_SYNC (clock lock status) =="); 171 match cmd(&handle, ctl_iface, &mut seq, CMD_GET_SYNC, &[], 16) { 172 Ok(data) => println!(" ok, {} bytes: {} (nonzero usually = locked)", data.len(), hex(&data)), 173 Err(e) => eprintln!(" GET_SYNC failed: {e:#}"), 174 } 175 176 println!("\n== GET_DATA (read config offset 0, 8 bytes) =="); 177 // GET_DATA payload = { offset: u32 LE, size: u32 LE } 178 let mut p = Vec::new(); 179 p.extend_from_slice(&0u32.to_le_bytes()); // offset 0 180 p.extend_from_slice(&8u32.to_le_bytes()); // 8 bytes 181 match cmd(&handle, ctl_iface, &mut seq, CMD_GET_DATA, &p, 8) { 182 Ok(data) => println!(" ok, {} bytes: {}", data.len(), hex(&data)), 183 Err(e) => eprintln!(" GET_DATA failed: {e:#}"), 184 } 185 186 // --- 4. Notification endpoint: try to claim the interface and read it. 187 // On macOS this likely fails (kernel owns the audio interface) — in 188 // which case we fall back to polling, which is fine. --------------- 189 if let Some((ifn, ep)) = notify { 190 println!("\n== Notify endpoint (claim + interrupt read, 500ms) =="); 191 match handle.claim_interface(ifn) { 192 Ok(()) => { 193 println!(" claimed interface {ifn}; waiting for a notification..."); 194 let mut buf = [0u8; 64]; 195 match handle.read_interrupt(ep, &mut buf, Duration::from_millis(500)) { 196 Ok(n) => println!(" got {n} bytes: {}", hex(&buf[..n])), 197 Err(rusb::Error::Timeout) => { 198 println!(" no notification within 500ms (expected when idle)") 199 } 200 Err(e) => eprintln!(" interrupt read error: {e}"), 201 } 202 let _ = handle.release_interface(ifn); 203 } 204 Err(e) => { 205 println!( 206 " could not claim interface {ifn}: {e}\n -> EXPECTED on macOS; we'll use polling for live updates." 207 ); 208 } 209 } 210 } 211 212 println!("\n\x1b[32mSPIKE PASSED\x1b[0m — EP0 control works; build the TUI on this foundation."); 213 println!("Confirm audio kept playing throughout, then hand the device back to Focusrite Control if needed."); 214 Ok(()) 215 } 216 217 /// One scarlett2 request/response round-trip over EP0. 218 fn cmd( 219 handle: &rusb::DeviceHandle<rusb::Context>, 220 iface: u8, 221 seq: &mut u16, 222 cmd: u32, 223 payload: &[u8], 224 resp_payload_cap: usize, 225 ) -> Result<Vec<u8>> { 226 let this_seq = *seq; 227 *seq = seq.wrapping_add(1); 228 229 // request packet: 16-byte header + payload 230 let mut req = Vec::with_capacity(HDR + payload.len()); 231 req.extend_from_slice(&cmd.to_le_bytes()); 232 req.extend_from_slice(&(payload.len() as u16).to_le_bytes()); 233 req.extend_from_slice(&this_seq.to_le_bytes()); 234 req.extend_from_slice(&0u32.to_le_bytes()); // error 235 req.extend_from_slice(&0u32.to_le_bytes()); // pad 236 req.extend_from_slice(payload); 237 238 let rt_out = rusb::request_type(Direction::Out, RequestType::Class, Recipient::Interface); 239 handle 240 .write_control(rt_out, REQ, 0, iface as u16, &req, TIMEOUT) 241 .context("control OUT (CMD_REQ)")?; 242 243 let mut buf = vec![0u8; HDR + resp_payload_cap]; 244 let rt_in = rusb::request_type(Direction::In, RequestType::Class, Recipient::Interface); 245 let n = handle 246 .read_control(rt_in, RESP, 0, iface as u16, &mut buf, TIMEOUT) 247 .context("control IN (CMD_RESP)")?; 248 buf.truncate(n); 249 250 if n < HDR { 251 bail!("short response: {n} bytes (< {HDR}-byte header)"); 252 } 253 let r_cmd = u32::from_le_bytes(buf[0..4].try_into().unwrap()); 254 let r_size = u16::from_le_bytes(buf[4..6].try_into().unwrap()); 255 let r_seq = u16::from_le_bytes(buf[6..8].try_into().unwrap()); 256 let r_err = u32::from_le_bytes(buf[8..12].try_into().unwrap()); 257 258 if r_err != 0 { 259 bail!("device error code {r_err:#x} (cmd {cmd:#x}, seq {this_seq})"); 260 } 261 if r_cmd != cmd { 262 eprintln!(" warn: response cmd {r_cmd:#x} != request {cmd:#x}"); 263 } 264 if r_seq != this_seq { 265 eprintln!(" warn: response seq {r_seq} != request {this_seq}"); 266 } 267 268 let end = (HDR + r_size as usize).min(buf.len()); 269 Ok(buf[HDR..end].to_vec()) 270 } 271 272 fn transfer_type_name(t: rusb::TransferType) -> &'static str { 273 match t { 274 rusb::TransferType::Control => "control", 275 rusb::TransferType::Isochronous => "isochronous", 276 rusb::TransferType::Bulk => "bulk", 277 rusb::TransferType::Interrupt => "interrupt", 278 } 279 } 280 281 fn hex(b: &[u8]) -> String { 282 b.iter().map(|x| format!("{x:02x} ")).collect() 283 } 284 285 fn maybe_hex(b: &[u8]) -> String { 286 if b.is_empty() { 287 String::new() 288 } else { 289 format!(": {}", hex(b)) 290 } 291 }