use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Instant;
use anyhow::Result;
use btleplug::api::{Central as _, Peripheral as _, ScanFilter, WriteType};
use btleplug::platform::{Adapter, Peripheral};
use futures_util::StreamExt;
use tokio::sync::broadcast;
use crate::kernel::consumer_hold::ConsumerHeldTracker;
use crate::kernel::event::{
AiVoiceKeyEvent, BoardEvent, ErrorEvent, KeySource, ModeChangeEvent, ModeSource,
};
use crate::kernel::key_aggregator::KeyStateAggregator;
use crate::kernel::protocol_gatt as protocol;
use crate::kernel::protocol_hid::{
find_key_index_by_value, is_ai_voice_consumer_code, key_index_to_mode, WorkMode,
CMD_AUDIO_DATA, CMD_DEVICE_DISCONNECT, CMD_STATUS, CMD_WORK_MODE_DATA,
};
#[cfg(feature = "test-mode")]
use crate::kernel::protocol_hid::{parse_factory_key_event_unscoped, CMD_AI_FACTORY_KEY_EVENT};
use crate::kernel::sink::AudioFrameSink;
const CCC_DESCRIPTOR_UUID: uuid::Uuid = uuid::Uuid::from_bytes([
0x00, 0x00, 0x29, 0x02, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0x80, 0x5f, 0x9b, 0x34, 0xfb,
]);
#[derive(Debug, Clone, serde::Serialize)]
pub struct BleDeviceInfo {
pub name: String,
pub id: String,
pub rssi: Option<i16>,
pub address: String,
}
#[derive(Clone)]
struct GattChars {
cmd: btleplug::api::Characteristic,
event: btleplug::api::Characteristic,
audio: btleplug::api::Characteristic,
}
struct PendingResponse {
#[allow(dead_code)]
cmd: u8,
tx: tokio::sync::oneshot::Sender<Vec<u8>>,
}
#[derive(Default)]
struct GattEventState {
consumer: ConsumerHeldTracker,
aggregator: Option<KeyStateAggregator>,
ai_voice_pressed: bool,
mode_key_pressed: usize,
}
impl GattEventState {
fn release_all(&mut self, event_tx: &broadcast::Sender<BoardEvent>) {
if let Some(aggregator) = &self.aggregator {
aggregator.report_change(KeySource::Gatt, Vec::new(), None);
}
if self.ai_voice_pressed {
self.ai_voice_pressed = false;
log::info!(target: "gatt", "通知监听退出,补发 AI 语音键释放");
let _ = event_tx.send(BoardEvent::AiVoiceKey(AiVoiceKeyEvent { pressed: false }));
}
}
}
pub struct VendorGattClient {
adapter: Adapter,
peripheral: Mutex<Option<Peripheral>>,
chars: Mutex<Option<GattChars>>,
running: Arc<AtomicBool>,
audio_sink: Arc<dyn AudioFrameSink>,
event_tx: broadcast::Sender<BoardEvent>,
pending_responses: Arc<Mutex<HashMap<u8, PendingResponse>>>,
write_lock: tokio::sync::Mutex<()>,
seq: AtomicU64,
notify_handle: Mutex<Option<tokio::task::JoinHandle<()>>>,
audio_thread_handle: Mutex<Option<std::thread::JoinHandle<()>>>,
}
impl VendorGattClient {
pub fn new(
adapter: Adapter,
audio_sink: Arc<dyn AudioFrameSink>,
event_tx: broadcast::Sender<BoardEvent>,
) -> Self {
Self {
adapter,
peripheral: Mutex::new(None),
chars: Mutex::new(None),
running: Arc::new(AtomicBool::new(false)),
audio_sink,
event_tx,
pending_responses: Arc::new(Mutex::new(HashMap::new())),
write_lock: tokio::sync::Mutex::new(()),
seq: AtomicU64::new(0),
notify_handle: Mutex::new(None),
audio_thread_handle: Mutex::new(None),
}
}
pub async fn scan_for_device(&self, target_name: Option<&str>) -> Result<(Peripheral, String)> {
let adapter = self.adapter.clone();
let target = target_name.map(|s| s.to_string());
adapter
.start_scan(ScanFilter::default())
.await
.map_err(|e| anyhow::anyhow!("BLE 扫描启动失败: {}", e))?;
for round in 0..5 {
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
let peripherals = adapter
.peripherals()
.await
.map_err(|e| anyhow::anyhow!("获取设备列表失败: {}", e))?;
for p in &peripherals {
if let Ok(Some(props)) = p.properties().await {
let name = props.local_name.unwrap_or_default();
let matched = match &target {
Some(t) => name == *t,
None => name.starts_with(protocol::VENDOR_DEVICE_PREFIX),
};
if matched {
log::info!(target: "gatt", "发现 Vendor GATT 设备: {} ({})", name, p.id());
if let Err(e) = adapter.stop_scan().await {
log::warn!(target: "gatt", "stop_scan 失败(可能仍占用适配器): {}", e);
}
return Ok((p.clone(), name));
}
}
}
log::debug!(
target: "gatt",
"扫描轮次 {}/5 未找到{}",
round + 1,
target.as_ref().map(|t| format!(" {}", t)).unwrap_or_default()
);
}
if let Err(e) = adapter.stop_scan().await {
log::warn!(target: "gatt", "stop_scan 失败(可能仍占用适配器): {}", e);
}
Err(anyhow::anyhow!(
"未发现 Vendor GATT 设备{}",
target
.as_ref()
.map(|t| format!(" {}", t))
.unwrap_or_default()
))
}
pub async fn scan_all_vendor_devices(
&self,
timeout: std::time::Duration,
) -> Result<Vec<BleDeviceInfo>> {
let adapter = self.adapter.clone();
adapter
.start_scan(ScanFilter::default())
.await
.map_err(|e| anyhow::anyhow!("BLE 扫描启动失败: {}", e))?;
let mut seen: std::collections::HashMap<String, BleDeviceInfo> =
std::collections::HashMap::new();
let deadline = tokio::time::Instant::now() + timeout;
while tokio::time::Instant::now() < deadline {
let peripherals = adapter
.peripherals()
.await
.map_err(|e| anyhow::anyhow!("获取设备列表失败: {}", e))?;
for p in &peripherals {
if let Ok(Some(props)) = p.properties().await {
let name = props.local_name.unwrap_or_default();
if name.starts_with(protocol::VENDOR_DEVICE_PREFIX) {
let id_str = format!("{:?}", p.id());
seen.entry(id_str.clone()).or_insert(BleDeviceInfo {
name: name.clone(),
id: id_str,
rssi: props.rssi,
address: format!("{}", props.address),
});
}
}
}
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
}
if let Err(e) = adapter.stop_scan().await {
log::warn!(target: "gatt", "stop_scan 失败(可能仍占用适配器): {}", e);
}
Ok(seen.into_values().collect())
}
pub async fn connect(&self, peripheral: &Peripheral) -> Result<()> {
let p = peripheral.clone();
let inner = async {
p.connect()
.await
.map_err(|e| anyhow::anyhow!("Vendor GATT 连接失败: {}", e))?;
log::info!(target: "gatt", "BLE 已连接,开始服务发现");
p.discover_services()
.await
.map_err(|e| anyhow::anyhow!("GATT 服务发现失败: {}", e))?;
let chars = p.characteristics();
log::info!(target: "gatt", "GATT 服务发现完成,共 {} 个特征值", chars.len());
let cmd = chars
.iter()
.find(|c| c.uuid == protocol::CMD_CHAR_UUID)
.ok_or_else(|| anyhow::anyhow!("未找到 Command 特征值 (FE61)"))?;
let event = chars
.iter()
.find(|c| c.uuid == protocol::EVENT_CHAR_UUID)
.ok_or_else(|| anyhow::anyhow!("未找到 Event 特征值 (FE62)"))?;
let audio = chars
.iter()
.find(|c| c.uuid == protocol::AUDIO_CHAR_UUID)
.ok_or_else(|| anyhow::anyhow!("未找到 Audio 特征值 (FE63)"))?;
subscribe_with_ccc_fallback(&p, event, "Event").await?;
subscribe_with_ccc_fallback(&p, audio, "Audio").await?;
Ok(GattChars {
cmd: cmd.clone(),
event: event.clone(),
audio: audio.clone(),
})
};
let found_chars =
match tokio::time::timeout(std::time::Duration::from_secs(10), inner).await {
Ok(r) => r,
Err(_) => Err(anyhow::anyhow!(
"BLE connect 超时(10s)— 可能 CoreBluetooth Event receiver died,将重试"
)),
}?;
*self.peripheral.lock().unwrap() = Some(peripheral.clone());
*self.chars.lock().unwrap() = Some(found_chars);
self.running.store(true, Ordering::SeqCst);
Ok(())
}
pub async fn send_command(&self, data: &[u8]) -> Result<()> {
let p = self
.peripheral
.lock()
.unwrap()
.clone()
.ok_or_else(|| anyhow::anyhow!("Vendor GATT 未连接"))?;
let chars = self
.chars
.lock()
.unwrap()
.clone()
.ok_or_else(|| anyhow::anyhow!("GATT 特征值未发现"))?;
let cmd_data = data.to_vec();
let seq = self.seq.fetch_add(1, Ordering::SeqCst);
log::debug!(target: "gatt", "[GATT:{}] TX CMD=0x{:02X} len={}", seq, cmd_data[0], cmd_data.len());
tokio::time::timeout(
std::time::Duration::from_secs(5),
p.write(&chars.cmd, &cmd_data, WriteType::WithResponse),
)
.await
.map_err(|_| anyhow::anyhow!("GATT 写入超时 5s"))?
.map_err(|e| anyhow::anyhow!("GATT 写入失败: {}", e))
}
pub async fn send_command_and_read_response(
&self,
expected_cmd: u8,
data: &[u8],
timeout_ms: u64,
) -> Result<Vec<u8>> {
let _write_guard = self.write_lock.lock().await;
log::info!(
target: "gatt",
"[GATT] cmd_and_read: CMD=0x{:02X} len={} timeout={}ms",
expected_cmd, data.len(), timeout_ms
);
let (tx, rx) = tokio::sync::oneshot::channel::<Vec<u8>>();
{
let mut pending = self.pending_responses.lock().unwrap();
if let Some(old) = pending.remove(&expected_cmd) {
log::warn!(target: "gatt", "[GATT] 清理残留 pending CMD=0x{:02X}", expected_cmd);
let _ = old.tx.send(vec![]); }
pending.insert(
expected_cmd,
PendingResponse {
cmd: expected_cmd,
tx,
},
);
}
if let Err(e) = self.send_command(data).await {
self.pending_responses.lock().unwrap().remove(&expected_cmd);
return Err(e);
}
match tokio::time::timeout(std::time::Duration::from_millis(timeout_ms), rx).await {
Ok(Ok(response)) => Ok(response),
Ok(Err(_)) => {
self.pending_responses.lock().unwrap().remove(&expected_cmd);
Err(anyhow::anyhow!(
"[GATT] CMD=0x{:02X} 响应通道已关闭(notification loop 已退出)",
expected_cmd
))
}
Err(_) => {
self.pending_responses.lock().unwrap().remove(&expected_cmd);
Err(anyhow::anyhow!(
"[GATT] CMD=0x{:02X} 响应超时 ({}ms)",
expected_cmd,
timeout_ms
))
}
}
}
pub fn start_notification_loop(&self) {
let p = match self.peripheral.lock().unwrap().clone() {
Some(p) => p,
None => return,
};
let running = self.running.clone();
let audio_sink = self.audio_sink.clone();
let event_tx = self.event_tx.clone();
let pending = self.pending_responses.clone();
let event_uuid = protocol::EVENT_CHAR_UUID;
let audio_uuid = protocol::AUDIO_CHAR_UUID;
let (audio_tx, audio_rx) = std::sync::mpsc::sync_channel::<Vec<u8>>(64);
let audio_running = running.clone();
let audio_handler = audio_sink.clone();
let audio_thread = std::thread::spawn(move || {
const MSBC_FRAME_SIZE: usize = 57;
let mut processed: u64 = 0;
while audio_running.load(Ordering::SeqCst) {
match audio_rx.recv_timeout(std::time::Duration::from_secs(1)) {
Ok(data) => {
if let Some((_flag, frames_data)) = protocol::parse_audio_packet(&data) {
for frame in frames_data.chunks(MSBC_FRAME_SIZE) {
if frame.len() == MSBC_FRAME_SIZE {
audio_handler.on_msbc_frame(frame);
processed += 1;
}
}
} else {
log::warn!(target: "gatt", "[GATT-audio] 解析失败: data_len={}", data.len());
}
}
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => continue,
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break,
}
}
log::debug!(target: "gatt", "音频处理线程退出,共处理 {} 帧", processed);
});
if let Ok(mut slot) = self.audio_thread_handle.lock() {
*slot = Some(audio_thread);
}
let notify_handle = tokio::spawn(async move {
let mut stream = match p.notifications().await {
Ok(s) => s,
Err(e) => {
log::warn!(target: "gatt", "通知流创建失败: {}", e);
return;
}
};
log::info!(target: "gatt", "通知监听已启动");
let mut state = GattEventState::default();
let mut notif_count: u64 = 0;
let mut audio_count: u64 = 0;
let heartbeat = std::time::Duration::from_secs(10);
let mut last_notif = tokio::time::Instant::now();
while running.load(Ordering::SeqCst) {
tokio::select! {
notification = stream.next() => {
last_notif = tokio::time::Instant::now();
match notification {
Some(notif) => {
notif_count += 1;
if notif.uuid == audio_uuid {
audio_count += 1;
match audio_tx.try_send(notif.value.to_vec()) {
Ok(_) => {}
Err(std::sync::mpsc::TrySendError::Full(_)) => {
if audio_count.is_multiple_of(100) {
log::warn!(target: "gatt", "[GATT] 音频帧丢弃(通道满): total={}", audio_count);
}
}
Err(std::sync::mpsc::TrySendError::Disconnected(_)) => {
log::warn!(target: "gatt", "[GATT] 音频解码通道关闭,结束 BLE 会话触发重连");
break;
}
}
} else if notif.uuid == event_uuid {
handle_event(¬if.value, &mut state, &event_tx, &pending);
} else {
log::warn!(target: "gatt", "收到未知 UUID 通知: {:?}", notif.uuid);
}
}
None => {
log::info!(target: "gatt", "通知流结束(设备断开),共 {} 通知", notif_count);
break;
}
}
}
_ = tokio::time::sleep_until(last_notif + heartbeat) => {
match p.is_connected().await {
Ok(false) | Err(_) => {
log::info!(target: "gatt", "心跳:10s 无通知且设备已断开");
break;
}
Ok(true) => {
last_notif = tokio::time::Instant::now();
}
}
}
}
}
state.release_all(&event_tx);
pending.lock().unwrap().clear();
running.store(false, Ordering::SeqCst);
log::info!(target: "gatt", "通知监听已退出");
});
if let Ok(mut slot) = self.notify_handle.lock() {
*slot = Some(notify_handle);
}
}
pub async fn disconnect(&self) -> Result<()> {
self.running.store(false, Ordering::SeqCst);
let p = self.peripheral.lock().unwrap().take();
let c = self.chars.lock().unwrap().take();
self.pending_responses.lock().unwrap().clear();
if let (Some(p), Some(c)) = (p, c) {
let _ = tokio::time::timeout(std::time::Duration::from_secs(3), async {
let _ = p.unsubscribe(&c.event).await;
let _ = p.unsubscribe(&c.audio).await;
let _ = p.disconnect().await;
})
.await;
log::info!(target: "gatt", "disconnect 完成(含超时保护)");
}
let notify_handle = self.notify_handle.lock().unwrap().take();
if let Some(h) = notify_handle {
let _ = tokio::time::timeout(std::time::Duration::from_secs(3), h).await;
}
let audio_handle = self.audio_thread_handle.lock().unwrap().take();
if let Some(h) = audio_handle {
let _ = h.join();
}
Ok(())
}
#[allow(dead_code)]
pub fn is_connected(&self) -> bool {
let _p = self.peripheral.lock().unwrap();
self.running.load(Ordering::SeqCst)
}
pub fn running(&self) -> Arc<AtomicBool> {
self.running.clone()
}
}
async fn subscribe_with_ccc_fallback(
p: &Peripheral,
char: &btleplug::api::Characteristic,
label: &str,
) -> Result<()> {
log::info!(target: "gatt", "启用 {} 通知...", label);
match tokio::time::timeout(std::time::Duration::from_secs(3), p.subscribe(char)).await {
Ok(Ok(())) => {
log::info!(target: "gatt", "{} 订阅成功", label);
Ok(())
}
result => {
log::warn!(target: "gatt", "{} subscribe 失败 {:?},尝试手动写 CCC", label, result);
let ccc_desc = char
.descriptors
.iter()
.find(|d| d.uuid == CCC_DESCRIPTOR_UUID);
match ccc_desc {
Some(desc) => {
let ccc: [u8; 2] = [0x01, 0x00];
p.write_descriptor(desc, &ccc)
.await
.map_err(|e| anyhow::anyhow!("{} CCC 写入失败: {}", label, e))?;
log::info!(target: "gatt", "{} 手动 CCC 写入成功", label);
Ok(())
}
None => Err(anyhow::anyhow!(
"{} 特征值无 CCC 描述符 (0x2902),无法启用通知",
label
)),
}
}
}
}
fn handle_event(
data: &[u8],
state: &mut GattEventState,
event_tx: &broadcast::Sender<BoardEvent>,
pending: &Arc<Mutex<HashMap<u8, PendingResponse>>>,
) {
let Some((cmd, _len, payload)) = protocol::parse_packet(data) else {
log::warn!(target: "gatt", "[GATT] Event 解析失败: data_len={}", data.len());
let _ = event_tx.send(BoardEvent::Error(ErrorEvent {
message: format!("GATT Event 解析失败(len={})", data.len()),
recoverable: true,
}));
return;
};
{
let mut pending_guard = pending.lock().unwrap();
if let Some(pending_resp) = pending_guard.remove(&cmd) {
let mut response = vec![cmd, _len];
response.extend_from_slice(payload);
let _ = pending_resp.tx.send(response); return;
}
}
match cmd {
#[cfg(feature = "test-mode")]
CMD_AI_FACTORY_KEY_EVENT => {
if let Ok(event) = parse_factory_key_event_unscoped(data) {
let _ = event_tx.send(BoardEvent::FactoryKey(event));
}
}
0x0C => {
if payload.len() >= 2 {
let key_value = ((payload[1] as u16) << 8) | (payload[0] as u16);
let frame = state.consumer.on_frame(key_value, Instant::now());
let aggregator = state
.aggregator
.get_or_insert_with(|| KeyStateAggregator::new(event_tx.clone()));
for batch in frame.batches {
aggregator.report_change(KeySource::Gatt, batch, None);
}
if is_ai_voice_consumer_code(key_value) {
state.ai_voice_pressed = true;
let _ =
event_tx.send(BoardEvent::AiVoiceKey(AiVoiceKeyEvent { pressed: true }));
}
let dial = find_key_index_by_value(key_value)
.and_then(|index| key_index_to_mode(index).map(|mode| (index, mode)));
if let Some((key_index, (mode_value, mode_name))) = dial {
state.mode_key_pressed = key_index;
let _ = event_tx.send(BoardEvent::ModeChange(ModeChangeEvent {
mode: mode_name.to_string(),
mode_value,
source: ModeSource::Dial,
}));
}
if frame.cleared {
if state.ai_voice_pressed {
state.ai_voice_pressed = false;
let _ = event_tx
.send(BoardEvent::AiVoiceKey(AiVoiceKeyEvent { pressed: false }));
}
let prev_mode = state.mode_key_pressed;
state.mode_key_pressed = 0;
if prev_mode == 9 || prev_mode == 10 {
let _ = event_tx.send(BoardEvent::ModeChange(ModeChangeEvent {
mode: "CHAT".to_string(),
mode_value: 0,
source: ModeSource::Dial,
}));
}
}
}
}
CMD_AUDIO_DATA => {
log::debug!(target: "gatt", "[GATT] Event 通道收到 Audio 数据");
}
CMD_STATUS => {
if payload.len() >= 2 && payload[0] == CMD_WORK_MODE_DATA {
let mode_value = payload[1];
if let Some(mode) = WorkMode::from_u8(mode_value) {
let _ = event_tx.send(BoardEvent::ModeChange(ModeChangeEvent {
mode: mode.display_name().to_string(),
mode_value,
source: ModeSource::Dial,
}));
}
}
}
CMD_DEVICE_DISCONNECT => {
log::info!(target: "gatt", "[GATT] 收到设备主动断开通知 (CMD=0x60),当异常断开处理");
}
_ => {
log::debug!(target: "gatt", "[GATT] 未处理 Event CMD=0x{:02X}", cmd);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::kernel::protocol_hid::AI_VOICE_MEDIA_CODE;
fn drain(rx: &mut broadcast::Receiver<BoardEvent>) -> Vec<BoardEvent> {
let mut v = Vec::new();
while let Ok(e) = rx.try_recv() {
v.push(e);
}
v
}
fn key_frame(key_value: u16) -> Vec<u8> {
vec![
0x0C,
0x02,
(key_value & 0xFF) as u8,
((key_value >> 8) & 0xFF) as u8,
]
}
#[cfg(feature = "test-mode")]
#[test]
fn gatt_monitor_emits_factory_physical_event() {
let (tx, mut rx) = broadcast::channel(4);
let pending = Arc::new(Mutex::new(HashMap::new()));
let mut state = GattEventState::default();
let frame = [
CMD_AI_FACTORY_KEY_EVENT,
0x06,
0x01,
0x34,
0x12,
0x06,
0x01,
0x09,
];
handle_event(&frame, &mut state, &tx, &pending);
match rx.try_recv().expect("factory event") {
BoardEvent::FactoryKey(event) => {
assert_eq!(event.session, 0x1234);
assert_eq!(event.input_index, 6);
assert!(event.pressed);
}
other => panic!("unexpected event: {other:?}"),
}
}
fn key_events(rx: &mut broadcast::Receiver<BoardEvent>) -> Vec<(usize, bool)> {
drain(rx)
.into_iter()
.filter_map(|event| match event {
BoardEvent::KeyPress(key) => Some((key.key_index, key.pressed)),
_ => None,
})
.collect()
}
#[test]
fn key0_pulse_emits_both_press_and_release() {
let (tx, mut rx) = broadcast::channel(64);
let pending = Arc::new(Mutex::new(HashMap::new()));
let mut state = GattEventState::default();
handle_event(&key_frame(0x0F07), &mut state, &tx, &pending);
assert_eq!(
key_events(&mut rx),
vec![(0, true), (0, false)],
"KEY0 的按下与释放都必须发出——释放丢失正是当初修的 bug"
);
handle_event(&key_frame(0x0000), &mut state, &tx, &pending);
assert!(key_events(&mut rx).is_empty(), "脉冲收尾不该重复发释放");
}
#[test]
fn holding_tab_through_knob_turns_still_reports_the_release() {
let (tx, mut rx) = broadcast::channel(64);
let pending = Arc::new(Mutex::new(HashMap::new()));
let mut state = GattEventState::default();
handle_event(&key_frame(0x0F01), &mut state, &tx, &pending);
assert_eq!(key_events(&mut rx), vec![(3, true)]);
for _ in 0..2 {
handle_event(&key_frame(0x0F08), &mut state, &tx, &pending);
assert_eq!(
key_events(&mut rx),
vec![(1, true), (1, false)],
"旋钮该敲一下就走"
);
handle_event(&key_frame(0x0000), &mut state, &tx, &pending);
assert!(
key_events(&mut rx).is_empty(),
"脉冲收尾不能把按住的 Tab 判成松开"
);
}
handle_event(&key_frame(0x0000), &mut state, &tx, &pending);
assert_eq!(
key_events(&mut rx),
vec![(3, false)],
"松手后必须发出 Tab 的释放——缺了它,上层按住的修饰键永远放不掉"
);
}
#[test]
fn holding_voice_key_through_a_knob_turn_does_not_report_release() {
let (tx, mut rx) = broadcast::channel(64);
let pending = Arc::new(Mutex::new(HashMap::new()));
let mut state = GattEventState::default();
handle_event(&key_frame(AI_VOICE_MEDIA_CODE), &mut state, &tx, &pending);
let _ = drain(&mut rx);
handle_event(&key_frame(0x0F08), &mut state, &tx, &pending);
handle_event(&key_frame(0x0000), &mut state, &tx, &pending);
let voice_events: Vec<bool> = drain(&mut rx)
.into_iter()
.filter_map(|event| match event {
BoardEvent::AiVoiceKey(voice) => Some(voice.pressed),
_ => None,
})
.collect();
assert!(
voice_events.is_empty(),
"转旋钮期间不该报语音键释放:{voice_events:?}"
);
assert!(state.ai_voice_pressed, "语音键仍应处于按住状态");
handle_event(&key_frame(0x0000), &mut state, &tx, &pending);
let released = drain(&mut rx)
.into_iter()
.any(|event| matches!(event, BoardEvent::AiVoiceKey(voice) if !voice.pressed));
assert!(released, "真松手必须报语音键释放");
}
#[test]
fn disconnect_releases_everything_still_held() {
let (tx, mut rx) = broadcast::channel(64);
let pending = Arc::new(Mutex::new(HashMap::new()));
let mut state = GattEventState::default();
handle_event(&key_frame(AI_VOICE_MEDIA_CODE), &mut state, &tx, &pending);
handle_event(&key_frame(0x0F01), &mut state, &tx, &pending);
let _ = drain(&mut rx);
state.release_all(&tx);
let events = drain(&mut rx);
let released: Vec<usize> = events
.iter()
.filter_map(|event| match event {
BoardEvent::KeyPress(key) if !key.pressed => Some(key.key_index),
_ => None,
})
.collect();
assert!(
released.contains(&6) && released.contains(&3),
"断连必须补发所有按住键的松开,实际:{released:?}"
);
assert!(
events
.iter()
.any(|event| matches!(event, BoardEvent::AiVoiceKey(voice) if !voice.pressed)),
"断连必须补发语音键释放,否则录音停不下来"
);
assert!(!state.ai_voice_pressed);
}
#[test]
fn ccc_descriptor_uuid_matches_bluetooth_spec() {
assert_eq!(
CCC_DESCRIPTOR_UUID.to_string(),
"00002902-0000-1000-8000-00805f9b34fb"
);
}
}