Skip to content
Closed
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
32 changes: 31 additions & 1 deletion crossplatform/windows/daemon/src/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ const IOCTL_LP_CONNECT: u32 = 0x8000_2000;
const IOCTL_LP_SEND: u32 = 0x8000_2008;
const IOCTL_LP_RECEIVE: u32 = 0x8000_200C;
const IOCTL_LP_GET_STATUS: u32 = 0x8000_2010;
const IOCTL_LP_ATT_SEND: u32 = 0x8000_2014;
const IOCTL_LP_ATT_RECEIVE: u32 = 0x8000_2018;

struct DriverHandle(HANDLE);
unsafe impl Send for DriverHandle {}
Expand Down Expand Up @@ -98,13 +100,41 @@ impl Driver {
Ok(ioctl(self.handle.0, IOCTL_LP_RECEIVE, &to, buf)? as usize)
}

/// Send a raw ATT PDU over the ATT (PSM 0x001F) hearing-aid channel.
pub fn att_send(&self, data: &[u8]) -> io::Result<()> {
ioctl(self.handle.0, IOCTL_LP_ATT_SEND, data, &mut [])?;
Ok(())
}

/// Receive a raw ATT PDU from the ATT channel (blocking up to timeout_ms).
pub fn att_recv(&self, timeout_ms: u32, buf: &mut [u8]) -> io::Result<usize> {
let to = timeout_ms.to_le_bytes();
Ok(ioctl(self.handle.0, IOCTL_LP_ATT_RECEIVE, &to, buf)? as usize)
}

/// Driver connection state (2 = connected). Reads a state variable only —
/// no L2CAP I/O, so it never disturbs the audio link.
pub fn status(&self) -> io::Result<u32> {
let mut out = [0u8; 12];
let mut out = [0u8; 32];
ioctl(self.handle.0, IOCTL_LP_GET_STATUS, &[], &mut out)?;
Ok(u32::from_le_bytes([out[0], out[1], out[2], out[3]]))
}

/// ATT (PSM 0x001F) hearing-aid server diagnostics from the driver:
/// (register_ntstatus, server_registered, connect_indications, accept_ntstatus,
/// channel_open). Lets us see the hearing-aid channel progress in the daemon log
/// without a kernel debugger.
pub fn att_diag(&self) -> io::Result<(i32, u32, u32, i32, u32)> {
let mut out = [0u8; 32];
ioctl(self.handle.0, IOCTL_LP_GET_STATUS, &[], &mut out)?;
Ok((
i32::from_le_bytes([out[28], out[29], out[30], out[31]]), // register status
u32::from_le_bytes([out[12], out[13], out[14], out[15]]), // registered 0/1
u32::from_le_bytes([out[16], out[17], out[18], out[19]]), // indications
i32::from_le_bytes([out[20], out[21], out[22], out[23]]), // accept status
u32::from_le_bytes([out[24], out[25], out[26], out[27]]), // channel open
))
}
}

fn open_driver() -> io::Result<HANDLE> {
Expand Down
121 changes: 121 additions & 0 deletions crossplatform/windows/daemon/src/hearing.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
//! AirPods Pro 3 hearing assistance: enable it over AAP (control commands 0x2C /
//! 0x33), then write the amplification settings to the ATT/GATT (PSM 0x001F, handle
//! 0x2A) via a read-modify-write. The layout mirrors the Android client
//! (HearingAidEnums): 8-band EQ per ear + per-ear amplification/tone/conversation-
//! boost as little-endian f32. We leave the audiogram EQ bands untouched for now and
//! drive only the overall amplification / balance / conversation boost.

use crate::aap;
use crate::driver::Driver;
use std::{thread, time::Duration};

// AAP hearing-assist enable/disable (0x09 control commands 0x2C / 0x33).
const HA_ON_2C: [u8; 11] = [0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x2C, 0x01, 0x01, 0x00, 0x00];
const HA_ON_33: [u8; 11] = [0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x33, 0x01, 0x00, 0x00, 0x00];
const HA_OFF_2C: [u8; 11] = [0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x2C, 0x01, 0x02, 0x00, 0x00];
const HA_OFF_33: [u8; 11] = [0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x33, 0x02, 0x00, 0x00, 0x00];

const H_SETTINGS: u16 = 0x002A; // hearing-aid settings characteristic
const H_CCCD: u16 = 0x002B; // its client-config descriptor

// f32 offsets into the settings value (bytes after the ATT opcode).
const OFF_MODE: usize = 2;
const OFF_LEFT_AMP: usize = 36;
const OFF_LEFT_TONE: usize = 40;
const OFF_LEFT_CONV: usize = 44;
const OFF_RIGHT_AMP: usize = 84;
const OFF_RIGHT_TONE: usize = 88;
const OFF_RIGHT_CONV: usize = 92;

fn put_f32(buf: &mut [u8], off: usize, v: f32) {
if off + 4 <= buf.len() {
buf[off..off + 4].copy_from_slice(&v.to_le_bytes());
}
}

fn att_read_req(handle: u16) -> [u8; 3] {
[0x0A, (handle & 0xff) as u8, (handle >> 8) as u8]
}

fn att_write_pdu(handle: u16, value: &[u8]) -> Vec<u8> {
let mut p = vec![0x12u8, (handle & 0xff) as u8, (handle >> 8) as u8];
p.extend_from_slice(value);
p
}

/// Apply hearing-assist settings. Requires the AAP + ATT channels to be up (the
/// driver opens ATT on connect). Returns a short summary for the daemon log.
pub fn apply(
drv: &Driver,
on: bool,
amplification: f32,
balance: f32,
conv_boost: bool,
) -> Result<String, String> {
if !on {
let _ = drv.send(&HA_OFF_33);
thread::sleep(Duration::from_millis(300));
let _ = drv.send(&HA_OFF_2C);
return Ok("hearing aid OFF".into());
}

// 1) Wake the buds' hearing-aid ATT server (it is dormant until enabled), and
// switch to Transparency (mode 3) so ambient sound passes through to be
// amplified — in ANC/Off there is nothing to amplify.
let _ = drv.send(&HA_ON_2C);
thread::sleep(Duration::from_millis(300));
let _ = drv.send(&aap::anc_command(3));
thread::sleep(Duration::from_millis(200));
let _ = drv.send(&HA_ON_33);
thread::sleep(Duration::from_millis(900));

// 2) Enable notifications on the settings CCCD.
let mut b = [0u8; 512];
let _ = drv.att_send(&att_write_pdu(H_CCCD, &[0x01, 0x00]));
let _ = drv.att_recv(2000, &mut b);

// 3) Read the current settings value (read-modify-write).
let _ = drv.att_send(&att_read_req(H_SETTINGS));
let n = drv
.att_recv(2000, &mut b)
.map_err(|e| format!("ATT read err: {e}"))?;
if n < 8 || b[0] != 0x0B {
return Err(format!("bad ATT read resp [{n}]"));
}
let mut val = b[1..n].to_vec(); // the characteristic value (~104 bytes)

// 4) Patch amplification / balance / conversation boost. Audiogram EQ untouched.
let amp = amplification.clamp(0.0, 1.0);
let bal = balance.clamp(-1.0, 1.0);
let left_amp = (amp + if bal < 0.0 { -bal } else { 0.0 }).clamp(0.0, 1.0);
let right_amp = (amp + if bal > 0.0 { bal } else { 0.0 }).clamp(0.0, 1.0);
let cb = if conv_boost { 1.0f32 } else { 0.0f32 };
if val.len() > OFF_MODE {
val[OFF_MODE] = 0x64;
}
// Flat audiogram: a broadband gain across all 8 EQ bands per ear. A zero
// audiogram leaves the amplification nothing to scale (you'd hear nothing), so
// we synthesize a flat boost from the slider. BAND_GAIN is a first guess at the
// units (dB-ish) — tune against hardware.
const BAND_GAIN: f32 = 30.0;
for i in 0..8usize {
put_f32(&mut val, 4 + i * 4, left_amp * BAND_GAIN);
put_f32(&mut val, 52 + i * 4, right_amp * BAND_GAIN);
}
put_f32(&mut val, OFF_LEFT_AMP, left_amp);
put_f32(&mut val, OFF_LEFT_TONE, 0.0);
put_f32(&mut val, OFF_LEFT_CONV, cb);
put_f32(&mut val, OFF_RIGHT_AMP, right_amp);
put_f32(&mut val, OFF_RIGHT_TONE, 0.0);
put_f32(&mut val, OFF_RIGHT_CONV, cb);

// 5) Write it back.
let _ = drv.att_send(&att_write_pdu(H_SETTINGS, &val));
let wn = drv.att_recv(2000, &mut b).unwrap_or(0);
let wr = if wn >= 1 && b[0] == 0x13 { "ok" } else { "no-resp" };

Ok(format!(
"hearing aid ON: wrote {} bytes leftAmp={left_amp:.2} rightAmp={right_amp:.2} conv={conv_boost} write={wr}",
val.len()
))
}
33 changes: 33 additions & 0 deletions crossplatform/windows/daemon/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ mod aap;
mod bt;
mod driver;
mod eld;
mod hearing;
mod hr;
mod le;
mod media;
Expand Down Expand Up @@ -745,6 +746,20 @@ fn apply_command(ctx: &Ctx, cmd: Command) {
ctx.sync_volume();
}
Command::SetHeartRate { on } => set_heart_rate(ctx, on),
Command::SetHearingAid { on, amplification, balance, conversation_boost } => {
// Runs on its own thread — hearing::apply has ~1.3 s of enable settle
// sleeps + ATT round-trips and must not block the command pump.
if let Some(drv) = ctx.driver_cell.lock().unwrap().clone() {
let ctx2 = ctx.clone();
thread::spawn(move || match hearing::apply(&drv, on, amplification, balance, conversation_boost) {
Ok(s) => {
log(&s);
ctx2.overlay(if on { "Hearing aid on" } else { "Hearing aid off" });
}
Err(e) => log(&format!("hearing aid FAILED: {e}")),
});
}
}
Command::Connect => {
// The user accepted the prompt — let the session start, and ask the OS
// to (re)connect the audio in case the device was BT-disconnected.
Expand Down Expand Up @@ -893,7 +908,25 @@ fn run_receiver(ctx: Ctx) {
// ears) and on a real disconnect (cased / on the phone) — status alone
// can't tell them apart, so data flow is the tie-breaker.
let mut last_data = Instant::now();
// ATT (PSM 0x001F) hearing-aid server diagnostics, polled from the driver
// and logged on change (DebugView never showed the driver's KdPrint).
let mut att_poll = Instant::now();
let mut last_att: (i32, u32, u32, i32, u32) = (0, 0, 0, 0, 0);
loop {
if att_poll.elapsed() >= Duration::from_millis(1500) {
att_poll = Instant::now();
if let Ok(d) = driver.att_diag() {
if d != last_att {
let was_open = last_att.4;
last_att = d;
let _ = was_open;
log(&format!(
"ATT: register=0x{:08X} registered={} indications={} accept=0x{:08X} channel_open={}",
d.0 as u32, d.1, d.2, d.3 as u32, d.4
));
}
}
}
// The user pressed Disconnect (connect_requested cleared) — release.
if !ctx.connect_requested.load(Ordering::Relaxed) {
log("run_receiver: disconnect requested — releasing");
Expand Down
10 changes: 10 additions & 0 deletions crossplatform/windows/drivers/aap/Device.c
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ LpEvtDevicePrepareHardware(

ctx->HasBthInterface = TRUE;
KdPrint(("LibrePodsAAP: acquired BTH profile interface\n"));

// NB: the ATT (PSM 0x001F) server is registered later, from LpConnect, once we
// know the AirPods' address (registering with BtAddress=0 here returned
// STATUS_INVALID_PARAMETER 0xC000000D).

return STATUS_SUCCESS;
}

Expand All @@ -62,6 +67,11 @@ LpEvtDeviceReleaseHardware(
LpDisconnect(ctx);
}

// Close the accepted ATT channel, then unregister the server, before dropping
// the interface (both use it).
LpCloseAttChannel(ctx);
LpUnregisterAttServer(ctx);

// Release the Bluetooth profile driver interface we took in
// PrepareHardware. WdfFdoQueryForInterface increments the interface's
// reference count; not dereferencing it leaks a reference to our BTHENUM
Expand Down
17 changes: 17 additions & 0 deletions crossplatform/windows/drivers/aap/Driver.c
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ LpEvtDeviceAdd(
ctx = DeviceGetContext(device);
RtlZeroMemory(ctx, sizeof(*ctx));
ctx->State = LpDisconnected;
ctx->AttAcceptStatus = STATUS_PENDING; // 0x00000103 = accept not yet attempted
ctx->AttRegisterStatus = STATUS_PENDING; // 0x00000103 = register not yet attempted
ctx->WdmDeviceObject = WdfDeviceWdmGetDeviceObject(device);

status = WdfSpinLockCreate(WDF_NO_OBJECT_ATTRIBUTES, &ctx->Lock);
Expand All @@ -91,6 +93,21 @@ LpEvtDeviceAdd(
return status;
}

// Work item that accepts the AirPods' inbound ATT (PSM 0x001F) connection at
// PASSIVE_LEVEL (the connect indication may run at DISPATCH_LEVEL).
{
WDF_WORKITEM_CONFIG wiConfig;
WDF_OBJECT_ATTRIBUTES wiAttrs;
WDF_WORKITEM_CONFIG_INIT(&wiConfig, LpAttAcceptWorkItem);
WDF_OBJECT_ATTRIBUTES_INIT(&wiAttrs);
wiAttrs.ParentObject = device;
status = WdfWorkItemCreate(&wiConfig, &wiAttrs, &ctx->AttAcceptWorkItem);
if (!NT_SUCCESS(status)) {
KdPrint(("LibrePodsAAP: WdfWorkItemCreate failed 0x%08X\n", status));
return status;
}
}

// Single sequential IOCTL queue (connect/send/receive are serialized).
WDF_IO_QUEUE_CONFIG_INIT_DEFAULT_QUEUE(&queueConfig, WdfIoQueueDispatchSequential);
queueConfig.EvtIoDeviceControl = LpEvtIoDeviceControl;
Expand Down
48 changes: 44 additions & 4 deletions crossplatform/windows/drivers/aap/Ioctl.c
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,50 @@ LpEvtIoDeviceControl(
status = WdfRequestRetrieveOutputBuffer(Request, sizeof(LP_STATUS_OUTPUT), &outBuf, &sz);
if (!NT_SUCCESS(status)) break;

out = (PLP_STATUS_OUTPUT)outBuf;
out->State = (ULONG)ctx->State;
out->ConnectedAddress = ctx->RemoteAddress;
information = sizeof(LP_STATUS_OUTPUT);
out = (PLP_STATUS_OUTPUT)outBuf;
out->State = (ULONG)ctx->State;
out->ConnectedAddress = ctx->RemoteAddress;
out->AttServerRegistered = ctx->AttServerRegistered ? 1u : 0u;
out->AttIndicationCount = ctx->AttIndicationCount;
out->AttAcceptStatus = ctx->AttAcceptStatus;
out->AttChannelOpen = ctx->AttConnected ? 1u : 0u;
out->AttRegisterStatus = ctx->AttRegisterStatus;
information = sizeof(LP_STATUS_OUTPUT);
break;
}

case IOCTL_LP_ATT_SEND: {
if (InputBufferLength == 0) {
status = STATUS_INVALID_PARAMETER;
break;
}
status = WdfRequestRetrieveInputBuffer(Request, 1, &inBuf, &sz);
if (!NT_SUCCESS(status)) break;
status = LpAttSend(ctx, inBuf, (ULONG)sz);
break;
}

case IOCTL_LP_ATT_RECEIVE: {
ULONG timeoutMs = 0;
ULONG bytesRead = 0;

if (InputBufferLength >= sizeof(LP_RECEIVE_INPUT)) {
status = WdfRequestRetrieveInputBuffer(Request, sizeof(LP_RECEIVE_INPUT), &inBuf, &sz);
if (NT_SUCCESS(status)) {
timeoutMs = ((PLP_RECEIVE_INPUT)inBuf)->TimeoutMs;
}
}
if (OutputBufferLength == 0) {
status = STATUS_BUFFER_TOO_SMALL;
break;
}
status = WdfRequestRetrieveOutputBuffer(Request, 1, &outBuf, &sz);
if (!NT_SUCCESS(status)) break;

status = LpAttReceive(ctx, outBuf, (ULONG)sz, &bytesRead, timeoutMs);
if (NT_SUCCESS(status)) {
information = bytesRead;
}
break;
}

Expand Down
Loading
Loading