use serde::{Deserialize, Serialize};
#[cfg(feature = "test-mode")]
use crate::kernel::error::{BoardError, Result};
#[cfg(feature = "test-mode")]
use crate::kernel::event::FactoryKeyEvent;
pub const VID: u16 = 0x363C;
pub const PID_USB: u16 = 0xED20;
pub const PID_BLE: u16 = 0xED20;
pub const PID: u16 = PID_USB;
pub fn is_target_pid(pid: u16) -> bool {
pid == PID_USB || pid == PID_BLE
}
#[allow(dead_code)]
pub fn target_pid(conn_type: crate::kernel::types::ConnectionType) -> u16 {
match conn_type {
crate::kernel::types::ConnectionType::Usb => PID_USB,
crate::kernel::types::ConnectionType::Ble => PID_BLE,
}
}
pub const REPORT_ID_INPUT: u8 = 0x0A;
pub const REPORT_ID_OUTPUT: u8 = 0x0B;
pub const REPORT_ID_KEY_EVENT: u8 = 0x02;
#[allow(dead_code)]
pub const REPORT_ID_AUDIO: u8 = 0xB1;
pub const CMD_GET_KEY_SETTING: u8 = 0x15;
pub const CMD_SET_KEY_SETTING: u8 = 0x16;
pub const CMD_STATUS: u8 = 0x12;
pub const CMD_GET_DEVICE_INFO: u8 = 0x13;
pub const CMD_AUDIO_DATA: u8 = 0x01;
pub const CMD_WORK_MODE_DATA: u8 = 0xC9;
#[cfg(feature = "test-mode")]
pub const CMD_AI_SHUTDOWN: u8 = 0x5E;
pub const CMD_DEVICE_DISCONNECT: u8 = 0x60;
pub const CMD_GET_SILENT_RECORD: u8 = 0x61;
pub const CMD_SET_SILENT_RECORD: u8 = 0x62;
pub const CMD_GET_SLEEP_TIMEOUT: u8 = 0x63;
pub const CMD_SET_SLEEP_TIMEOUT: u8 = 0x64;
pub const CMD_AI_APP_ONLINE_NOTIFY: u8 = 0x65;
pub const CMD_AI_GET_APP_ONLINE: u8 = 0x66;
pub const CMD_AI_GET_OPEN_URL: u8 = 0x67;
pub const CMD_AI_SET_OPEN_URL: u8 = 0x68;
pub const CMD_AI_READ_BINDINGS_BLOB: u8 = 0x69;
pub const CMD_AI_WRITE_BINDINGS_BLOB: u8 = 0x6A;
#[cfg(feature = "test-mode")]
pub const CMD_AI_FACTORY_KEY_TEST_CONTROL: u8 = 0x6C;
#[cfg(feature = "test-mode")]
pub const CMD_AI_FACTORY_KEY_EVENT: u8 = 0x6D;
#[cfg(feature = "test-mode")]
pub const FACTORY_KEY_TEST_PROTOCOL_VERSION: u8 = 0x01;
#[cfg(feature = "test-mode")]
pub const FACTORY_KEY_TEST_INPUT_COUNT: u8 = 12;
#[cfg(feature = "test-mode")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum FactoryKeyControlResult {
Ok = 0,
UnsupportedVersion = 1,
Busy = 2,
SessionMismatch = 3,
InvalidRequest = 4,
}
#[cfg(feature = "test-mode")]
impl TryFrom<u8> for FactoryKeyControlResult {
type Error = BoardError;
fn try_from(value: u8) -> Result<Self> {
match value {
0 => Ok(Self::Ok),
1 => Ok(Self::UnsupportedVersion),
2 => Ok(Self::Busy),
3 => Ok(Self::SessionMismatch),
4 => Ok(Self::InvalidRequest),
_ => Err(BoardError::Protocol(format!(
"未知工厂按键测试结果: {value}"
))),
}
}
}
#[cfg(feature = "test-mode")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct FactoryKeyControlAck {
pub result: FactoryKeyControlResult,
pub enabled: bool,
pub session: u16,
}
pub const AI_VOICE_MEDIA_CODE: u16 = 0x0F04;
pub const AI_VOICE_MARK_CODE: u16 = 0x0F99;
pub fn is_ai_voice_consumer_code(code: u16) -> bool {
matches!(code, AI_VOICE_MEDIA_CODE | AI_VOICE_MARK_CODE)
}
pub const PACKET_SIZE: usize = 64;
pub const KEY_DATA_LEN: usize = 60;
pub const KEY_COUNT: usize = 20;
pub const ACTIVE_KEY_COUNT: usize = 12;
#[allow(dead_code)]
pub const SAMPLE_RATE: u32 = 16000;
pub const MSBC_FRAME_SIZE: usize = 57;
#[allow(dead_code)]
pub const DECODED_SAMPLES_PER_FRAME: usize = 120;
#[allow(dead_code)]
pub const MSBC_FLAG_OFFSET: usize = 1;
#[allow(dead_code)]
pub const MSBC_LEN_OFFSET: usize = 2;
pub const MSBC_DATA_OFFSET: usize = 3;
#[allow(dead_code)]
pub const MSBC_DATA_LEN: usize = 57;
#[allow(dead_code)]
pub const USAGE_PAGE_KEYBOARD: u16 = 0x0001;
pub const USAGE_PAGE_CONSUMER: u16 = 0x000C;
pub const USAGE_PAGE_CONFIG: u16 = 0xFFA0;
#[allow(dead_code)]
pub const USAGE_PAGE_AUDIO: u16 = 0xFFAA;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum KeyClass {
Default = 0x00,
MouseKey = 0x01,
MouseDpi = 0x02,
MouseTilt = 0x03,
MouseFireKey = 0x04,
MouseShortcutKey = 0x05,
Macro = 0x06,
SwitchReportRate = 0x07,
SwitchProfile = 0x08,
Wheel = 0x09,
Media = 0x0A,
Keyboard = 0x0B,
LockX = 0x0D,
AiVoice = 0x0E,
LockY = 0x0C,
Disable = 0xFF,
}
impl KeyClass {
pub fn from_u8(value: u8) -> Option<Self> {
match value {
0x00 => Some(Self::Default),
0x01 => Some(Self::MouseKey),
0x02 => Some(Self::MouseDpi),
0x03 => Some(Self::MouseTilt),
0x04 => Some(Self::MouseFireKey),
0x05 => Some(Self::MouseShortcutKey),
0x06 => Some(Self::Macro),
0x07 => Some(Self::SwitchReportRate),
0x08 => Some(Self::SwitchProfile),
0x09 => Some(Self::Wheel),
0x0A => Some(Self::Media),
0x0B => Some(Self::Keyboard),
0x0D => Some(Self::LockX),
0x0E => Some(Self::AiVoice), 0x0C => Some(Self::LockY), 0xFF => Some(Self::Disable),
_ => None,
}
}
pub fn display_name(&self) -> &'static str {
match self {
Self::Default => "默认",
Self::MouseKey => "鼠标按键",
Self::MouseDpi => "DPI切换",
Self::MouseTilt => "鼠标滚轮倾斜",
Self::MouseFireKey => "火力键",
Self::MouseShortcutKey => "快捷键",
Self::Macro => "宏",
Self::SwitchReportRate => "报告率切换",
Self::SwitchProfile => "配置切换",
Self::Wheel => "滚轮",
Self::Media => "多媒体键",
Self::Keyboard => "键盘按键",
Self::LockX => "锁定X轴",
Self::LockY => "锁定Y轴",
Self::AiVoice => "AI语音",
Self::Disable => "禁用",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct KeyInfo {
pub key_class: u8,
pub key_value_l: u8,
pub key_value_h: u8,
}
impl KeyInfo {
pub fn new(key_class: u8, key_value_l: u8, key_value_h: u8) -> Self {
Self {
key_class,
key_value_l,
key_value_h,
}
}
pub fn disabled() -> Self {
Self {
key_class: KeyClass::Disable as u8,
key_value_l: 0x00,
key_value_h: 0x00,
}
}
pub fn key_value(&self) -> u16 {
((self.key_value_h as u16) << 8) | (self.key_value_l as u16)
}
#[allow(dead_code)]
pub fn set_key_value(&mut self, value: u16) {
self.key_value_l = (value & 0xFF) as u8;
self.key_value_h = ((value >> 8) & 0xFF) as u8;
}
pub fn from_bytes(data: [u8; 3]) -> Self {
Self {
key_class: data[0],
key_value_l: data[1],
key_value_h: data[2],
}
}
pub fn to_bytes(self) -> [u8; 3] {
[self.key_class, self.key_value_l, self.key_value_h]
}
#[allow(dead_code)]
pub fn get_class(&self) -> Option<KeyClass> {
KeyClass::from_u8(self.key_class)
}
}
pub fn get_key_name(index: usize) -> &'static str {
match index {
0 => "音量A相(KEY0)",
1 => "音量B相(KEY1)",
2 => "音量按压(KEY2)",
3 => "Tab键(KEY3)",
4 => "New键(KEY4)",
5 => "Esc键(KEY5)",
6 => "AI语音(KEY6)",
7 => "Action键(KEY7)",
8 => "Enter键(KEY8)",
9 => "YOLO拨杆(KEY9)",
10 => "PLAN拨杆(KEY10)",
11 => "CHAT拨杆(KEY11)",
_ => "未知",
}
}
pub fn get_default_key_value(index: usize) -> u16 {
match index {
0 => 0x0F07, 1 => 0x0F08, 2 => 0x0F09, 3 => 0x0F01, 4 => 0x0F02, 5 => 0x0F03, 6 => 0x0F04, 7 => 0x0F05, 8 => 0x0F06, 9 => 0x0F0A, 10 => 0x0F0B, 11 => 0x0F0C, _ => 0x0000,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum WorkMode {
Chat = 0,
Yolo = 1,
Plan = 2,
}
impl WorkMode {
pub fn from_u8(value: u8) -> Option<Self> {
match value {
0x00 | 0x0C => Some(Self::Chat),
0x01 | 0x0A => Some(Self::Yolo),
0x02 | 0x0B | 0x0F => Some(Self::Plan),
_ => None,
}
}
pub fn display_name(&self) -> &'static str {
match self {
Self::Chat => "CHAT",
Self::Yolo => "YOLO",
Self::Plan => "PLAN",
}
}
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct HidPacket {
pub report_id: u8,
pub cmd: u8,
pub len: u8,
pub data: [u8; 61],
}
impl HidPacket {
#[allow(dead_code)]
pub fn new(report_id: u8, cmd: u8, len: u8, data: [u8; 61]) -> Self {
Self {
report_id,
cmd,
len,
data,
}
}
pub fn get_key_config() -> [u8; PACKET_SIZE] {
let mut packet = [0u8; PACKET_SIZE];
packet[0] = REPORT_ID_OUTPUT;
packet[1] = CMD_GET_KEY_SETTING;
packet[2] = 0x00;
packet
}
pub fn get_device_info() -> [u8; PACKET_SIZE] {
let mut packet = [0u8; PACKET_SIZE];
packet[0] = REPORT_ID_OUTPUT;
packet[1] = CMD_GET_DEVICE_INFO;
packet[2] = 0x02;
packet
}
pub fn set_key_config(key_data: &[u8; KEY_DATA_LEN]) -> [u8; PACKET_SIZE] {
let mut packet = [0u8; PACKET_SIZE];
packet[0] = REPORT_ID_OUTPUT;
packet[1] = CMD_SET_KEY_SETTING;
packet[2] = KEY_DATA_LEN as u8;
packet[3..63].copy_from_slice(key_data);
packet
}
pub fn get_silent_record() -> [u8; PACKET_SIZE] {
let mut packet = [0u8; PACKET_SIZE];
packet[0] = REPORT_ID_OUTPUT;
packet[1] = CMD_GET_SILENT_RECORD;
packet
}
pub fn get_work_mode() -> [u8; PACKET_SIZE] {
let mut packet = [0u8; PACKET_SIZE];
packet[0] = REPORT_ID_OUTPUT;
packet[1] = CMD_STATUS;
packet[2] = 0x04; packet[3] = CMD_WORK_MODE_DATA;
packet
}
pub fn set_silent_record(enable: bool) -> [u8; PACKET_SIZE] {
let mut packet = [0u8; PACKET_SIZE];
packet[0] = REPORT_ID_OUTPUT;
packet[1] = CMD_SET_SILENT_RECORD;
packet[2] = 0x01;
packet[3] = u8::from(enable);
packet
}
pub fn get_sleep_timeout() -> [u8; PACKET_SIZE] {
let mut packet = [0u8; PACKET_SIZE];
packet[0] = REPORT_ID_OUTPUT;
packet[1] = CMD_GET_SLEEP_TIMEOUT;
packet
}
pub fn set_sleep_timeout(timeout: crate::kernel::types::SleepTimeout) -> [u8; PACKET_SIZE] {
let mut packet = [0u8; PACKET_SIZE];
packet[0] = REPORT_ID_OUTPUT;
packet[1] = CMD_SET_SLEEP_TIMEOUT;
packet[2] = 0x04; packet[3..5].copy_from_slice(&timeout.disconnected.to_le_bytes());
packet[5..7].copy_from_slice(&timeout.connected.to_le_bytes());
packet
}
pub fn app_online_notify(online: bool) -> [u8; PACKET_SIZE] {
let mut packet = [0u8; PACKET_SIZE];
packet[0] = REPORT_ID_OUTPUT;
packet[1] = CMD_AI_APP_ONLINE_NOTIFY;
packet[2] = 0x01;
packet[3] = u8::from(online);
packet
}
pub fn get_app_online() -> [u8; PACKET_SIZE] {
let mut packet = [0u8; PACKET_SIZE];
packet[0] = REPORT_ID_OUTPUT;
packet[1] = CMD_AI_GET_APP_ONLINE;
packet
}
pub fn get_open_url() -> [u8; PACKET_SIZE] {
let mut packet = [0u8; PACKET_SIZE];
packet[0] = REPORT_ID_OUTPUT;
packet[1] = CMD_AI_GET_OPEN_URL;
packet
}
pub fn set_open_url(url: &str) -> [u8; PACKET_SIZE] {
let mut packet = [0u8; PACKET_SIZE];
packet[0] = REPORT_ID_OUTPUT;
packet[1] = CMD_AI_SET_OPEN_URL;
packet[2] = 0x40; let bytes = url.as_bytes();
let copy_len = bytes.len().min(63); packet[3..3 + copy_len].copy_from_slice(&bytes[..copy_len]);
packet
}
#[cfg(feature = "test-mode")]
pub fn factory_key_test_control(enable: bool, session: u16) -> Result<[u8; PACKET_SIZE]> {
if session == 0 {
return Err(BoardError::Protocol(
"工厂按键测试 session 不能为 0".to_string(),
));
}
let mut packet = [0u8; PACKET_SIZE];
packet[0] = REPORT_ID_OUTPUT;
packet[1] = CMD_AI_FACTORY_KEY_TEST_CONTROL;
packet[2] = 0x04;
packet[3] = u8::from(enable);
packet[4] = FACTORY_KEY_TEST_PROTOCOL_VERSION;
packet[5..7].copy_from_slice(&session.to_le_bytes());
Ok(packet)
}
#[cfg(feature = "test-mode")]
pub fn shutdown(keep_pair: bool) -> [u8; PACKET_SIZE] {
let mut packet = [0u8; PACKET_SIZE];
packet[0] = REPORT_ID_OUTPUT;
packet[1] = CMD_AI_SHUTDOWN;
packet[2] = 0x01; packet[3] = if keep_pair { 0x01 } else { 0x02 };
packet
}
#[allow(dead_code)]
pub fn from_bytes(data: &[u8]) -> Option<Self> {
if data.len() < 4 {
return None;
}
let mut packet_data = [0u8; 61];
if data.len() > 3 {
let copy_len = (data.len() - 3).min(61);
packet_data[..copy_len].copy_from_slice(&data[3..3 + copy_len]);
}
Some(Self {
report_id: data[0],
cmd: data[1],
len: data[2],
data: packet_data,
})
}
#[allow(dead_code)]
pub fn is_success(&self) -> bool {
self.data[0] == 0x00
}
}
#[cfg(feature = "test-mode")]
fn factory_packet_without_report_id(data: &[u8], expected_cmd: u8) -> Result<&[u8]> {
let packet = if data.first() == Some(&REPORT_ID_INPUT) {
data.get(1..)
.ok_or_else(|| BoardError::Protocol("工厂测试 HID 包长度不足".to_string()))?
} else {
data
};
if packet.first() != Some(&expected_cmd) {
return Err(BoardError::Protocol(format!(
"工厂测试 CMD 不匹配: expected=0x{expected_cmd:02X} actual=0x{:02X}",
packet.first().copied().unwrap_or_default()
)));
}
Ok(packet)
}
#[cfg(feature = "test-mode")]
pub fn parse_factory_key_control_ack(
data: &[u8],
expected_session: u16,
) -> Result<FactoryKeyControlAck> {
let packet = factory_packet_without_report_id(data, CMD_AI_FACTORY_KEY_TEST_CONTROL)?;
if packet.len() < 7 || packet[1] != 0x05 {
return Err(BoardError::Protocol(
"工厂测试控制 ACK 长度无效".to_string(),
));
}
if packet[3] != FACTORY_KEY_TEST_PROTOCOL_VERSION {
return Err(BoardError::Protocol(format!(
"工厂测试协议版本不匹配: {}",
packet[3]
)));
}
if packet[4] > 1 {
return Err(BoardError::Protocol(
"工厂测试 ACK enabled 无效".to_string(),
));
}
let session = u16::from_le_bytes([packet[5], packet[6]]);
if session != expected_session {
return Err(BoardError::Protocol(format!(
"工厂测试 ACK session 不匹配: expected={expected_session:#06X} actual={session:#06X}"
)));
}
Ok(FactoryKeyControlAck {
result: packet[2].try_into()?,
enabled: packet[4] != 0,
session,
})
}
#[cfg(feature = "test-mode")]
fn parse_factory_key_event_inner(
data: &[u8],
expected_session: Option<u16>,
) -> Result<FactoryKeyEvent> {
let packet = factory_packet_without_report_id(data, CMD_AI_FACTORY_KEY_EVENT)?;
if packet.len() < 8 || packet[1] != 0x06 {
return Err(BoardError::Protocol("工厂物理按键事件长度无效".to_string()));
}
if packet[2] != FACTORY_KEY_TEST_PROTOCOL_VERSION {
return Err(BoardError::Protocol(format!(
"工厂物理按键协议版本不匹配: {}",
packet[2]
)));
}
let session = u16::from_le_bytes([packet[3], packet[4]]);
if session == 0 || expected_session.is_some_and(|expected| expected != session) {
return Err(BoardError::Protocol(format!(
"工厂物理按键 session 无效: {session:#06X}"
)));
}
if packet[5] >= FACTORY_KEY_TEST_INPUT_COUNT {
return Err(BoardError::Protocol(format!(
"工厂物理按键索引越界: {}",
packet[5]
)));
}
if packet[6] > 1 {
return Err(BoardError::Protocol(
"工厂物理按键 pressed 无效".to_string(),
));
}
Ok(FactoryKeyEvent {
session,
input_index: packet[5],
pressed: packet[6] != 0,
sequence: packet[7],
})
}
#[cfg(feature = "test-mode")]
pub fn parse_factory_key_event(data: &[u8], expected_session: u16) -> Result<FactoryKeyEvent> {
parse_factory_key_event_inner(data, Some(expected_session))
}
#[cfg(feature = "test-mode")]
pub(crate) fn parse_factory_key_event_unscoped(data: &[u8]) -> Result<FactoryKeyEvent> {
parse_factory_key_event_inner(data, None)
}
pub fn read_bindings_blob_packet(offset: u16) -> [u8; PACKET_SIZE] {
let mut packet = [0u8; PACKET_SIZE];
packet[0] = REPORT_ID_OUTPUT;
packet[1] = CMD_AI_READ_BINDINGS_BLOB;
packet[2] = 0x02;
packet[3..5].copy_from_slice(&offset.to_le_bytes());
packet
}
pub fn write_bindings_blob_packet(offset: u16, chunk: &[u8]) -> [u8; PACKET_SIZE] {
assert!(chunk.len() <= 56, "blob 分片超过 56 字节");
let mut packet = [0u8; PACKET_SIZE];
packet[0] = REPORT_ID_OUTPUT;
packet[1] = CMD_AI_WRITE_BINDINGS_BLOB;
packet[2] = (2 + chunk.len()) as u8;
packet[3..5].copy_from_slice(&offset.to_le_bytes());
packet[5..5 + chunk.len()].copy_from_slice(chunk);
packet
}
pub fn commit_bindings_blob_packet(total_len: u16, crc16: u16) -> [u8; PACKET_SIZE] {
let mut packet = [0u8; PACKET_SIZE];
packet[0] = REPORT_ID_OUTPUT;
packet[1] = CMD_AI_WRITE_BINDINGS_BLOB;
packet[2] = 0x06;
packet[3..5].copy_from_slice(&0xFFFFu16.to_le_bytes());
packet[5..7].copy_from_slice(&total_len.to_le_bytes());
packet[7..9].copy_from_slice(&crc16.to_le_bytes());
packet
}
pub fn parse_silent_record_hid_response(response: &[u8], expected_cmd: u8) -> Option<bool> {
if response.len() < 5 || response[1] != expected_cmd || response[2] < 2 || response[3] != 0 {
return None;
}
Some(response[4] != 0)
}
pub fn parse_silent_record_gatt_response(response: &[u8], expected_cmd: u8) -> Option<bool> {
if response.len() < 4 || response[0] != expected_cmd || response[1] < 2 || response[2] != 0 {
return None;
}
Some(response[3] != 0)
}
pub fn parse_work_mode_hid_response(response: &[u8]) -> Option<WorkMode> {
if response.len() < 5 || response[1] != CMD_STATUS || response[3] != CMD_WORK_MODE_DATA {
return None;
}
WorkMode::from_u8(response[4])
}
pub fn parse_work_mode_gatt_response(response: &[u8]) -> Option<WorkMode> {
if response.len() < 4 || response[0] != CMD_STATUS || response[2] != CMD_WORK_MODE_DATA {
return None;
}
WorkMode::from_u8(response[3])
}
pub fn parse_sleep_timeout_hid_response(
response: &[u8],
expected_cmd: u8,
) -> Option<crate::kernel::types::SleepTimeout> {
if response.len() < 8 || response[1] != expected_cmd || response[2] < 5 || response[3] != 0 {
return None;
}
let disconnected = u16::from_le_bytes([response[4], response[5]]);
let connected = u16::from_le_bytes([response[6], response[7]]);
Some(crate::kernel::types::SleepTimeout::new(
disconnected,
connected,
))
}
pub fn parse_sleep_timeout_gatt_response(
response: &[u8],
expected_cmd: u8,
) -> Option<crate::kernel::types::SleepTimeout> {
if response.len() < 7 || response[0] != expected_cmd || response[1] < 5 || response[2] != 0 {
return None;
}
let disconnected = u16::from_le_bytes([response[3], response[4]]);
let connected = u16::from_le_bytes([response[5], response[6]]);
Some(crate::kernel::types::SleepTimeout::new(
disconnected,
connected,
))
}
pub fn parse_app_online_hid_response(response: &[u8], expected_cmd: u8) -> Option<bool> {
if response.len() < 5 || response[1] != expected_cmd || response[2] < 2 || response[3] != 0 {
return None;
}
Some(response[4] != 0)
}
pub fn parse_app_online_gatt_response(response: &[u8], expected_cmd: u8) -> Option<bool> {
if response.len() < 4 || response[0] != expected_cmd || response[1] < 2 || response[2] != 0 {
return None;
}
Some(response[3] != 0)
}
pub fn parse_open_url_hid_response(response: &[u8], expected_cmd: u8) -> Option<String> {
if response.len() < 5 || response[1] != expected_cmd || response[3] != 0 {
return None;
}
let url_bytes = &response[4..];
let end = url_bytes
.iter()
.position(|&b| b == 0)
.unwrap_or(url_bytes.len());
Some(String::from_utf8_lossy(&url_bytes[..end]).into_owned())
}
pub fn parse_open_url_gatt_response(response: &[u8], expected_cmd: u8) -> Option<String> {
if response.len() < 4 || response[0] != expected_cmd || response[2] != 0 {
return None;
}
let url_bytes = &response[3..];
let end = url_bytes
.iter()
.position(|&b| b == 0)
.unwrap_or(url_bytes.len());
Some(String::from_utf8_lossy(&url_bytes[..end]).into_owned())
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeyConfig {
pub keys: [KeyInfo; KEY_COUNT],
}
impl KeyConfig {
pub fn new() -> Self {
Self {
keys: [KeyInfo::new(0, 0, 0); KEY_COUNT],
}
}
pub fn from_bytes(data: &[u8; KEY_DATA_LEN]) -> Self {
let mut keys = [KeyInfo::new(0, 0, 0); KEY_COUNT];
for (i, key) in keys.iter_mut().enumerate() {
let offset = i * 3;
if offset + 2 < KEY_DATA_LEN {
*key = KeyInfo::from_bytes([data[offset], data[offset + 1], data[offset + 2]]);
}
}
Self { keys }
}
pub fn to_bytes(&self) -> [u8; KEY_DATA_LEN] {
let mut data = [0u8; KEY_DATA_LEN];
for i in 0..KEY_COUNT {
let offset = i * 3;
let bytes = self.keys[i].to_bytes();
data[offset] = bytes[0];
data[offset + 1] = bytes[1];
data[offset + 2] = bytes[2];
}
data
}
#[allow(dead_code)]
pub fn get_key(&self, index: usize) -> Option<&KeyInfo> {
self.keys.get(index)
}
pub fn set_key(&mut self, index: usize, key_info: KeyInfo) -> bool {
if index < KEY_COUNT {
self.keys[index] = key_info;
true
} else {
false
}
}
#[allow(dead_code)]
pub fn disable_key(&mut self, index: usize) -> bool {
self.set_key(index, KeyInfo::disabled())
}
pub fn clear_unused_keys(&mut self) {
for i in ACTIVE_KEY_COUNT..KEY_COUNT {
self.keys[i] = KeyInfo::new(0x00, 0x00, 0x00);
}
}
}
pub fn key_config_has_ai_voice(config: &KeyConfig) -> bool {
config.keys[..ACTIVE_KEY_COUNT].iter().any(|key| {
matches!(KeyClass::from_u8(key.key_class), Some(KeyClass::AiVoice))
|| matches!(KeyClass::from_u8(key.key_class), Some(KeyClass::Media))
&& is_ai_voice_consumer_code(key.key_value())
})
}
impl Default for KeyConfig {
fn default() -> Self {
let mut config = Self::new();
for i in 0..ACTIVE_KEY_COUNT {
let default_value = get_default_key_value(i);
config.keys[i] = KeyInfo::new(
KeyClass::Media as u8,
(default_value & 0xFF) as u8,
((default_value >> 8) & 0xFF) as u8,
);
}
for i in ACTIVE_KEY_COUNT..KEY_COUNT {
config.keys[i] = KeyInfo::new(0x00, 0x00, 0x00);
}
config
}
}
pub fn find_key_index_by_value(value: u16) -> Option<usize> {
match value {
0x0F01 => Some(3), 0x0F02 => Some(4), 0x0F03 => Some(5), 0x0F04 => Some(6), 0x0F05 => Some(7), 0x0F06 => Some(8), 0x0F07 => Some(0), 0x0F08 => Some(1), 0x0F09 => Some(2), 0x0F0A => Some(9), 0x0F0B => Some(10), 0x0F0C => Some(11), _ => None,
}
}
pub fn is_knob_pulse_key_index(key_index: usize) -> bool {
matches!(key_index, 0 | 1)
}
pub fn key_index_to_mode(key_index: usize) -> Option<(u8, &'static str)> {
match key_index {
9 => Some((1, "YOLO")),
10 => Some((2, "PLAN")),
11 => Some((0, "CHAT")),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_key_info() {
let key_info = KeyInfo::new(0x0A, 0x01, 0x0F);
assert_eq!(key_info.key_value(), 0x0F01);
assert_eq!(key_info.get_class(), Some(KeyClass::Media));
}
#[test]
fn test_key_config() {
let mut config = KeyConfig::default();
config.set_key(6, KeyInfo::new(KeyClass::Media as u8, 0x04, 0x0F));
assert_eq!(config.keys[6].key_class, KeyClass::Media as u8);
assert_eq!(config.keys[6].key_value(), AI_VOICE_MEDIA_CODE);
assert!(is_ai_voice_consumer_code(AI_VOICE_MEDIA_CODE));
assert!(is_ai_voice_consumer_code(AI_VOICE_MARK_CODE));
assert!(!is_ai_voice_consumer_code(0x0F05));
}
#[test]
fn test_key_config_has_ai_voice() {
let mut config = KeyConfig::default();
assert!(key_config_has_ai_voice(&config));
config.set_key(6, KeyInfo::new(KeyClass::Media as u8, 0x05, 0x0F));
assert!(!key_config_has_ai_voice(&config));
config.set_key(7, KeyInfo::new(KeyClass::AiVoice as u8, 0x00, 0x00));
assert!(key_config_has_ai_voice(&config));
}
#[test]
fn test_work_mode() {
assert_eq!(WorkMode::from_u8(0), Some(WorkMode::Chat));
assert_eq!(WorkMode::from_u8(1), Some(WorkMode::Yolo));
assert_eq!(WorkMode::from_u8(2), Some(WorkMode::Plan));
}
#[test]
fn test_silent_record_packets() {
let get = HidPacket::get_silent_record();
assert_eq!(&get[..3], &[REPORT_ID_OUTPUT, CMD_GET_SILENT_RECORD, 0]);
let enabled = HidPacket::set_silent_record(true);
assert_eq!(
&enabled[..4],
&[REPORT_ID_OUTPUT, CMD_SET_SILENT_RECORD, 1, 1]
);
let disabled = HidPacket::set_silent_record(false);
assert_eq!(disabled[3], 0);
}
#[test]
fn test_silent_record_response_parsing() {
assert_eq!(
parse_silent_record_hid_response(&[0x0A, 0x61, 2, 0, 1], 0x61),
Some(true)
);
assert_eq!(
parse_silent_record_gatt_response(&[0x62, 2, 0, 0], 0x62),
Some(false)
);
assert_eq!(
parse_silent_record_hid_response(&[0x0A, 0x61, 2, 0xFF, 1], 0x61),
None
);
assert_eq!(
parse_silent_record_gatt_response(&[0x61, 1, 0, 1], 0x61),
None
);
}
#[test]
fn test_work_mode_packets() {
let get = HidPacket::get_work_mode();
assert_eq!(
&get[..4],
&[REPORT_ID_OUTPUT, CMD_STATUS, 0x04, CMD_WORK_MODE_DATA]
);
assert_eq!(&get[4..], &[0u8; PACKET_SIZE - 4]);
}
#[test]
fn test_work_mode_response_parsing() {
assert_eq!(
parse_work_mode_hid_response(&[0x0A, 0x12, 0x02, 0xC9, 0x00]),
Some(WorkMode::Chat)
);
assert_eq!(
parse_work_mode_hid_response(&[0x0A, 0x12, 0x02, 0xC9, 0x01]),
Some(WorkMode::Yolo)
);
assert_eq!(
parse_work_mode_hid_response(&[0x0A, 0x12, 0x02, 0xC9, 0x02]),
Some(WorkMode::Plan)
);
assert_eq!(
parse_work_mode_gatt_response(&[0x12, 0x02, 0xC9, 0x00]),
Some(WorkMode::Chat)
);
assert_eq!(
parse_work_mode_gatt_response(&[0x12, 0x02, 0xC9, 0x01]),
Some(WorkMode::Yolo)
);
assert_eq!(
parse_work_mode_hid_response(&[0x0A, 0x61, 0x02, 0xC9, 0x00]),
None
);
assert_eq!(
parse_work_mode_hid_response(&[0x0A, 0x12, 0x02, 0xC8, 0x00]),
None
);
assert_eq!(
parse_work_mode_gatt_response(&[0x12, 0x02, 0xC8, 0x00]),
None
);
assert_eq!(
parse_work_mode_hid_response(&[0x0A, 0x12, 0x02, 0xC9]),
None
);
assert_eq!(parse_work_mode_gatt_response(&[0x12, 0x02, 0xC9]), None);
assert_eq!(
parse_work_mode_hid_response(&[0x0A, 0x12, 0x02, 0xC9, 0x05]),
None
);
}
#[test]
fn test_sleep_timeout_packets() {
use crate::kernel::types::SleepTimeout;
let get = HidPacket::get_sleep_timeout();
assert_eq!(&get[..3], &[REPORT_ID_OUTPUT, CMD_GET_SLEEP_TIMEOUT, 0]);
let set = HidPacket::set_sleep_timeout(SleepTimeout::new(60, 600));
assert_eq!(
&set[..7],
&[
REPORT_ID_OUTPUT,
CMD_SET_SLEEP_TIMEOUT,
4,
0x3C,
0x00,
0x58,
0x02
]
);
}
#[test]
fn test_sleep_timeout_response_parsing() {
use crate::kernel::types::SleepTimeout;
assert_eq!(
parse_sleep_timeout_hid_response(&[0x0A, 0x63, 5, 0, 0x3C, 0x00, 0x58, 0x02], 0x63),
Some(SleepTimeout::new(60, 600))
);
assert_eq!(
parse_sleep_timeout_gatt_response(&[0x64, 5, 0, 0x3C, 0x00, 0x58, 0x02], 0x64),
Some(SleepTimeout::new(60, 600))
);
assert_eq!(
parse_sleep_timeout_hid_response(&[0x0A, 0x63, 5, 0xFF, 0x3C, 0x00, 0x58, 0x02], 0x63),
None
);
assert_eq!(
parse_sleep_timeout_gatt_response(&[0x61, 5, 0, 0x3C, 0x00, 0x58, 0x02], 0x63),
None
);
assert_eq!(
parse_sleep_timeout_hid_response(&[0x0A, 0x63, 5, 0], 0x63),
None
);
}
}