use num_enum::TryFromPrimitive;
use openlogi_hidpp_derive::Feature;
use crate::{feature::FeatureEndpoint, protocol::v20::Hidpp20Error};
bitflags::bitflags! {
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct HostsInfoCapabilities: u8 {
const GET_NAME = 1 << 0;
const SET_NAME = 1 << 1;
const MOVE_HOST = 1 << 2;
const DELETE_HOST = 1 << 3;
const SET_OS_VERSION = 1 << 4;
}
}
bitflags::bitflags! {
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct HostDescriptorCapabilities: u8 {
const EQUAD = 1 << 0;
const USB = 1 << 1;
const BT = 1 << 2;
const BLE = 1 << 3;
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum HostIndex {
Current,
Slot(u8),
}
impl From<HostIndex> for u8 {
fn from(value: HostIndex) -> Self {
match value {
HostIndex::Current => 0xff,
HostIndex::Slot(index) => index,
}
}
}
impl From<u8> for HostIndex {
fn from(value: u8) -> Self {
if value == 0xff {
Self::Current
} else {
Self::Slot(value)
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, TryFromPrimitive)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
#[repr(u8)]
pub enum HostSlotStatus {
Empty = 0,
Paired = 1,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, TryFromPrimitive)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
#[repr(u8)]
pub enum HostBusType {
Undefined = 0,
Equad = 1,
Usb = 2,
Bt = 3,
Ble = 4,
BlePro = 5,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub struct HostsInfoFeatureInfo {
pub capabilities: HostsInfoCapabilities,
pub descriptor_capabilities: HostDescriptorCapabilities,
pub host_count: u8,
pub current_host: HostIndex,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub struct HostInfo {
pub host_index: HostIndex,
pub status: HostSlotStatus,
pub bus_type: HostBusType,
pub page_count: u8,
pub name_len: u8,
pub name_max_len: u8,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub struct HostDescriptorPage {
pub host_index: HostIndex,
pub bus_type: HostBusType,
pub page_index: u8,
pub body: [u8; 14],
}
pub const HOST_NAME_CHUNK_LEN: usize = 14;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub struct HostNameChunk {
pub host_index: HostIndex,
pub offset: u8,
pub bytes: [u8; HOST_NAME_CHUNK_LEN],
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, TryFromPrimitive)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
#[repr(u8)]
pub enum HostOsType {
Unknown = 0,
Windows = 1,
WindowsEmbedded = 2,
Linux = 3,
Chrome = 4,
Android = 5,
MacOs = 6,
Ios = 7,
WebOs = 8,
Tizen = 9,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub struct HostOsVersion {
pub host_index: HostIndex,
pub os_type: HostOsType,
pub major: u8,
pub minor: u16,
pub patch: u16,
}
#[derive(Clone, Feature)]
#[creatable(id = 0x1815, version = 2)]
pub struct HostsInfoFeature {
endpoint: FeatureEndpoint,
}
impl HostsInfoFeature {
pub async fn get_feature_info(&self) -> Result<HostsInfoFeatureInfo, Hidpp20Error> {
let payload = self.endpoint.call(0, [0; 3]).await?.extend_payload();
Ok(HostsInfoFeatureInfo {
capabilities: HostsInfoCapabilities::from_bits_retain(payload[0]),
descriptor_capabilities: HostDescriptorCapabilities::from_bits_retain(payload[1]),
host_count: payload[2],
current_host: HostIndex::from(payload[3]),
})
}
pub async fn get_host_info(&self, host: HostIndex) -> Result<HostInfo, Hidpp20Error> {
let payload = self
.endpoint
.call(1, [u8::from(host), 0, 0])
.await?
.extend_payload();
Ok(HostInfo {
host_index: HostIndex::from(payload[0]),
status: HostSlotStatus::try_from(payload[1])
.map_err(|_| Hidpp20Error::UnsupportedResponse)?,
bus_type: HostBusType::try_from(payload[2])
.map_err(|_| Hidpp20Error::UnsupportedResponse)?,
page_count: payload[3],
name_len: payload[4],
name_max_len: payload[5],
})
}
pub async fn get_host_descriptor(
&self,
host: HostIndex,
page: u8,
) -> Result<HostDescriptorPage, Hidpp20Error> {
let payload = self
.endpoint
.call(2, [u8::from(host), page, 0])
.await?
.extend_payload();
let mut body = [0; 14];
body.copy_from_slice(&payload[2..16]);
Ok(HostDescriptorPage {
host_index: HostIndex::from(payload[0]),
bus_type: HostBusType::try_from(payload[1] >> 4)
.map_err(|_| Hidpp20Error::UnsupportedResponse)?,
page_index: payload[1] & 0x0f,
body,
})
}
pub async fn get_host_friendly_name(
&self,
host: HostIndex,
offset: u8,
) -> Result<HostNameChunk, Hidpp20Error> {
let payload = self
.endpoint
.call(3, [u8::from(host), offset, 0])
.await?
.extend_payload();
let mut bytes = [0; HOST_NAME_CHUNK_LEN];
bytes.copy_from_slice(&payload[2..2 + HOST_NAME_CHUNK_LEN]);
Ok(HostNameChunk {
host_index: HostIndex::from(payload[0]),
offset: payload[1],
bytes,
})
}
pub async fn set_host_friendly_name(
&self,
host: HostIndex,
offset: u8,
bytes: [u8; HOST_NAME_CHUNK_LEN],
) -> Result<(), Hidpp20Error> {
let mut args = [0; 16];
args[0] = u8::from(host);
args[1] = offset;
args[2..].copy_from_slice(&bytes);
self.endpoint.call_long(4, args).await?;
Ok(())
}
pub async fn get_host_os_version(
&self,
host: HostIndex,
) -> Result<HostOsVersion, Hidpp20Error> {
let payload = self
.endpoint
.call(7, [u8::from(host), 0, 0])
.await?
.extend_payload();
Ok(HostOsVersion {
host_index: HostIndex::from(payload[0]),
os_type: HostOsType::try_from(payload[1])
.map_err(|_| Hidpp20Error::UnsupportedResponse)?,
major: payload[2],
minor: u16::from_be_bytes([payload[3], payload[4]]),
patch: u16::from_be_bytes([payload[5], payload[6]]),
})
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::*;
use crate::channel::mock::{MockRawHidChannel, channel_with_reader};
use crate::feature::CreatableFeature;
use crate::nibble::U4;
use crate::protocol::v20::{Message, MessageHeader};
fn answered<T, F>(
function: u8,
reply: Message,
call: impl FnOnce(HostsInfoFeature) -> F,
) -> (Result<T, Hidpp20Error>, Vec<u8>)
where
F: std::future::Future<Output = Result<T, Hidpp20Error>>,
{
futures::executor::block_on(async {
let (raw, handle) = MockRawHidChannel::new();
let channel = Arc::new(channel_with_reader(raw).await);
let feature = HostsInfoFeature::new(channel, 3, 0x0c);
let mut pending = Box::pin(call(feature));
assert!(futures::poll!(pending.as_mut()).is_pending());
handle.send_incoming(reply.into()).await;
let result = pending.await;
let written = handle.written_reports();
let request = written.last().expect("a request was sent");
assert_eq!(
request[3] >> 4,
function,
"the request names function {function}"
);
(result, request[4..].to_vec())
})
}
fn header(function: u8) -> MessageHeader {
MessageHeader {
device_index: 3,
feature_index: 0x0c,
function_id: U4::from_lo(function),
software_id: U4::from_lo(1),
}
}
#[test]
fn a_name_chunk_skips_the_echoed_host_and_offset() {
let mut reply = [0; 16];
reply[..2].copy_from_slice(&[1, 14]);
reply[2..].copy_from_slice(b"Studio MacBook");
let (chunk, request) = answered(3, Message::Long(header(3), reply), |feature| async move {
feature.get_host_friendly_name(HostIndex::Slot(1), 14).await
});
let chunk = chunk.expect("the device answered");
assert_eq!(&request[..3], &[1, 14, 0], "slot then byte offset");
assert_eq!(chunk.host_index, HostIndex::Slot(1));
assert_eq!(chunk.offset, 14);
assert_eq!(&chunk.bytes, b"Studio MacBook");
}
#[test]
fn a_name_write_is_one_long_report_of_slot_offset_and_fourteen_bytes() {
let (result, request) =
answered(4, Message::Long(header(4), [0; 16]), |feature| async move {
feature
.set_host_friendly_name(HostIndex::Slot(2), 28, *b"ab\0\0\0\0\0\0\0\0\0\0\0\0")
.await
});
result.expect("the device acknowledged");
assert_eq!(request.len(), 16, "a long report");
assert_eq!(&request[..4], &[2, 28, b'a', b'b']);
assert!(request[4..].iter().all(|&byte| byte == 0));
}
#[test]
fn an_os_version_is_type_major_then_big_endian_minor_and_patch() {
let mut reply = [0; 16];
reply[..7].copy_from_slice(&[0, 6, 15, 0x01, 0x02, 0x03, 0x04]);
let (os, request) = answered(7, Message::Long(header(7), reply), |feature| async move {
feature.get_host_os_version(HostIndex::Slot(0)).await
});
let os = os.expect("the device answered");
assert_eq!(&request[..3], &[0, 0, 0]);
assert_eq!(os.os_type, HostOsType::MacOs);
assert_eq!((os.major, os.minor, os.patch), (15, 0x0102, 0x0304));
}
#[test]
fn an_unknown_os_type_is_an_unsupported_response() {
let mut reply = [0; 16];
reply[1] = 10;
let (os, _) = answered(7, Message::Long(header(7), reply), |feature| async move {
feature.get_host_os_version(HostIndex::Slot(0)).await
});
assert!(matches!(os, Err(Hidpp20Error::UnsupportedResponse)));
}
}