From 011247c633e2a8f072ae3ff9a74c9126e0419b7c Mon Sep 17 00:00:00 2001 From: Billy Price Date: Tue, 15 Sep 2026 12:31:17 -0700 Subject: [PATCH 1/7] time-alarm: implement HID relay --- Cargo.lock | 1 + .../src/relay/{hid.rs => hid/mod.rs} | 4 + embedded-service/src/relay/hid/reports.rs | 480 +++++++++++++++ examples/rt685s-evk/Cargo.lock | 1 + examples/rt685s-evk/src/bin/time_alarm_hid.rs | 86 +++ .../bin/{time_alarm.rs => time_alarm_mctp.rs} | 3 +- time-alarm-service-relay/Cargo.toml | 1 + .../src/hid/SystemWakeTimerAndRtc.wara | 321 ++++++++++ time-alarm-service-relay/src/hid/mod.rs | 286 +++++++++ .../src/hid/serialization.rs | 554 ++++++++++++++++++ time-alarm-service-relay/src/lib.rs | 57 +- time-alarm-service-relay/src/mctp/mod.rs | 57 ++ .../src/{ => mctp}/serialization.rs | 0 13 files changed, 1795 insertions(+), 56 deletions(-) rename embedded-service/src/relay/{hid.rs => hid/mod.rs} (99%) create mode 100644 embedded-service/src/relay/hid/reports.rs create mode 100644 examples/rt685s-evk/src/bin/time_alarm_hid.rs rename examples/rt685s-evk/src/bin/{time_alarm.rs => time_alarm_mctp.rs} (95%) create mode 100644 time-alarm-service-relay/src/hid/SystemWakeTimerAndRtc.wara create mode 100644 time-alarm-service-relay/src/hid/mod.rs create mode 100644 time-alarm-service-relay/src/hid/serialization.rs create mode 100644 time-alarm-service-relay/src/mctp/mod.rs rename time-alarm-service-relay/src/{ => mctp}/serialization.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index f7d472c69..0feb65ea6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2045,6 +2045,7 @@ dependencies = [ "log", "num_enum", "time-alarm-service-interface", + "typenum", ] [[package]] diff --git a/embedded-service/src/relay/hid.rs b/embedded-service/src/relay/hid/mod.rs similarity index 99% rename from embedded-service/src/relay/hid.rs rename to embedded-service/src/relay/hid/mod.rs index ff3acae74..854cb6644 100644 --- a/embedded-service/src/relay/hid.rs +++ b/embedded-service/src/relay/hid/mod.rs @@ -3,6 +3,10 @@ use generic_array::ArrayLength; use num_enum::TryFromPrimitive; +pub mod reports; + +pub use crate::hid_report; + /// Errors that a HID device operation can fail with. /// /// Reporting failure triggers a device-initiated reset, so callers must handle these errors explicitly diff --git a/embedded-service/src/relay/hid/reports.rs b/embedded-service/src/relay/hid/reports.rs new file mode 100644 index 000000000..c5c2d1b5f --- /dev/null +++ b/embedded-service/src/relay/hid/reports.rs @@ -0,0 +1,480 @@ +//! Bit-level packing and unpacking helper for HID reports. +//! +//! HID lays report fields out as a little-endian bit stream: the first field starts at bit 0 of +//! byte 0, each subsequent field starts where the previous one ended, and fields straddle +//! byte boundaries. Signed fields are also supported, and accessing them requires doing sign extension. +//! This means that for reports that don't happen to be byte-aligned (e.g. boot protocol mouse/keyboard reports), +//! #[repr(C)] structs aren't sufficient to handle reports and you need to do some bit-level packing and unpacking manually. +//! +//! This module provides the following utilities for packing/unpacking reports that can't just be #[repr(C)] structs: +//! +//! * [`get_bits`] / [`set_bits`] for ad-hoc access to a bit range in a buffer. +//! * [`hid_report!`](crate::hid_report) to declare a report as a normal Rust struct with per-field +//! bit widths, generating `pack`/`pack_into`/`unpack`. +//! +//! Report IDs are *not* handled here; strip or prepend the ID byte in the caller. +//! +//! ```ignore +//! hid_report! { +//! /// Report ID 3 (Time and Date), output direction. +//! pub struct SetTimeReport { +//! /// 1900 - 9999 +//! pub year: u16 => 14, +//! /// 1 - 12 +//! pub month: u8 => 4, +//! /// 1 - 31 +//! pub day: u8 => 5, +//! /// 0 - 23 +//! pub hour: u8 => 5, +//! /// 0 - 59 +//! pub minute: u8 => 6, +//! /// 0 - 59 +//! pub second: u8 => 6, +//! /// 0 - 999 +//! pub millisecond: u16 => 10, +//! /// -720 - 840, minutes from UTC +//! pub time_zone: i16 => 11, +//! pub dst_observed: bool => 1, +//! pub dst_active: bool => 1, +//! pub reserved: u8 => 1, +//! } +//! } +//! +//! let report = SetTimeReport::unpack(payload)?; +//! let bytes = report.pack()?; +//! ``` + +/// Failure modes for packing and unpacking report fields. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ReportError { + /// The requested bit range extends past the end of the buffer. + OutOfBounds, + + /// The requested width is zero, wider than 32 bits, or wider than the field's storage type. + InvalidWidth, + + /// The value does not fit in the requested number of bits. + ValueOutOfRange, +} + +/// Mask with the low `width` bits set. +const fn mask(width: u32) -> u32 { + if width >= u32::BITS { + u32::MAX + } else { + (1u32 << width) - 1 + } +} + +/// Interprets the low `width` bits of `raw` as a two's-complement signed value. +const fn sign_extend(raw: u32, width: u32) -> i32 { + if width >= u32::BITS { + raw as i32 + } else { + let shift = u32::BITS - width; + ((raw << shift) as i32) >> shift + } +} + +/// Inclusive range representable by a two's-complement field of `width` bits. `width` must be 1..=32. +const fn signed_range(width: u32) -> (i32, i32) { + if width >= u32::BITS { + (i32::MIN, i32::MAX) + } else { + let magnitude = 1i32 << (width - 1); + (-magnitude, magnitude - 1) + } +} + +fn check_bounds(data_len: usize, offset: usize, width: u32) -> Result<(), ReportError> { + if width == 0 || width > u32::BITS { + return Err(ReportError::InvalidWidth); + } + + let end = offset.checked_add(width as usize).ok_or(ReportError::OutOfBounds)?; + if end.div_ceil(8) > data_len { + return Err(ReportError::OutOfBounds); + } + + Ok(()) +} + +/// Reads `width` bits starting at bit `offset` (LSB of byte 0 is bit 0), returned right-aligned. +pub fn get_bits(data: &[u8], offset: usize, width: u32) -> Result { + check_bounds(data.len(), offset, width)?; + + let mut value = 0u32; + for bit in 0..width as usize { + let pos = offset + bit; + let byte = *data.get(pos / 8).ok_or(ReportError::OutOfBounds)?; + if byte & (1u8 << (pos % 8)) != 0 { + value |= 1u32 << bit; + } + } + + Ok(value) +} + +/// Writes the low `width` bits of `value` starting at bit `offset`. Bits outside the range are left alone. +pub fn set_bits(data: &mut [u8], offset: usize, width: u32, value: u32) -> Result<(), ReportError> { + check_bounds(data.len(), offset, width)?; + + if value & !mask(width) != 0 { + return Err(ReportError::ValueOutOfRange); + } + + for bit in 0..width as usize { + let pos = offset + bit; + let byte = data.get_mut(pos / 8).ok_or(ReportError::OutOfBounds)?; + let bit_mask = 1u8 << (pos % 8); + if value & (1u32 << bit) != 0 { + *byte |= bit_mask; + } else { + *byte &= !bit_mask; + } + } + + Ok(()) +} + +/// A type that can be stored in a HID report bitfield. +pub trait ReportField: Copy + Sized { + /// Width of the type this field is stored in; a field may not be declared wider than this. + const STORAGE_BITS: u32; + + /// Converts the right-aligned raw bits of a `width`-bit field into this type. + fn from_bits(raw: u32, width: u32) -> Result; + + /// Converts this value into the right-aligned raw bits of a `width`-bit field. + fn to_bits(self, width: u32) -> Result; +} + +fn check_width(width: u32) -> Result<(), ReportError> { + if width == 0 || width > u32::BITS || width > T::STORAGE_BITS { + Err(ReportError::InvalidWidth) + } else { + Ok(()) + } +} + +macro_rules! impl_unsigned_field { + ($($ty:ty),* $(,)?) => { + $( + impl ReportField for $ty { + const STORAGE_BITS: u32 = <$ty>::BITS; + + fn from_bits(raw: u32, width: u32) -> Result { + check_width::(width)?; + Ok((raw & mask(width)) as $ty) + } + + fn to_bits(self, width: u32) -> Result { + check_width::(width)?; + let value = u32::from(self); + if value & !mask(width) != 0 { + return Err(ReportError::ValueOutOfRange); + } + Ok(value) + } + } + )* + }; +} + +macro_rules! impl_signed_field { + ($($ty:ty),* $(,)?) => { + $( + impl ReportField for $ty { + const STORAGE_BITS: u32 = <$ty>::BITS; + + fn from_bits(raw: u32, width: u32) -> Result { + check_width::(width)?; + Ok(sign_extend(raw, width) as $ty) + } + + fn to_bits(self, width: u32) -> Result { + check_width::(width)?; + let value = i32::from(self); + let (min, max) = signed_range(width); + if value < min || value > max { + return Err(ReportError::ValueOutOfRange); + } + Ok((value as u32) & mask(width)) + } + } + )* + }; +} + +impl_unsigned_field!(u8, u16, u32); +impl_signed_field!(i8, i16, i32); + +impl ReportField for bool { + const STORAGE_BITS: u32 = 1; + + fn from_bits(raw: u32, width: u32) -> Result { + check_width::(width)?; + Ok(raw & 1 != 0) + } + + fn to_bits(self, width: u32) -> Result { + check_width::(width)?; + Ok(u32::from(self)) + } +} + +/// Declares a HID report struct and generates its bit-level (de)serialization. +/// +/// Fields are laid out LSB-first in declaration order, matching the order of `Input`/`Output`/ +/// `Feature` items in the report descriptor. Every field of the descriptor must be declared, +/// including constant/padding items, so that the offsets line up. +/// +/// Field syntax is `name: storage_type => bit_width`. Supported storage types are `u8`, `u16`, +/// `u32`, `i8`, `i16`, `i32` (two's complement) and `bool` (exactly one bit). +/// +/// The declared widths must sum to a whole number of bytes, which is checked at compile time; +/// declare the report descriptor's constant/padding items to make up any shortfall. +/// +/// The Windows HID APIs don't support field sizes larger than 32 bits, so if you need larger values, +/// you have to split them into multiple fields. +#[macro_export] +macro_rules! hid_report { + ( + $(#[$meta:meta])* + $vis:vis struct $name:ident { + $( + $(#[$field_meta:meta])* + $field_vis:vis $field:ident : $ty:ty => $bits:expr + ),* $(,)? + } + ) => { + $(#[$meta])* + #[derive(Clone, Copy, Debug, Default, PartialEq)] + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + #[allow(dead_code)] + $vis struct $name { + $( + $(#[$field_meta])* + $field_vis $field: $ty, + )* + } + + #[allow(dead_code)] + impl $name { + /// Total width of the report in bits, including padding fields. + pub const BIT_LEN: usize = 0 $( + ($bits as usize) )*; + + /// Size of the packed report in bytes, excluding any report ID prefix. + pub const BYTE_LEN: usize = $name::BIT_LEN.div_ceil(8); + + /// Bit offset of each field, in declaration order. + #[allow(dead_code, clippy::indexing_slicing)] + pub const FIELD_OFFSETS: [usize; 0 $( + { let _ = $bits; 1 } )*] = { + let mut offsets = [0usize; 0 $( + { let _ = $bits; 1 } )*]; + let mut offset = 0usize; + let mut index = 0usize; + $( + offsets[index] = offset; + offset += ($bits as usize); + index += 1; + )* + let _ = (offset, index); + offsets + }; + + /// Packs the report into a new buffer. + pub fn pack(&self) -> Result<[u8; $name::BYTE_LEN], $crate::relay::hid::reports::ReportError> { + let mut buffer = [0u8; $name::BYTE_LEN]; + self.pack_into(&mut buffer)?; + Ok(buffer) + } + + /// Packs the report into the start of `buffer`, returning the number of bytes written. + /// + /// Bytes past the end of the report are left untouched. + pub fn pack_into(&self, buffer: &mut [u8]) -> Result { + #[allow(unused_mut, unused_variables)] + let mut offset: usize = 0; + $( + $crate::relay::hid::reports::set_bits( + buffer, + offset, + $bits, + $crate::relay::hid::reports::ReportField::to_bits(self.$field, $bits)?, + )?; + offset += ($bits as usize); + )* + let _ = offset; + Ok($name::BYTE_LEN) + } + + /// Unpacks the report from the start of `buffer`; trailing bytes are ignored. + pub fn unpack(buffer: &[u8]) -> Result { + #[allow(unused_mut, unused_variables)] + let mut offset: usize = 0; + $( + let $field = <$ty as $crate::relay::hid::reports::ReportField>::from_bits( + $crate::relay::hid::reports::get_bits(buffer, offset, $bits)?, + $bits, + )?; + offset += ($bits as usize); + )* + let _ = offset; + Ok(Self { $( $field, )* }) + } + } + + const _: () = assert!( + $name::BIT_LEN % 8 == 0, + concat!( + stringify!($name), + ": HID report field widths must sum to a whole number of bytes - declare the \ + descriptor's constant/padding items to make up the difference", + ), + ); + }; +} + +#[cfg(test)] +#[allow(clippy::unwrap_used, clippy::indexing_slicing)] +mod tests { + use super::*; + + crate::hid_report! { + pub struct TimeReport { + pub year: u16 => 14, + pub month: u8 => 4, + pub day: u8 => 5, + pub hour: u8 => 5, + pub minute: u8 => 6, + pub second: u8 => 6, + pub millisecond: u16 => 10, + pub time_zone: i16 => 11, + pub dst_observed: bool => 1, + pub dst_active: bool => 1, + pub reserved: u8 => 1, + } + } + + #[test] + fn report_geometry() { + assert_eq!(TimeReport::BIT_LEN, 64); + assert_eq!(TimeReport::BYTE_LEN, 8); + assert_eq!(TimeReport::FIELD_OFFSETS, [0, 14, 18, 23, 28, 34, 40, 50, 61, 62, 63]); + } + + #[test] + fn round_trip() { + let report = TimeReport { + year: 2026, + month: 9, + day: 1, + hour: 13, + minute: 45, + second: 59, + millisecond: 999, + time_zone: -480, + dst_observed: true, + dst_active: false, + reserved: 0, + }; + + let packed = report.pack().unwrap(); + assert_eq!(TimeReport::unpack(&packed).unwrap(), report); + } + + #[test] + fn unpack_ignores_trailing_bytes() { + let report = TimeReport { + year: 1900, + time_zone: 840, + ..Default::default() + }; + + let mut buffer = [0xAAu8; 12]; + assert_eq!(report.pack_into(&mut buffer).unwrap(), 8); + assert_eq!(TimeReport::unpack(&buffer).unwrap(), report); + assert_eq!(buffer[8..], [0xAA; 4]); + } + + #[test] + fn straddles_byte_boundaries() { + // year (14 bits) then month (4 bits) starting mid-byte. + let report = TimeReport { + year: 0x3FFF, + month: 0xF, + ..Default::default() + }; + + let packed = report.pack().unwrap(); + assert_eq!(packed[0], 0xFF); + assert_eq!(packed[1], 0xFF); + assert_eq!(packed[2], 0x03); + } + + #[test] + fn rejects_oversized_values() { + let report = TimeReport { + month: 16, + ..Default::default() + }; + assert_eq!(report.pack(), Err(ReportError::ValueOutOfRange)); + + let report = TimeReport { + time_zone: 1024, + ..Default::default() + }; + assert_eq!(report.pack(), Err(ReportError::ValueOutOfRange)); + } + + #[test] + fn rejects_short_buffers() { + let mut buffer = [0u8; 7]; + assert_eq!( + TimeReport::default().pack_into(&mut buffer), + Err(ReportError::OutOfBounds) + ); + assert_eq!(TimeReport::unpack(&buffer), Err(ReportError::OutOfBounds)); + } + + #[test] + fn bit_accessors() { + let data = [0b1010_0101u8, 0b0000_0011]; + assert_eq!(get_bits(&data, 0, 4).unwrap(), 0b0101); + assert_eq!(get_bits(&data, 4, 4).unwrap(), 0b1010); + assert_eq!(get_bits(&data, 6, 4).unwrap(), 0b1110); + assert_eq!(get_bits(&data, 0, 16).unwrap(), 0x03A5); + + let mut data = [0u8; 2]; + set_bits(&mut data, 6, 4, 0b1110).unwrap(); + assert_eq!(data, [0b1000_0000, 0b0000_0011]); + set_bits(&mut data, 6, 4, 0).unwrap(); + assert_eq!(data, [0, 0]); + } + + #[test] + fn bit_accessor_errors() { + let mut data = [0u8; 2]; + assert_eq!(get_bits(&data, 0, 0), Err(ReportError::InvalidWidth)); + assert_eq!(get_bits(&data, 0, 33), Err(ReportError::InvalidWidth)); + assert_eq!(get_bits(&data, 9, 8), Err(ReportError::OutOfBounds)); + assert_eq!(set_bits(&mut data, 0, 4, 0x10), Err(ReportError::ValueOutOfRange)); + } + + #[test] + fn signed_fields_sign_extend() { + assert_eq!(::from_bits(0b111_1010_0000, 11).unwrap(), -96); + assert_eq!(::to_bits(-96, 11).unwrap(), 0b111_1010_0000); + assert_eq!( + ::to_bits(-1025, 11), + Err(ReportError::ValueOutOfRange) + ); + } + + #[test] + fn width_must_fit_storage_type() { + assert_eq!(::from_bits(0, 9), Err(ReportError::InvalidWidth)); + assert_eq!(::to_bits(true, 2), Err(ReportError::InvalidWidth)); + } +} diff --git a/examples/rt685s-evk/Cargo.lock b/examples/rt685s-evk/Cargo.lock index a88ad7bf4..40401e58e 100644 --- a/examples/rt685s-evk/Cargo.lock +++ b/examples/rt685s-evk/Cargo.lock @@ -1272,6 +1272,7 @@ dependencies = [ "embedded-services", "num_enum", "time-alarm-service-interface", + "typenum", ] [[package]] diff --git a/examples/rt685s-evk/src/bin/time_alarm_hid.rs b/examples/rt685s-evk/src/bin/time_alarm_hid.rs new file mode 100644 index 000000000..889d0ff74 --- /dev/null +++ b/examples/rt685s-evk/src/bin/time_alarm_hid.rs @@ -0,0 +1,86 @@ +#![no_std] +#![no_main] + +use embassy_imxrt::i2c::slave::{Address, I2cSlave}; +use embassy_imxrt::i2c::{self, Async}; +use embassy_imxrt::{bind_interrupts, peripherals}; +use embedded_mcu_hal::nvram::Nvram; +use embedded_services::info; +use static_cell::StaticCell; +use time_alarm_service_interface::TimeAlarmService; +use {defmt_rtt as _, panic_probe as _}; + +const SLAVE_ADDR: Option
= Address::new(0x15); + +bind_interrupts!(struct Irqs { + FLEXCOMM2 => i2c::InterruptHandler; +}); + +type TimeAlarmServiceType = time_alarm_service::Service<'static>; +type TimeAlarmServiceRelayHandlerType = time_alarm_service_relay::hid::TimeAlarmHidRelay<'static, TimeAlarmServiceType>; + +#[embassy_executor::main] +async fn main(spawner: embassy_executor::Spawner) { + let p = embassy_imxrt::init(Default::default()); + + static RTC: StaticCell = StaticCell::new(); + let rtc = RTC.init(embassy_imxrt::rtc::Rtc::new(p.RTC)); + let (dt_clock, rtc_nvram) = rtc.split(); + + let [tz, ac_expiration, ac_policy, dc_expiration, dc_policy, ..] = rtc_nvram.storage(); + + embedded_services::init().await; + info!("services initialized"); + + let time_service = odp_service_common::spawn_service!(spawner, TimeAlarmServiceType, |resources| { + time_alarm_service::Service::new( + resources, + dt_clock, + tz, + ac_expiration, + ac_policy, + dc_expiration, + dc_policy, + ) + }) + .expect("Failed to spawn time alarm service"); + + let i2c = I2cSlave::new_async(p.FLEXCOMM2, p.PIO0_18, p.PIO0_17, Irqs, SLAVE_ADDR.unwrap(), p.DMA0_CH4).unwrap(); + // GPIO on P0_28. + use embassy_imxrt::gpio; + let attn_pin = gpio::Output::new( + p.PIO0_28, + gpio::Level::High, + gpio::DriveMode::OpenDrain, + gpio::DriveStrength::Normal, + gpio::SlewRate::Standard, + ); + + let _hidsvc = odp_service_common::spawn_service!( + spawner, + hidi2c_target_service::Service< + 'static, + I2cSlave<'static, Async>, + gpio::Output<'static>, + TimeAlarmServiceRelayHandlerType, + >, + |resources| hidi2c_target_service::Service::new( + resources, + i2c, + attn_pin, + TimeAlarmServiceRelayHandlerType::new(time_service), + hidi2c_target_service::HardwareVersionInfo { + vendor_id: hidi2c_target_service::VendorId::new(0x3333).unwrap(), + product_id: hidi2c_target_service::ProductId(0x4444), + version_id: hidi2c_target_service::VersionId(0x0001), + }, + hidi2c_target_service::TimeoutSettings::default() + ) + ) + .expect("Failed to spawn HID service"); + + loop { + embassy_time::Timer::after(embassy_time::Duration::from_secs(10)).await; + info!("Current time from service: {:?}", time_service.get_real_time().unwrap()); + } +} diff --git a/examples/rt685s-evk/src/bin/time_alarm.rs b/examples/rt685s-evk/src/bin/time_alarm_mctp.rs similarity index 95% rename from examples/rt685s-evk/src/bin/time_alarm.rs rename to examples/rt685s-evk/src/bin/time_alarm_mctp.rs index 7be20a348..2e634a478 100644 --- a/examples/rt685s-evk/src/bin/time_alarm.rs +++ b/examples/rt685s-evk/src/bin/time_alarm_mctp.rs @@ -16,7 +16,8 @@ use {defmt_rtt as _, panic_probe as _}; // This is especially helpful for the relay handler, which has a lot of generic parameters due to the traits it needs to implement. // type TimeAlarmServiceType = time_alarm_service::Service<'static>; -type TimeAlarmServiceRelayHandlerType = time_alarm_service_relay::TimeAlarmServiceRelayHandler; +type TimeAlarmServiceRelayHandlerType = + time_alarm_service_relay::mctp::TimeAlarmServiceRelayHandler; #[embassy_executor::main] async fn main(spawner: embassy_executor::Spawner) { diff --git a/time-alarm-service-relay/Cargo.toml b/time-alarm-service-relay/Cargo.toml index e098b1904..f168c3086 100644 --- a/time-alarm-service-relay/Cargo.toml +++ b/time-alarm-service-relay/Cargo.toml @@ -15,6 +15,7 @@ embedded-services.workspace = true embedded-mcu-hal.workspace = true num_enum.workspace = true time-alarm-service-interface.workspace = true +typenum.workspace = true [features] defmt = [ diff --git a/time-alarm-service-relay/src/hid/SystemWakeTimerAndRtc.wara b/time-alarm-service-relay/src/hid/SystemWakeTimerAndRtc.wara new file mode 100644 index 000000000..dc77b4e31 --- /dev/null +++ b/time-alarm-service-relay/src/hid/SystemWakeTimerAndRtc.wara @@ -0,0 +1,321 @@ +[[usagePage]] +id = 0x01 +name = "Generic Desktop" + + [[usagePage.usage]] + id = 0x14 + name = "System Wake Timer and Real Time Clock" + types = ['CA'] + + [[usagePage.usage]] + id = 0xF0 + name = "Timer Expiration: External Power" + types = ['DV'] + + [[usagePage.usage]] + id = 0xF1 + name = "Timer Expiration: Internal Power" + types = ['DV'] + + [[usagePage.usage]] + id = 0xF2 + name = "Power Source Change Minimum Expiration" + types = ['DV'] + + [[usagePage.usage]] + id = 0xF3 + name = "Lowest System Wakeable Power State" + types = ['NAry'] + + [[usagePage.usage]] + id = 0xF4 + name = "S1" + types = ['Sel'] + + [[usagePage.usage]] + id = 0xF5 + name = "S2" + types = ['Sel'] + + [[usagePage.usage]] + id = 0xF6 + name = "S3" + types = ['Sel'] + + [[usagePage.usage]] + id = 0xF7 + name = "S4" + types = ['Sel'] + + [[usagePage.usage]] + id = 0xF8 + name = "S5" + types = ['Sel'] + +[[usagePage]] +id = 0x06 +name = "Generic Device Controls" + + [[usagePage.usage]] + id = 0x50 + name = "Vendor Current State" + types = ['DV'] + + [[usagePage.usage]] + id = 0x51 + name = "Current State" + types = ['NAry'] + + [[usagePage.usage]] + id = 0x52 + name = "Cleared" + types = ['Sel'] + + [[usagePage.usage]] + id = 0x53 + name = "Failed" + types = ['Sel'] + + [[usagePage.usage]] + id = 0x54 + name = "Running" + types = ['Sel'] + + [[usagePage.usage]] + id = 0x55 + name = "Expired" + types = ['Sel'] + + [[usagePage.usage]] + id = 0x56 + name = "Signaled" + types = ['Sel'] + +[[usagePage]] +id = 0x13 +name = "Time and Date" + + [[usagePage.usage]] + id = 0x1 + name = "Year" + types = ['DV'] + + [[usagePage.usage]] + id = 0x2 + name = "Month" + types = ['DV'] + + [[usagePage.usage]] + id = 0x3 + name = "Day" + types = ['DV'] + + [[usagePage.usage]] + id = 0x4 + name = "Hour" + types = ['DV'] + + [[usagePage.usage]] + id = 0x5 + name = "Minute" + types = ['DV'] + + [[usagePage.usage]] + id = 0x6 + name = "Second" + types = ['DV'] + + [[usagePage.usage]] + id = 0x7 + name = "Millisecond" + types = ['DV'] + + [[usagePage.usage]] + id = 0x10 + name = "Time Zone Offset From UTC" + types = ['DV'] + + [[usagePage.usage]] + id = 0x11 + name = "Daylight Savings Time Observed" + types = ['DF'] + + [[usagePage.usage]] + id = 0x12 + name = "Daylight Savings Time Active" + types = ['DF'] + +[[applicationCollection]] +usage = ['Generic Desktop', 'System Wake Timer and Real Time Clock'] + + [[applicationCollection.featureReport]] + + # Indicate the lowest power-state of the system that this timer may trigger a wake. + # If system goes to a lower power-state, regardless of timer settings, will NOT trigger a wake. + [[applicationCollection.featureReport.logicalCollection]] + usage = ['Generic Desktop', 'Lowest System Wakeable Power State'] + + [[applicationCollection.featureReport.logicalCollection.arrayItem]] + usageRange = ['Generic Desktop', 'S3', 'S5'] + + [[applicationCollection.outputReport]] + + # Configures the Wake-Timer. + # Null values (e.g. 0) clears the timer configuration. + + [[applicationCollection.outputReport.variableItem]] + usage = ['Generic Desktop', 'Timer Expiration: External Power'] + logicalValueRange = [1, 2147483647] + unit = 'second' + reportFlags = ['nullstate'] + + [[applicationCollection.outputReport]] + + [[applicationCollection.outputReport.variableItem]] + usage = ['Generic Desktop', 'Timer Expiration: Internal Power'] + logicalValueRange = [1, 2147483647] + unit = 'second' + reportFlags = ['nullstate'] + + [[applicationCollection.outputReport]] + + [[applicationCollection.outputReport.variableItem]] + usage = ['Generic Desktop', 'Power Source Change Minimum Expiration'] + logicalValueRange = [1, 60] + unit = 'second' + reportFlags = ['nullstate'] + + [[applicationCollection.inputReport]] + + # Null values (e.g. 0) indicates the configuration has not been set. + + [[applicationCollection.inputReport.variableItem]] + usage = ['Generic Desktop', 'Timer Expiration: External Power'] + logicalValueRange = [1, 2147483647] + unit = 'second' + reportFlags = ['nullstate'] + + [[applicationCollection.inputReport.variableItem]] + usage = ['Generic Desktop', 'Timer Expiration: Internal Power'] + logicalValueRange = [1, 2147483647] + unit = 'second' + reportFlags = ['nullstate'] + + [[applicationCollection.inputReport.variableItem]] + usage = ['Generic Desktop', 'Power Source Change Minimum Expiration'] + logicalValueRange = [1, 60] + unit = 'second' + reportFlags = ['nullstate'] + + [[applicationCollection.inputReport.logicalCollection]] + usage = ['Generic Device Controls', 'Current State'] + + [[applicationCollection.inputReport.logicalCollection.arrayItem]] + usages = [ ['Generic Device Controls', 'Cleared'], + ['Generic Device Controls', 'Failed'], + ['Generic Device Controls', 'Running'], + ['Generic Device Controls', 'Expired'], + ['Generic Device Controls', 'Signaled']] + + [[applicationCollection.inputReport.variableItem]] + usage = ['Generic Device Controls', 'Vendor Current State'] + sizeInBits = 4 + logicalValueRange = 'maxUnsignedSizeRange' + + [[applicationCollection.outputReport]] + + [[applicationCollection.outputReport.variableItem]] + usage = ['Time and Date', 'Year'] + logicalValueRange = [1900,9999] + + [[applicationCollection.outputReport.variableItem]] + usage = ['Time and Date', 'Month'] + logicalValueRange = [1,12] + + [[applicationCollection.outputReport.variableItem]] + usage = ['Time and Date', 'Day'] + logicalValueRange = [1,31] + + [[applicationCollection.outputReport.variableItem]] + usage = ['Time and Date', 'Hour'] + logicalValueRange = [0,23] + + [[applicationCollection.outputReport.variableItem]] + usage = ['Time and Date', 'Minute'] + logicalValueRange = [0,59] + + [[applicationCollection.outputReport.variableItem]] + usage = ['Time and Date', 'Second'] + logicalValueRange = [0,59] + + [[applicationCollection.outputReport.variableItem]] + usage = ['Time and Date', 'Millisecond'] + logicalValueRange = [0,999] + + [[applicationCollection.outputReport.variableItem]] + usage = ['Time and Date', 'Time Zone Offset From UTC'] + logicalValueRange = [-720, 840] + reportFlags = ['nullstate'] + + [[applicationCollection.outputReport.variableItem]] + usage = ['Time and Date', 'Daylight Savings Time Observed'] + logicalValueRange = [0,1] + + [[applicationCollection.outputReport.variableItem]] + usage = ['Time and Date', 'Daylight Savings Time Active'] + logicalValueRange = [0,1] + + [[applicationCollection.inputReport]] + + [[applicationCollection.inputReport.variableItem]] + usage = ['Time and Date', 'Year'] + logicalValueRange = [1900,9999] + + [[applicationCollection.inputReport.variableItem]] + usage = ['Time and Date', 'Month'] + logicalValueRange = [1,12] + + [[applicationCollection.inputReport.variableItem]] + usage = ['Time and Date', 'Day'] + logicalValueRange = [1,31] + + [[applicationCollection.inputReport.variableItem]] + usage = ['Time and Date', 'Hour'] + logicalValueRange = [0,23] + + [[applicationCollection.inputReport.variableItem]] + usage = ['Time and Date', 'Minute'] + logicalValueRange = [0,59] + + [[applicationCollection.inputReport.variableItem]] + usage = ['Time and Date', 'Second'] + logicalValueRange = [0,59] + + [[applicationCollection.inputReport.variableItem]] + usage = ['Time and Date', 'Millisecond'] + logicalValueRange = [0,999] + + [[applicationCollection.inputReport.variableItem]] + usage = ['Time and Date', 'Time Zone Offset From UTC'] + logicalValueRange = [-720, 840] + reportFlags = ['nullstate'] + + [[applicationCollection.inputReport.variableItem]] + usage = ['Time and Date', 'Daylight Savings Time Observed'] + logicalValueRange = [0,1] + + [[applicationCollection.inputReport.variableItem]] + usage = ['Time and Date', 'Daylight Savings Time Active'] + logicalValueRange = [0,1] + + [[applicationCollection.inputReport.logicalCollection]] + usage = ['Generic Device Controls', 'Current State'] + + [[applicationCollection.inputReport.logicalCollection.arrayItem]] + usages = [ ['Generic Device Controls', 'Failed'], + ['Generic Device Controls', 'Running']] + + [[applicationCollection.inputReport.variableItem]] + usage = ['Generic Device Controls', 'Vendor Current State'] + sizeInBits = 4 + logicalValueRange = 'maxUnsignedSizeRange' \ No newline at end of file diff --git a/time-alarm-service-relay/src/hid/mod.rs b/time-alarm-service-relay/src/hid/mod.rs new file mode 100644 index 000000000..42cea025e --- /dev/null +++ b/time-alarm-service-relay/src/hid/mod.rs @@ -0,0 +1,286 @@ +//! Relay types for the time-alarm service over HID. + +use embedded_services::relay::hid::HidError; +use embedded_services::relay::hid::{ + GetHidReport, GetHidReportType, HidDevicePowerState, HidReport, HidReportDescriptor, ReportId, SetHidReport, + reports, +}; +use embedded_services::{error, info}; +use time_alarm_service_interface::{AcpiTimerId, AlarmTimerSeconds, TimeAlarmService}; + +mod serialization; + +use serialization::{AlarmCurrentState, FeatureReportId, InputReportId, OutputReportId}; + +use crate::hid::serialization::GetAlarmReport; + +/// Anything that can make a Set report fail. The host is told about it through the Failed state of +/// the matching Get report rather than through the HID transport. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +enum SetReportError { + /// The report payload didn't match the layout we published in our report descriptor. + Malformed, + + /// The service rejected the requested value. + Service, +} + +impl From for SetReportError { + fn from(_: reports::ReportError) -> Self { + Self::Malformed + } +} + +impl From for SetReportError { + fn from(_: serialization::TimestampConversionError) -> Self { + Self::Malformed + } +} + +impl From for SetReportError { + fn from(_: embedded_mcu_hal::time::DatetimeClockError) -> Self { + Self::Service + } +} + +/// Relay adapter that presents the time-alarm service to a HID transport service as a HidDevice. +pub struct TimeAlarmHidRelay<'s, Service: TimeAlarmService> { + service: Service, + + descriptor: HidReportDescriptor<'static>, + + /// Sticky failure of the last SetAlarm report, reported back on the next GetAlarm read. + alarm_failed: bool, + + /// Sticky failure of the last SetTime report, reported back on the next GetTime read. + time_failed: bool, + + _phantom: core::marker::PhantomData<&'s ()>, +} + +impl<'s, Service: TimeAlarmService> TimeAlarmHidRelay<'s, Service> { + pub fn new(service: Service) -> Self { + Self { + service, + + // Panic safety: The HID report descriptor is a static constant and should always be valid. + #[allow(clippy::expect_used)] + descriptor: HidReportDescriptor::new(serialization::TIME_ALARM_HID_DESCRIPTOR) + .expect("time alarm HID report descriptor is a static constant and should be valid"), + + alarm_failed: false, + time_failed: false, + _phantom: core::marker::PhantomData, + } + } + + fn apply_set_alarm(&self, timer_id: AcpiTimerId, data: &[u8]) -> Result<(), SetReportError> { + let set_alarm = serialization::SetAlarmReport::unpack(data)?; + info!("Parsed SetAlarmReport: {:?}", set_alarm); + + self.service + .set_timer_value(timer_id, serialization::convert_timer(set_alarm.timer_seconds))?; + + Ok(()) + } + + fn apply_set_policy(&self, data: &[u8]) -> Result<(), SetReportError> { + let set_debounce = serialization::SetDebounceReport::unpack(data)?; + info!("Parsed SetDebounceReport: {:?}", set_debounce); + + let wake_policy = serialization::convert_policy(set_debounce.power_source_change_debounce_seconds); + self.service + .set_expired_timer_policy(AcpiTimerId::AcPower, wake_policy)?; + self.service + .set_expired_timer_policy(AcpiTimerId::DcPower, wake_policy)?; + + Ok(()) + } + + fn apply_set_time(&self, data: &[u8]) -> Result<(), SetReportError> { + let time: time_alarm_service_interface::AcpiTimestamp = + serialization::SetTimeReport::unpack(data)?.try_into()?; + info!("Setting time to {:?}", time); + self.service.set_real_time(time)?; + + Ok(()) + } +} + +impl<'s, Service: TimeAlarmService> embedded_services::relay::hid::HidDevice for TimeAlarmHidRelay<'s, Service> { + type InputReportMaxSize = typenum::U10; + type OutputReportMaxSize = typenum::U8; + type FeatureReportMaxSize = typenum::U1; + + const MAX_REPORT_COUNT: u8 = 2; + const MAX_DESCRIPTOR_LEN: usize = serialization::TIME_ALARM_HID_DESCRIPTOR.len(); + + fn report_descriptor(&self) -> &HidReportDescriptor<'_> { + info!("descriptor len: {}", self.descriptor.as_bytes().len()); + &self.descriptor + } + + async fn process_get_report( + &mut self, + report_type: GetHidReportType, + report_id: ReportId, + process_report: impl AsyncFnOnce(GetHidReport<'_>) -> R, + ) -> Result { + info!("Received command to get report with ID {:?}", report_id); + + match report_type { + GetHidReportType::Input => match InputReportId::try_from(report_id).map_err(|_| HidError::TriggerReset)? { + InputReportId::GetAlarm => { + info!("Received command to get alarm report"); + let ac_timer = self.service.get_timer_value(AcpiTimerId::AcPower); + let dc_timer = self.service.get_timer_value(AcpiTimerId::DcPower); + let read_failed = ac_timer.is_err() || dc_timer.is_err(); + if read_failed { + error!("Failed to read alarm timer values"); + } + let ac_timer = ac_timer.unwrap_or(AlarmTimerSeconds::DISABLED); + let dc_timer = dc_timer.unwrap_or(AlarmTimerSeconds::DISABLED); + + // HID-TAD doesn't support separate AC/DC timer debounces so they'll report the same thing + let wake_policy = self.service.get_expired_timer_policy(AcpiTimerId::AcPower); + + let ac_state = self.service.get_wake_status(AcpiTimerId::AcPower); + let dc_state = self.service.get_wake_status(AcpiTimerId::DcPower); + + let current_state = if self.alarm_failed || read_failed { + AlarmCurrentState::Failed + } else if ac_state.timer_triggered_wake() || dc_state.timer_triggered_wake() { + AlarmCurrentState::Signaled + } else if ac_state.timer_expired() || dc_state.timer_expired() { + AlarmCurrentState::Expired + } else if ac_timer != AlarmTimerSeconds::DISABLED || dc_timer != AlarmTimerSeconds::DISABLED { + AlarmCurrentState::Running + } else { + AlarmCurrentState::Cleared + }; + + let vendor_current_state = 0; + let report_payload = + GetAlarmReport::new(ac_timer, dc_timer, wake_policy, current_state, vendor_current_state); + let report = report_payload.pack().map_err(|_| HidError::TriggerReset)?; + let report = HidReport::new(report_id, &report); + Ok(process_report(GetHidReport::Input(report)).await) + } + InputReportId::GetTime => { + info!("Received command to get time report"); + let report_payload = match self.service.get_real_time() { + Ok(time) if !self.time_failed => { + serialization::GetTimeReport::new(time, serialization::TimeCurrentState::Running, 0) + } + Ok(time) => serialization::GetTimeReport::new(time, serialization::TimeCurrentState::Failed, 0), + Err(_) => { + error!("Failed to read the current time"); + serialization::GetTimeReport::failed(0) + } + }; + let report = report_payload.pack().map_err(|_| HidError::TriggerReset)?; + let report = HidReport::new(report_id, &report); + Ok(process_report(GetHidReport::Input(report)).await) + } + }, + GetHidReportType::Feature => { + match FeatureReportId::try_from(report_id).map_err(|_| HidError::TriggerReset)? { + FeatureReportId::Capabilities => { + let capabilities = self.service.get_capabilities(); + let deepest_power_state = + if capabilities.ac_s5_wake_supported() || capabilities.dc_s5_wake_supported() { + serialization::PowerState::S5 + } else if capabilities.ac_s4_wake_supported() || capabilities.dc_s4_wake_supported() { + serialization::PowerState::S4 + } else { + serialization::PowerState::S3 + }; + let capabilities = serialization::CapabilitiesFeatureReport::new(deepest_power_state) + .pack() + .map_err(|_| HidError::TriggerReset)?; + Ok(process_report(GetHidReport::Feature(HidReport::new(report_id, &capabilities))).await) + } + } + } + } + } + + async fn set_report(&mut self, report: &SetHidReport<'_>) -> Result<(), HidError> { + match report { + SetHidReport::Output(r) => { + info!("Received command to set output report with ID {:?}", r.id()); + match OutputReportId::try_from(r.id()).map_err(|_| HidError::TriggerReset)? { + id @ (OutputReportId::SetAcAlarm | OutputReportId::SetDcAlarm) => { + let timer_id = if id == OutputReportId::SetAcAlarm { + AcpiTimerId::AcPower + } else { + AcpiTimerId::DcPower + }; + info!("Received command to set {:?} alarm report", timer_id); + info!("Report data: {:?}", r.data()); + + self.alarm_failed = self + .apply_set_alarm(timer_id, r.data()) + .inspect_err(|e| error!("Failed to apply SetAlarmReport: {:?}", e)) + .is_err(); + } + OutputReportId::SetDebounce => { + info!("Received command to set debounce report"); + info!("Report data: {:?}", r.data()); + + self.alarm_failed = self + .apply_set_policy(r.data()) + .inspect_err(|e| error!("Failed to apply SetDebounceReport: {:?}", e)) + .is_err(); + } + OutputReportId::SetTime => { + info!("Received command to set time report"); + info!("Report data: {:?}", r.data()); + self.time_failed = self + .apply_set_time(r.data()) + .inspect_err(|e| error!("Failed to apply SetTimeReport: {:?}", e)) + .is_err(); + } + } + } + SetHidReport::Feature(r) => info!( + "NOT IMPLEMENTED: Received command to set feature report with ID {:?}", + r.id() + ), + } + Ok(()) + } + + async fn wait_for_input_report(&mut self) { + // We don't emit any unsolicited input reports, so this will never complete. Wake is done out-of-band. + core::future::pending::<()>().await; + } + + async fn process_next_input_report( + &mut self, + _process_report: impl AsyncFnOnce(HidReport<'_>) -> R, + ) -> Result { + // We don't emit any unsolicited input reports, so this will never complete. Wake is done out-of-band. + core::future::pending::<()>().await; + Err(HidError::TriggerReset) // unreachable + } + + fn has_pending_input_report(&mut self) -> bool { + // We don't emit any unsolicited input reports, so this will never complete. Wake is done out-of-band. + false + } + + async fn set_power_state(&mut self, state: HidDevicePowerState) -> Result<(), HidError> { + info!("Received command to set power state to {:?}", state); + // We don't turn off when commanded because it's our job to wake the host out-of-band, even when we're "sleeping". + Ok(()) + } + + async fn reset(&mut self) { + info!("Received reset command"); + + self.alarm_failed = false; + self.time_failed = false; + } +} diff --git a/time-alarm-service-relay/src/hid/serialization.rs b/time-alarm-service-relay/src/hid/serialization.rs new file mode 100644 index 000000000..9e0f5745d --- /dev/null +++ b/time-alarm-service-relay/src/hid/serialization.rs @@ -0,0 +1,554 @@ +use embedded_mcu_hal::time::{Datetime, DatetimeFields}; +use embedded_services::hid_report; +use embedded_services::relay::hid::ReportId; +use embedded_services::relay::hid::reports; + +#[allow(dead_code)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub(crate) enum TimestampConversionError { + Report(reports::ReportError), + Datetime(embedded_mcu_hal::time::DatetimeError), + DatetimeClock(embedded_mcu_hal::time::DatetimeClockError), +} + +impl From for TimestampConversionError { + fn from(err: reports::ReportError) -> Self { + Self::Report(err) + } +} +impl From for TimestampConversionError { + fn from(err: embedded_mcu_hal::time::DatetimeError) -> Self { + Self::Datetime(err) + } +} +impl From for TimestampConversionError { + fn from(err: embedded_mcu_hal::time::DatetimeClockError) -> Self { + Self::DatetimeClock(err) + } +} + +// HID Usage Tables: 1.7.0 +// Descriptor size: 367 (bytes) +// +----------+---------+-------------------+ +// | ReportId | Kind | ReportSizeInBytes | +// +----------+---------+-------------------+ +// | 1 | Input | 10 | +// +----------+---------+-------------------+ +// | 1 | Output | 4 | +// +----------+---------+-------------------+ +// | 1 | Feature | 1 | +// +----------+---------+-------------------+ +// | 2 | Input | 9 | +// +----------+---------+-------------------+ +// | 2 | Output | 4 | +// +----------+---------+-------------------+ +// | 3 | Output | 1 | +// +----------+---------+-------------------+ +// | 4 | Output | 8 | +// +----------+---------+-------------------+ + +// -------- INPUT REPORTS -------- + +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, num_enum::IntoPrimitive, num_enum::TryFromPrimitive)] +pub(crate) enum InputReportId { + GetAlarm = 1, + GetTime = 2, +} + +impl TryFrom for InputReportId { + type Error = num_enum::TryFromPrimitiveError; + + fn try_from(value: ReportId) -> Result { + Self::try_from(value.0) + } +} + +hid_report! { + pub(crate) struct GetAlarmReport { + ac_timer: u32 => 31, + _padding1: u8 => 1, + dc_timer: u32 => 31, + power_source_change_debounce_seconds: u8 => 6, + current_state: u8 => 3, + vendor_current_state: u8 => 4, + _padding: u8 => 4 + // Define the fields for the GetAlarmReport here + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, num_enum::IntoPrimitive, num_enum::TryFromPrimitive)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[repr(u8)] +pub(crate) enum AlarmCurrentState { + Cleared = 1, + Failed = 2, + Running = 3, + Expired = 4, + Signaled = 5, +} + +impl GetAlarmReport { + pub(crate) fn new( + ac_timer: time_alarm_service_interface::AlarmTimerSeconds, + dc_timer: time_alarm_service_interface::AlarmTimerSeconds, + power_policy: time_alarm_service_interface::AlarmExpiredWakePolicy, + current_state: AlarmCurrentState, + vendor_current_state: u8, + ) -> Self { + Self { + ac_timer: convert_timer_to_wire(ac_timer), + _padding1: 0, + dc_timer: convert_timer_to_wire(dc_timer), + power_source_change_debounce_seconds: convert_policy_to_wire(power_policy), + current_state: current_state.into(), + vendor_current_state, + _padding: 0, + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, num_enum::IntoPrimitive, num_enum::TryFromPrimitive)] +#[repr(u8)] +pub(crate) enum TimeCurrentState { + Failed = 1, + Running = 2, +} + +hid_report! { + pub(crate) struct GetTimeReport { + /// 1900 - 9999 + year: u16 => 14, + /// 1 - 12 + month: u8 => 4, + /// 1 - 31 + day: u8 => 5, + /// 0 - 23 + hour: u8 => 5, + /// 0 - 59 + minute: u8 => 6, + /// 0 - 59 + second: u8 => 6, + /// 0 - 999 + millisecond: u16 => 10, + /// -720 - 840, minutes from UTC + time_zone: i16 => 11, + dst_observed: bool => 1, + dst_active: bool => 1, + current_state: u8 => 2, + vendor_current_state: u8 => 4, + _reserved: u8 => 3 + } +} + +impl GetTimeReport { + pub(crate) fn new( + ts: time_alarm_service_interface::AcpiTimestamp, + current_state: TimeCurrentState, + vendor_state: u8, + ) -> Self { + Self { + year: ts.datetime.year(), + month: ts.datetime.month().into(), + day: ts.datetime.day(), + hour: ts.datetime.hour(), + minute: ts.datetime.minute(), + second: ts.datetime.second(), + millisecond: (ts.datetime.nanoseconds() / 1_000_000) as u16, + time_zone: i16::from(ts.time_zone), + dst_observed: matches!( + ts.dst_status, + time_alarm_service_interface::AcpiDaylightSavingsTimeStatus::NotAdjusted + | time_alarm_service_interface::AcpiDaylightSavingsTimeStatus::Adjusted + ), + dst_active: matches!( + ts.dst_status, + time_alarm_service_interface::AcpiDaylightSavingsTimeStatus::Adjusted + ), + current_state: current_state.into(), + vendor_current_state: vendor_state, + _reserved: 0, + } + } + + /// Report used when the current time is unavailable; the time fields carry no meaning. + pub(crate) fn failed(vendor_state: u8) -> Self { + Self { + current_state: TimeCurrentState::Failed.into(), + vendor_current_state: vendor_state, + ..Default::default() + } + } +} + +// -------- OUTPUT REPORTS -------- + +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, num_enum::IntoPrimitive, num_enum::TryFromPrimitive)] +#[allow(clippy::enum_variant_names)] +pub(crate) enum OutputReportId { + SetAcAlarm = 1, + SetDcAlarm = 2, + SetDebounce = 3, + SetTime = 4, +} + +impl TryFrom for OutputReportId { + type Error = num_enum::TryFromPrimitiveError; + + fn try_from(value: ReportId) -> Result { + Self::try_from(value.0) + } +} + +hid_report! { + pub(crate) struct SetAlarmReport { + pub timer_seconds: u32 => 31, + _padding: u8 => 1 + } +} + +hid_report! { + pub(crate) struct SetDebounceReport { + pub power_source_change_debounce_seconds: u8 => 6, + _padding: u8 => 2 + } +} + +/// Converts a timer value in seconds to an `AlarmTimerSeconds` enum, mapping the HID null value to +/// disabled. See [`TIMER_NULL`]; if we change the supported logical value range, we'll need to +/// update this too. +pub(crate) fn convert_timer(seconds: u32) -> time_alarm_service_interface::AlarmTimerSeconds { + if seconds == TIMER_NULL { + time_alarm_service_interface::AlarmTimerSeconds::DISABLED + } else { + time_alarm_service_interface::AlarmTimerSeconds(seconds) + } +} + +/// A value in the physical range but out of the logical range for our timer. Must agree with report descriptor. +const TIMER_NULL: u32 = 0; + +/// LogicalMaximum of the alarm timer items in our report descriptor; also the widest value the +/// 31-bit timer fields can hold. +const TIMER_LOGICAL_MIN: u32 = 1; +const TIMER_LOGICAL_MAX: u32 = 0x7FFF_FFFF; + +/// LogicalMaximum of the power source change debounce items in our report descriptor. +const DEBOUNCE_LOGICAL_MIN: u32 = 1; +const DEBOUNCE_LOGICAL_MAX: u32 = 60; + +/// Inverse of [`convert_timer`]: a disabled timer is reported as the null value, and values the +/// descriptor can't express are clamped rather than failing the whole report. +fn convert_timer_to_wire(timer: time_alarm_service_interface::AlarmTimerSeconds) -> u32 { + if timer == time_alarm_service_interface::AlarmTimerSeconds::DISABLED { + TIMER_NULL + } else if timer == time_alarm_service_interface::AlarmTimerSeconds(0) { + // TODO - there's currently a disagreement between HID and ACPI on what a logical value of "0" means. + // ACPI says it means "this timer is expired" but HID says it's not a valid value, which in + // effect means "this timer is disabled". + // For now, we treat it as disabled, but there may be a case to be made that it should mean + // the same thing as in ACPI. + TIMER_NULL + } else { + timer.0.clamp(TIMER_LOGICAL_MIN, TIMER_LOGICAL_MAX) + } +} + +fn convert_policy_to_wire(policy: time_alarm_service_interface::AlarmExpiredWakePolicy) -> u8 { + match policy { + time_alarm_service_interface::AlarmExpiredWakePolicy::INSTANTLY => 0, + // TODO "never" isn't expressible with the current HID interface; need to circle back with time and hid folks + // on if this was a deliberate design decision or an oversight. For now, report it as our logical max. + time_alarm_service_interface::AlarmExpiredWakePolicy::NEVER => DEBOUNCE_LOGICAL_MAX as u8, + time_alarm_service_interface::AlarmExpiredWakePolicy(seconds) => { + seconds.clamp(DEBOUNCE_LOGICAL_MIN, DEBOUNCE_LOGICAL_MAX) as u8 + } + } +} + +/// Inverse of [`convert_policy_to_wire`]. The null value means the host isn't asking for a minimum +/// expiration, so we wake instantly; NEVER has no wire encoding and so can't be round-tripped. +pub(crate) fn convert_policy(seconds: u8) -> time_alarm_service_interface::AlarmExpiredWakePolicy { + match u32::from(seconds) { + seconds @ DEBOUNCE_LOGICAL_MIN..=DEBOUNCE_LOGICAL_MAX => { + time_alarm_service_interface::AlarmExpiredWakePolicy(seconds) + } + _ => time_alarm_service_interface::AlarmExpiredWakePolicy::INSTANTLY, + } +} + +hid_report! { + pub(crate) struct SetTimeReport { + /// 1900 - 9999 + pub year: u16 => 14, + /// 1 - 12 + pub month: u8 => 4, + /// 1 - 31 + pub day: u8 => 5, + /// 0 - 23 + pub hour: u8 => 5, + /// 0 - 59 + pub minute: u8 => 6, + /// 0 - 59 + pub second: u8 => 6, + /// 0 - 999 + pub millisecond: u16 => 10, + /// -720 - 840, minutes from UTC + pub time_zone: i16 => 11, + pub dst_observed: bool => 1, + pub dst_active: bool => 1, + _reserved: u8 => 1, + } +} + +impl TryFrom for time_alarm_service_interface::AcpiTimestamp { + type Error = TimestampConversionError; + fn try_from(report: SetTimeReport) -> Result { + Ok(Self { + datetime: Datetime::new(DatetimeFields { + year: report.year, + month: report + .month + .try_into() + .map_err(|_| TimestampConversionError::Datetime(embedded_mcu_hal::time::DatetimeError::Month))?, + day: report.day, + hour: report.hour, + minute: report.minute, + second: report.second, + nanosecond: report.millisecond as u32 * 1_000_000, + })?, + time_zone: time_alarm_service_interface::AcpiTimeZone::try_from(report.time_zone)?, + dst_status: match (report.dst_observed, report.dst_active) { + (false, false) => time_alarm_service_interface::AcpiDaylightSavingsTimeStatus::NotObserved, + (true, false) => time_alarm_service_interface::AcpiDaylightSavingsTimeStatus::NotAdjusted, + (true, true) => time_alarm_service_interface::AcpiDaylightSavingsTimeStatus::Adjusted, + (false, true) => { + return Err(TimestampConversionError::Report(reports::ReportError::ValueOutOfRange)); + } + }, + }) + } +} + +// -------- FEATURE REPORTS -------- + +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, num_enum::IntoPrimitive, num_enum::TryFromPrimitive)] +pub(crate) enum FeatureReportId { + Capabilities = 1, +} + +impl TryFrom for FeatureReportId { + type Error = num_enum::TryFromPrimitiveError; + + fn try_from(value: ReportId) -> Result { + Self::try_from(value.0) + } +} + +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, num_enum::IntoPrimitive, num_enum::TryFromPrimitive)] +pub(crate) enum PowerState { + S3 = 1, + S4 = 2, + S5 = 3, +} + +hid_report! { + pub(crate) struct CapabilitiesFeatureReport { + pub power_state: u8 => 2, // Type: PowerState + _padding: u8 => 6 + } +} + +impl CapabilitiesFeatureReport { + pub(crate) fn new(power_state: PowerState) -> Self { + Self { + power_state: power_state.into(), + _padding: 0, + } + } +} + +// Generated from Waratah - don't hand-edit this. Instead, modify the .wara file and rerun Waratah. +// See https://github.com/microsoft/hidtools more information on Waratah. +// Modifications to this require alterations to the above types to match. +#[rustfmt::skip] +pub(crate) const TIME_ALARM_HID_DESCRIPTOR: &[u8] = &[ + 0x05, 0x01, // UsagePage(Generic Desktop[0x0001]) + 0x09, 0x14, // UsageId(System Wake Timer and Real Time Clock[0x0014]) + 0xA1, 0x01, // Collection(Application) + 0x85, 0x01, // ReportId(1) + 0x09, 0xF3, // UsageId(Lowest System Wakeable Power State[0x00F3]) + 0xA1, 0x02, // Collection(Logical) + 0x19, 0xF6, // UsageIdMin(S3[0x00F6]) + 0x29, 0xF8, // UsageIdMax(S5[0x00F8]) + 0x15, 0x01, // LogicalMinimum(1) + 0x25, 0x03, // LogicalMaximum(3) + 0x95, 0x01, // ReportCount(1) + 0x75, 0x02, // ReportSize(2) + 0xB1, 0x00, // Feature(Data, Array, Absolute, NoWrap, Linear, PreferredState, NoNullPosition, NonVolatile, BitField) + 0xC0, // EndCollection() + 0x75, 0x06, // ReportSize(6) + 0xB1, 0x03, // Feature(Constant, Variable, Absolute, NoWrap, Linear, PreferredState, NoNullPosition, NonVolatile, BitField) + 0x09, 0xF0, // UsageId(Timer Expiration: External Power[0x00F0]) + 0x66, 0x01, 0x10, // Unit('second', SiLinear, Seconds:1) + 0x27, 0xFF, 0xFF, 0xFF, 0x7F, // LogicalMaximum(2,147,483,647) + 0x75, 0x1F, // ReportSize(31) + 0x91, 0x42, // Output(Data, Variable, Absolute, NoWrap, Linear, PreferredState, NullState, NonVolatile, BitField) + 0x75, 0x01, // ReportSize(1) + 0x91, 0x03, // Output(Constant, Variable, Absolute, NoWrap, Linear, PreferredState, NoNullPosition, NonVolatile, BitField) + 0x85, 0x02, // ReportId(2) + 0x09, 0xF1, // UsageId(Timer Expiration: Internal Power[0x00F1]) + 0x75, 0x1F, // ReportSize(31) + 0x91, 0x42, // Output(Data, Variable, Absolute, NoWrap, Linear, PreferredState, NullState, NonVolatile, BitField) + 0x75, 0x01, // ReportSize(1) + 0x91, 0x03, // Output(Constant, Variable, Absolute, NoWrap, Linear, PreferredState, NoNullPosition, NonVolatile, BitField) + 0x85, 0x03, // ReportId(3) + 0x09, 0xF2, // UsageId(Power Source Change Minimum Expiration[0x00F2]) + 0x25, 0x3C, // LogicalMaximum(60) + 0x75, 0x06, // ReportSize(6) + 0x91, 0x42, // Output(Data, Variable, Absolute, NoWrap, Linear, PreferredState, NullState, NonVolatile, BitField) + 0x75, 0x02, // ReportSize(2) + 0x91, 0x03, // Output(Constant, Variable, Absolute, NoWrap, Linear, PreferredState, NoNullPosition, NonVolatile, BitField) + 0x85, 0x01, // ReportId(1) + 0x09, 0xF0, // UsageId(Timer Expiration: External Power[0x00F0]) + 0x27, 0xFF, 0xFF, 0xFF, 0x7F, // LogicalMaximum(2,147,483,647) + 0x75, 0x1F, // ReportSize(31) + 0x81, 0x42, // Input(Data, Variable, Absolute, NoWrap, Linear, PreferredState, NullState, BitField) + 0x75, 0x01, // ReportSize(1) + 0x81, 0x03, // Input(Constant, Variable, Absolute, NoWrap, Linear, PreferredState, NoNullPosition, BitField) + 0x09, 0xF1, // UsageId(Timer Expiration: Internal Power[0x00F1]) + 0x75, 0x1F, // ReportSize(31) + 0x81, 0x42, // Input(Data, Variable, Absolute, NoWrap, Linear, PreferredState, NullState, BitField) + 0x09, 0xF2, // UsageId(Power Source Change Minimum Expiration[0x00F2]) + 0x25, 0x3C, // LogicalMaximum(60) + 0x75, 0x06, // ReportSize(6) + 0x81, 0x42, // Input(Data, Variable, Absolute, NoWrap, Linear, PreferredState, NullState, BitField) + 0x05, 0x06, // UsagePage(Generic Device Controls[0x0006]) + 0x09, 0x51, // UsageId(Current State[0x0051]) + 0xA1, 0x02, // Collection(Logical) + 0x09, 0x52, // UsageId(Cleared[0x0052]) + 0x09, 0x53, // UsageId(Failed[0x0053]) + 0x09, 0x54, // UsageId(Running[0x0054]) + 0x09, 0x55, // UsageId(Expired[0x0055]) + 0x09, 0x56, // UsageId(Signaled[0x0056]) + 0x25, 0x05, // LogicalMaximum(5) + 0x75, 0x03, // ReportSize(3) + 0x81, 0x00, // Input(Data, Array, Absolute, NoWrap, Linear, PreferredState, NoNullPosition, BitField) + 0xC0, // EndCollection() + 0x09, 0x50, // UsageId(Vendor Current State[0x0050]) + 0x65, 0x00, // Unit(None) + 0x15, 0x00, // LogicalMinimum(0) + 0x25, 0x0F, // LogicalMaximum(15) + 0x75, 0x04, // ReportSize(4) + 0x81, 0x02, // Input(Data, Variable, Absolute, NoWrap, Linear, PreferredState, NoNullPosition, BitField) + 0x81, 0x03, // Input(Constant, Variable, Absolute, NoWrap, Linear, PreferredState, NoNullPosition, BitField) + 0x85, 0x04, // ReportId(4) + 0x05, 0x13, // UsagePage(Time and Date[0x0013]) + 0x09, 0x01, // UsageId(Year[0x0001]) + 0x16, 0x6C, 0x07, // LogicalMinimum(1,900) + 0x26, 0x0F, 0x27, // LogicalMaximum(9,999) + 0x75, 0x0E, // ReportSize(14) + 0x91, 0x02, // Output(Data, Variable, Absolute, NoWrap, Linear, PreferredState, NoNullPosition, NonVolatile, BitField) + 0x09, 0x02, // UsageId(Month[0x0002]) + 0x15, 0x01, // LogicalMinimum(1) + 0x25, 0x0C, // LogicalMaximum(12) + 0x75, 0x04, // ReportSize(4) + 0x91, 0x02, // Output(Data, Variable, Absolute, NoWrap, Linear, PreferredState, NoNullPosition, NonVolatile, BitField) + 0x09, 0x03, // UsageId(Day[0x0003]) + 0x25, 0x1F, // LogicalMaximum(31) + 0x75, 0x05, // ReportSize(5) + 0x91, 0x02, // Output(Data, Variable, Absolute, NoWrap, Linear, PreferredState, NoNullPosition, NonVolatile, BitField) + 0x09, 0x04, // UsageId(Hour[0x0004]) + 0x15, 0x00, // LogicalMinimum(0) + 0x25, 0x17, // LogicalMaximum(23) + 0x91, 0x02, // Output(Data, Variable, Absolute, NoWrap, Linear, PreferredState, NoNullPosition, NonVolatile, BitField) + 0x09, 0x05, // UsageId(Minute[0x0005]) + 0x09, 0x06, // UsageId(Second[0x0006]) + 0x25, 0x3B, // LogicalMaximum(59) + 0x95, 0x02, // ReportCount(2) + 0x75, 0x06, // ReportSize(6) + 0x91, 0x02, // Output(Data, Variable, Absolute, NoWrap, Linear, PreferredState, NoNullPosition, NonVolatile, BitField) + 0x09, 0x07, // UsageId(Millisecond[0x0007]) + 0x26, 0xE7, 0x03, // LogicalMaximum(999) + 0x95, 0x01, // ReportCount(1) + 0x75, 0x0A, // ReportSize(10) + 0x91, 0x02, // Output(Data, Variable, Absolute, NoWrap, Linear, PreferredState, NoNullPosition, NonVolatile, BitField) + 0x09, 0x10, // UsageId(Time Zone Offset From UTC[0x0010]) + 0x16, 0x30, 0xFD, // LogicalMinimum(-720) + 0x26, 0x48, 0x03, // LogicalMaximum(840) + 0x75, 0x0B, // ReportSize(11) + 0x91, 0x42, // Output(Data, Variable, Absolute, NoWrap, Linear, PreferredState, NullState, NonVolatile, BitField) + 0x09, 0x11, // UsageId(Daylight Savings Time Observed[0x0011]) + 0x09, 0x12, // UsageId(Daylight Savings Time Active[0x0012]) + 0x15, 0x00, // LogicalMinimum(0) + 0x25, 0x01, // LogicalMaximum(1) + 0x95, 0x02, // ReportCount(2) + 0x75, 0x01, // ReportSize(1) + 0x91, 0x02, // Output(Data, Variable, Absolute, NoWrap, Linear, PreferredState, NoNullPosition, NonVolatile, BitField) + 0x95, 0x01, // ReportCount(1) + 0x91, 0x03, // Output(Constant, Variable, Absolute, NoWrap, Linear, PreferredState, NoNullPosition, NonVolatile, BitField) + 0x85, 0x02, // ReportId(2) + 0x09, 0x01, // UsageId(Year[0x0001]) + 0x16, 0x6C, 0x07, // LogicalMinimum(1,900) + 0x26, 0x0F, 0x27, // LogicalMaximum(9,999) + 0x75, 0x0E, // ReportSize(14) + 0x81, 0x02, // Input(Data, Variable, Absolute, NoWrap, Linear, PreferredState, NoNullPosition, BitField) + 0x09, 0x02, // UsageId(Month[0x0002]) + 0x15, 0x01, // LogicalMinimum(1) + 0x25, 0x0C, // LogicalMaximum(12) + 0x75, 0x04, // ReportSize(4) + 0x81, 0x02, // Input(Data, Variable, Absolute, NoWrap, Linear, PreferredState, NoNullPosition, BitField) + 0x09, 0x03, // UsageId(Day[0x0003]) + 0x25, 0x1F, // LogicalMaximum(31) + 0x75, 0x05, // ReportSize(5) + 0x81, 0x02, // Input(Data, Variable, Absolute, NoWrap, Linear, PreferredState, NoNullPosition, BitField) + 0x09, 0x04, // UsageId(Hour[0x0004]) + 0x15, 0x00, // LogicalMinimum(0) + 0x25, 0x17, // LogicalMaximum(23) + 0x81, 0x02, // Input(Data, Variable, Absolute, NoWrap, Linear, PreferredState, NoNullPosition, BitField) + 0x09, 0x05, // UsageId(Minute[0x0005]) + 0x09, 0x06, // UsageId(Second[0x0006]) + 0x25, 0x3B, // LogicalMaximum(59) + 0x95, 0x02, // ReportCount(2) + 0x75, 0x06, // ReportSize(6) + 0x81, 0x02, // Input(Data, Variable, Absolute, NoWrap, Linear, PreferredState, NoNullPosition, BitField) + 0x09, 0x07, // UsageId(Millisecond[0x0007]) + 0x26, 0xE7, 0x03, // LogicalMaximum(999) + 0x95, 0x01, // ReportCount(1) + 0x75, 0x0A, // ReportSize(10) + 0x81, 0x02, // Input(Data, Variable, Absolute, NoWrap, Linear, PreferredState, NoNullPosition, BitField) + 0x09, 0x10, // UsageId(Time Zone Offset From UTC[0x0010]) + 0x16, 0x30, 0xFD, // LogicalMinimum(-720) + 0x26, 0x48, 0x03, // LogicalMaximum(840) + 0x75, 0x0B, // ReportSize(11) + 0x81, 0x42, // Input(Data, Variable, Absolute, NoWrap, Linear, PreferredState, NullState, BitField) + 0x09, 0x11, // UsageId(Daylight Savings Time Observed[0x0011]) + 0x09, 0x12, // UsageId(Daylight Savings Time Active[0x0012]) + 0x15, 0x00, // LogicalMinimum(0) + 0x25, 0x01, // LogicalMaximum(1) + 0x95, 0x02, // ReportCount(2) + 0x75, 0x01, // ReportSize(1) + 0x81, 0x02, // Input(Data, Variable, Absolute, NoWrap, Linear, PreferredState, NoNullPosition, BitField) + 0x05, 0x06, // UsagePage(Generic Device Controls[0x0006]) + 0x09, 0x51, // UsageId(Current State[0x0051]) + 0xA1, 0x02, // Collection(Logical) + 0x09, 0x53, // UsageId(Failed[0x0053]) + 0x09, 0x54, // UsageId(Running[0x0054]) + 0x15, 0x01, // LogicalMinimum(1) + 0x25, 0x02, // LogicalMaximum(2) + 0x95, 0x01, // ReportCount(1) + 0x75, 0x02, // ReportSize(2) + 0x81, 0x00, // Input(Data, Array, Absolute, NoWrap, Linear, PreferredState, NoNullPosition, BitField) + 0xC0, // EndCollection() + 0x09, 0x50, // UsageId(Vendor Current State[0x0050]) + 0x15, 0x00, // LogicalMinimum(0) + 0x25, 0x0F, // LogicalMaximum(15) + 0x75, 0x04, // ReportSize(4) + 0x81, 0x02, // Input(Data, Variable, Absolute, NoWrap, Linear, PreferredState, NoNullPosition, BitField) + 0x75, 0x03, // ReportSize(3) + 0x81, 0x03, // Input(Constant, Variable, Absolute, NoWrap, Linear, PreferredState, NoNullPosition, BitField) + 0xC0, // EndCollection() +]; diff --git a/time-alarm-service-relay/src/lib.rs b/time-alarm-service-relay/src/lib.rs index ed444a394..ab0f1748f 100644 --- a/time-alarm-service-relay/src/lib.rs +++ b/time-alarm-service-relay/src/lib.rs @@ -2,58 +2,5 @@ use time_alarm_service_interface::TimeAlarmService; -mod serialization; -pub use serialization::{AcpiTimeAlarmRequest, AcpiTimeAlarmResponse, AcpiTimeAlarmResult}; - -/// A relay handler that converts MCTP messages into function calls against the time-alarm service. -pub struct TimeAlarmServiceRelayHandler { - service: T, -} - -impl TimeAlarmServiceRelayHandler { - /// Construct a new relay handler that transmits requests to the given time-alarm service. - pub fn new(service: T) -> Self { - Self { service } - } -} - -impl embedded_services::relay::mctp::RelayServiceHandlerTypes for TimeAlarmServiceRelayHandler { - type RequestType = AcpiTimeAlarmRequest; - type ResultType = AcpiTimeAlarmResult; -} - -impl embedded_services::relay::mctp::RelayServiceHandler for TimeAlarmServiceRelayHandler { - async fn process_request(&self, request: Self::RequestType) -> Self::ResultType { - match request { - AcpiTimeAlarmRequest::GetCapabilities => { - Ok(AcpiTimeAlarmResponse::Capabilities(self.service.get_capabilities())) - } - AcpiTimeAlarmRequest::GetRealTime => Ok(AcpiTimeAlarmResponse::RealTime(self.service.get_real_time()?)), - AcpiTimeAlarmRequest::SetRealTime(timestamp) => { - self.service.set_real_time(timestamp)?; - Ok(AcpiTimeAlarmResponse::OkNoData) - } - AcpiTimeAlarmRequest::GetWakeStatus(timer_id) => Ok(AcpiTimeAlarmResponse::TimerStatus( - self.service.get_wake_status(timer_id), - )), - AcpiTimeAlarmRequest::ClearWakeStatus(timer_id) => { - self.service.clear_wake_status(timer_id); - Ok(AcpiTimeAlarmResponse::OkNoData) - } - AcpiTimeAlarmRequest::SetExpiredTimerPolicy(timer_id, timer_policy) => { - self.service.set_expired_timer_policy(timer_id, timer_policy)?; - Ok(AcpiTimeAlarmResponse::OkNoData) - } - AcpiTimeAlarmRequest::GetExpiredTimerPolicy(timer_id) => Ok(AcpiTimeAlarmResponse::WakePolicy( - self.service.get_expired_timer_policy(timer_id), - )), - AcpiTimeAlarmRequest::SetTimerValue(timer_id, timer_value) => { - self.service.set_timer_value(timer_id, timer_value)?; - Ok(AcpiTimeAlarmResponse::OkNoData) - } - AcpiTimeAlarmRequest::GetTimerValue(timer_id) => Ok(AcpiTimeAlarmResponse::TimerSeconds( - self.service.get_timer_value(timer_id)?, - )), - } - } -} +pub mod hid; +pub mod mctp; diff --git a/time-alarm-service-relay/src/mctp/mod.rs b/time-alarm-service-relay/src/mctp/mod.rs new file mode 100644 index 000000000..54537954b --- /dev/null +++ b/time-alarm-service-relay/src/mctp/mod.rs @@ -0,0 +1,57 @@ +use super::*; + +mod serialization; +pub use serialization::{AcpiTimeAlarmRequest, AcpiTimeAlarmResponse, AcpiTimeAlarmResult}; + +/// A relay handler that converts MCTP messages into function calls against the time-alarm service. +pub struct TimeAlarmServiceRelayHandler { + service: T, +} + +impl TimeAlarmServiceRelayHandler { + /// Construct a new relay handler that transmits requests to the given time-alarm service. + pub fn new(service: T) -> Self { + Self { service } + } +} + +impl embedded_services::relay::mctp::RelayServiceHandlerTypes for TimeAlarmServiceRelayHandler { + type RequestType = AcpiTimeAlarmRequest; + type ResultType = AcpiTimeAlarmResult; +} + +impl embedded_services::relay::mctp::RelayServiceHandler for TimeAlarmServiceRelayHandler { + async fn process_request(&self, request: Self::RequestType) -> Self::ResultType { + match request { + AcpiTimeAlarmRequest::GetCapabilities => { + Ok(AcpiTimeAlarmResponse::Capabilities(self.service.get_capabilities())) + } + AcpiTimeAlarmRequest::GetRealTime => Ok(AcpiTimeAlarmResponse::RealTime(self.service.get_real_time()?)), + AcpiTimeAlarmRequest::SetRealTime(timestamp) => { + self.service.set_real_time(timestamp)?; + Ok(AcpiTimeAlarmResponse::OkNoData) + } + AcpiTimeAlarmRequest::GetWakeStatus(timer_id) => Ok(AcpiTimeAlarmResponse::TimerStatus( + self.service.get_wake_status(timer_id), + )), + AcpiTimeAlarmRequest::ClearWakeStatus(timer_id) => { + self.service.clear_wake_status(timer_id); + Ok(AcpiTimeAlarmResponse::OkNoData) + } + AcpiTimeAlarmRequest::SetExpiredTimerPolicy(timer_id, timer_policy) => { + self.service.set_expired_timer_policy(timer_id, timer_policy)?; + Ok(AcpiTimeAlarmResponse::OkNoData) + } + AcpiTimeAlarmRequest::GetExpiredTimerPolicy(timer_id) => Ok(AcpiTimeAlarmResponse::WakePolicy( + self.service.get_expired_timer_policy(timer_id), + )), + AcpiTimeAlarmRequest::SetTimerValue(timer_id, timer_value) => { + self.service.set_timer_value(timer_id, timer_value)?; + Ok(AcpiTimeAlarmResponse::OkNoData) + } + AcpiTimeAlarmRequest::GetTimerValue(timer_id) => Ok(AcpiTimeAlarmResponse::TimerSeconds( + self.service.get_timer_value(timer_id)?, + )), + } + } +} diff --git a/time-alarm-service-relay/src/serialization.rs b/time-alarm-service-relay/src/mctp/serialization.rs similarity index 100% rename from time-alarm-service-relay/src/serialization.rs rename to time-alarm-service-relay/src/mctp/serialization.rs From 23aa5ed58bbcb18a8d5e2f90fee52f357ede4a31 Mon Sep 17 00:00:00 2001 From: Billy Price Date: Thu, 1 Oct 2026 10:11:18 -0700 Subject: [PATCH 2/7] fix feature passthrough --- time-alarm-service-relay/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/time-alarm-service-relay/Cargo.toml b/time-alarm-service-relay/Cargo.toml index f168c3086..82767f1c7 100644 --- a/time-alarm-service-relay/Cargo.toml +++ b/time-alarm-service-relay/Cargo.toml @@ -21,6 +21,7 @@ typenum.workspace = true defmt = [ "dep:defmt", "embedded-mcu-hal/defmt", + "embedded-services/defmt", "time-alarm-service-interface/defmt", ] log = ["dep:log", "embedded-services/log"] From 6053f82770c81b2286a862947036ba2a2900ad7e Mon Sep 17 00:00:00 2001 From: Billy Price Date: Thu, 1 Oct 2026 16:43:31 -0700 Subject: [PATCH 3/7] fix null-state handling for time zone --- .../src/hid/SystemWakeTimerAndRtc.wara | 4 +- time-alarm-service-relay/src/hid/mod.rs | 2 +- .../src/hid/serialization.rs | 48 ++++++++++++------- 3 files changed, 34 insertions(+), 20 deletions(-) diff --git a/time-alarm-service-relay/src/hid/SystemWakeTimerAndRtc.wara b/time-alarm-service-relay/src/hid/SystemWakeTimerAndRtc.wara index dc77b4e31..3873b3b55 100644 --- a/time-alarm-service-relay/src/hid/SystemWakeTimerAndRtc.wara +++ b/time-alarm-service-relay/src/hid/SystemWakeTimerAndRtc.wara @@ -254,7 +254,7 @@ usage = ['Generic Desktop', 'System Wake Timer and Real Time Clock'] [[applicationCollection.outputReport.variableItem]] usage = ['Time and Date', 'Time Zone Offset From UTC'] - logicalValueRange = [-720, 840] + logicalValueRange = [-1440, 1440] reportFlags = ['nullstate'] [[applicationCollection.outputReport.variableItem]] @@ -297,7 +297,7 @@ usage = ['Generic Desktop', 'System Wake Timer and Real Time Clock'] [[applicationCollection.inputReport.variableItem]] usage = ['Time and Date', 'Time Zone Offset From UTC'] - logicalValueRange = [-720, 840] + logicalValueRange = [-1440, 1440] reportFlags = ['nullstate'] [[applicationCollection.inputReport.variableItem]] diff --git a/time-alarm-service-relay/src/hid/mod.rs b/time-alarm-service-relay/src/hid/mod.rs index 42cea025e..2f2f2049a 100644 --- a/time-alarm-service-relay/src/hid/mod.rs +++ b/time-alarm-service-relay/src/hid/mod.rs @@ -113,7 +113,7 @@ impl<'s, Service: TimeAlarmService> embedded_services::relay::hid::HidDevice for type OutputReportMaxSize = typenum::U8; type FeatureReportMaxSize = typenum::U1; - const MAX_REPORT_COUNT: u8 = 2; + const MAX_REPORT_COUNT: u8 = 4; const MAX_DESCRIPTOR_LEN: usize = serialization::TIME_ALARM_HID_DESCRIPTOR.len(); fn report_descriptor(&self) -> &HidReportDescriptor<'_> { diff --git a/time-alarm-service-relay/src/hid/serialization.rs b/time-alarm-service-relay/src/hid/serialization.rs index 9e0f5745d..8119876ac 100644 --- a/time-alarm-service-relay/src/hid/serialization.rs +++ b/time-alarm-service-relay/src/hid/serialization.rs @@ -2,6 +2,7 @@ use embedded_mcu_hal::time::{Datetime, DatetimeFields}; use embedded_services::hid_report; use embedded_services::relay::hid::ReportId; use embedded_services::relay::hid::reports; +use time_alarm_service_interface::{AcpiTimeZone, AcpiTimeZoneOffset}; #[allow(dead_code)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] @@ -131,13 +132,13 @@ hid_report! { second: u8 => 6, /// 0 - 999 millisecond: u16 => 10, - /// -720 - 840, minutes from UTC - time_zone: i16 => 11, + /// -1440 - 1440, minutes from UTC + time_zone: i16 => 12, dst_observed: bool => 1, dst_active: bool => 1, current_state: u8 => 2, vendor_current_state: u8 => 4, - _reserved: u8 => 3 + _reserved: u8 => 2 } } @@ -155,7 +156,7 @@ impl GetTimeReport { minute: ts.datetime.minute(), second: ts.datetime.second(), millisecond: (ts.datetime.nanoseconds() / 1_000_000) as u16, - time_zone: i16::from(ts.time_zone), + time_zone: time_zone_to_wire(ts.time_zone), dst_observed: matches!( ts.dst_status, time_alarm_service_interface::AcpiDaylightSavingsTimeStatus::NotAdjusted @@ -278,6 +279,21 @@ pub(crate) fn convert_policy(seconds: u8) -> time_alarm_service_interface::Alarm } } +const NULL_HID_TIME_ZONE: i16 = 2047; +fn time_zone_to_wire(time_zone: AcpiTimeZone) -> i16 { + match time_zone { + AcpiTimeZone::Unknown => NULL_HID_TIME_ZONE, + AcpiTimeZone::MinutesFromUtc(offset) => offset.minutes_from_utc(), + } +} + +fn time_zone_from_wire(time_zone: i16) -> AcpiTimeZone { + match AcpiTimeZoneOffset::new(time_zone) { + Ok(offset) => AcpiTimeZone::MinutesFromUtc(offset), + Err(_) => AcpiTimeZone::Unknown, + } +} + hid_report! { pub(crate) struct SetTimeReport { /// 1900 - 9999 @@ -294,11 +310,10 @@ hid_report! { pub second: u8 => 6, /// 0 - 999 pub millisecond: u16 => 10, - /// -720 - 840, minutes from UTC - pub time_zone: i16 => 11, + /// -1440 - 1440, minutes from UTC + pub time_zone: i16 => 12, pub dst_observed: bool => 1, pub dst_active: bool => 1, - _reserved: u8 => 1, } } @@ -318,7 +333,7 @@ impl TryFrom for time_alarm_service_interface::AcpiTimestamp { second: report.second, nanosecond: report.millisecond as u32 * 1_000_000, })?, - time_zone: time_alarm_service_interface::AcpiTimeZone::try_from(report.time_zone)?, + time_zone: time_zone_from_wire(report.time_zone), dst_status: match (report.dst_observed, report.dst_active) { (false, false) => time_alarm_service_interface::AcpiDaylightSavingsTimeStatus::NotObserved, (true, false) => time_alarm_service_interface::AcpiDaylightSavingsTimeStatus::NotAdjusted, @@ -477,9 +492,9 @@ pub(crate) const TIME_ALARM_HID_DESCRIPTOR: &[u8] = &[ 0x75, 0x0A, // ReportSize(10) 0x91, 0x02, // Output(Data, Variable, Absolute, NoWrap, Linear, PreferredState, NoNullPosition, NonVolatile, BitField) 0x09, 0x10, // UsageId(Time Zone Offset From UTC[0x0010]) - 0x16, 0x30, 0xFD, // LogicalMinimum(-720) - 0x26, 0x48, 0x03, // LogicalMaximum(840) - 0x75, 0x0B, // ReportSize(11) + 0x16, 0x60, 0xFA, // LogicalMinimum(-1,440) + 0x26, 0xA0, 0x05, // LogicalMaximum(1,440) + 0x75, 0x0C, // ReportSize(12) 0x91, 0x42, // Output(Data, Variable, Absolute, NoWrap, Linear, PreferredState, NullState, NonVolatile, BitField) 0x09, 0x11, // UsageId(Daylight Savings Time Observed[0x0011]) 0x09, 0x12, // UsageId(Daylight Savings Time Active[0x0012]) @@ -488,12 +503,11 @@ pub(crate) const TIME_ALARM_HID_DESCRIPTOR: &[u8] = &[ 0x95, 0x02, // ReportCount(2) 0x75, 0x01, // ReportSize(1) 0x91, 0x02, // Output(Data, Variable, Absolute, NoWrap, Linear, PreferredState, NoNullPosition, NonVolatile, BitField) - 0x95, 0x01, // ReportCount(1) - 0x91, 0x03, // Output(Constant, Variable, Absolute, NoWrap, Linear, PreferredState, NoNullPosition, NonVolatile, BitField) 0x85, 0x02, // ReportId(2) 0x09, 0x01, // UsageId(Year[0x0001]) 0x16, 0x6C, 0x07, // LogicalMinimum(1,900) 0x26, 0x0F, 0x27, // LogicalMaximum(9,999) + 0x95, 0x01, // ReportCount(1) 0x75, 0x0E, // ReportSize(14) 0x81, 0x02, // Input(Data, Variable, Absolute, NoWrap, Linear, PreferredState, NoNullPosition, BitField) 0x09, 0x02, // UsageId(Month[0x0002]) @@ -521,9 +535,9 @@ pub(crate) const TIME_ALARM_HID_DESCRIPTOR: &[u8] = &[ 0x75, 0x0A, // ReportSize(10) 0x81, 0x02, // Input(Data, Variable, Absolute, NoWrap, Linear, PreferredState, NoNullPosition, BitField) 0x09, 0x10, // UsageId(Time Zone Offset From UTC[0x0010]) - 0x16, 0x30, 0xFD, // LogicalMinimum(-720) - 0x26, 0x48, 0x03, // LogicalMaximum(840) - 0x75, 0x0B, // ReportSize(11) + 0x16, 0x60, 0xFA, // LogicalMinimum(-1,440) + 0x26, 0xA0, 0x05, // LogicalMaximum(1,440) + 0x75, 0x0C, // ReportSize(12) 0x81, 0x42, // Input(Data, Variable, Absolute, NoWrap, Linear, PreferredState, NullState, BitField) 0x09, 0x11, // UsageId(Daylight Savings Time Observed[0x0011]) 0x09, 0x12, // UsageId(Daylight Savings Time Active[0x0012]) @@ -548,7 +562,7 @@ pub(crate) const TIME_ALARM_HID_DESCRIPTOR: &[u8] = &[ 0x25, 0x0F, // LogicalMaximum(15) 0x75, 0x04, // ReportSize(4) 0x81, 0x02, // Input(Data, Variable, Absolute, NoWrap, Linear, PreferredState, NoNullPosition, BitField) - 0x75, 0x03, // ReportSize(3) + 0x75, 0x02, // ReportSize(2) 0x81, 0x03, // Input(Constant, Variable, Absolute, NoWrap, Linear, PreferredState, NoNullPosition, BitField) 0xC0, // EndCollection() ]; From 8f0a33ec612210d9c2510f7c5059fab73429f978 Mon Sep 17 00:00:00 2001 From: Billy Price Date: Fri, 2 Oct 2026 12:28:52 -0700 Subject: [PATCH 4/7] drop hidreport macro --- Cargo.lock | 1 + embedded-service/src/relay/hid/mod.rs | 4 - embedded-service/src/relay/hid/reports.rs | 480 ------------------ examples/rt685s-evk/Cargo.lock | 1 + time-alarm-service-relay/Cargo.toml | 1 + time-alarm-service-relay/src/hid/mod.rs | 17 +- .../src/hid/serialization.rs | 331 +++++++----- 7 files changed, 213 insertions(+), 622 deletions(-) delete mode 100644 embedded-service/src/relay/hid/reports.rs diff --git a/Cargo.lock b/Cargo.lock index 0feb65ea6..636a191fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2039,6 +2039,7 @@ dependencies = [ name = "time-alarm-service-relay" version = "0.1.0" dependencies = [ + "bitfield 0.17.0", "defmt 0.3.100", "embedded-mcu-hal", "embedded-services", diff --git a/embedded-service/src/relay/hid/mod.rs b/embedded-service/src/relay/hid/mod.rs index 854cb6644..ff3acae74 100644 --- a/embedded-service/src/relay/hid/mod.rs +++ b/embedded-service/src/relay/hid/mod.rs @@ -3,10 +3,6 @@ use generic_array::ArrayLength; use num_enum::TryFromPrimitive; -pub mod reports; - -pub use crate::hid_report; - /// Errors that a HID device operation can fail with. /// /// Reporting failure triggers a device-initiated reset, so callers must handle these errors explicitly diff --git a/embedded-service/src/relay/hid/reports.rs b/embedded-service/src/relay/hid/reports.rs deleted file mode 100644 index c5c2d1b5f..000000000 --- a/embedded-service/src/relay/hid/reports.rs +++ /dev/null @@ -1,480 +0,0 @@ -//! Bit-level packing and unpacking helper for HID reports. -//! -//! HID lays report fields out as a little-endian bit stream: the first field starts at bit 0 of -//! byte 0, each subsequent field starts where the previous one ended, and fields straddle -//! byte boundaries. Signed fields are also supported, and accessing them requires doing sign extension. -//! This means that for reports that don't happen to be byte-aligned (e.g. boot protocol mouse/keyboard reports), -//! #[repr(C)] structs aren't sufficient to handle reports and you need to do some bit-level packing and unpacking manually. -//! -//! This module provides the following utilities for packing/unpacking reports that can't just be #[repr(C)] structs: -//! -//! * [`get_bits`] / [`set_bits`] for ad-hoc access to a bit range in a buffer. -//! * [`hid_report!`](crate::hid_report) to declare a report as a normal Rust struct with per-field -//! bit widths, generating `pack`/`pack_into`/`unpack`. -//! -//! Report IDs are *not* handled here; strip or prepend the ID byte in the caller. -//! -//! ```ignore -//! hid_report! { -//! /// Report ID 3 (Time and Date), output direction. -//! pub struct SetTimeReport { -//! /// 1900 - 9999 -//! pub year: u16 => 14, -//! /// 1 - 12 -//! pub month: u8 => 4, -//! /// 1 - 31 -//! pub day: u8 => 5, -//! /// 0 - 23 -//! pub hour: u8 => 5, -//! /// 0 - 59 -//! pub minute: u8 => 6, -//! /// 0 - 59 -//! pub second: u8 => 6, -//! /// 0 - 999 -//! pub millisecond: u16 => 10, -//! /// -720 - 840, minutes from UTC -//! pub time_zone: i16 => 11, -//! pub dst_observed: bool => 1, -//! pub dst_active: bool => 1, -//! pub reserved: u8 => 1, -//! } -//! } -//! -//! let report = SetTimeReport::unpack(payload)?; -//! let bytes = report.pack()?; -//! ``` - -/// Failure modes for packing and unpacking report fields. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum ReportError { - /// The requested bit range extends past the end of the buffer. - OutOfBounds, - - /// The requested width is zero, wider than 32 bits, or wider than the field's storage type. - InvalidWidth, - - /// The value does not fit in the requested number of bits. - ValueOutOfRange, -} - -/// Mask with the low `width` bits set. -const fn mask(width: u32) -> u32 { - if width >= u32::BITS { - u32::MAX - } else { - (1u32 << width) - 1 - } -} - -/// Interprets the low `width` bits of `raw` as a two's-complement signed value. -const fn sign_extend(raw: u32, width: u32) -> i32 { - if width >= u32::BITS { - raw as i32 - } else { - let shift = u32::BITS - width; - ((raw << shift) as i32) >> shift - } -} - -/// Inclusive range representable by a two's-complement field of `width` bits. `width` must be 1..=32. -const fn signed_range(width: u32) -> (i32, i32) { - if width >= u32::BITS { - (i32::MIN, i32::MAX) - } else { - let magnitude = 1i32 << (width - 1); - (-magnitude, magnitude - 1) - } -} - -fn check_bounds(data_len: usize, offset: usize, width: u32) -> Result<(), ReportError> { - if width == 0 || width > u32::BITS { - return Err(ReportError::InvalidWidth); - } - - let end = offset.checked_add(width as usize).ok_or(ReportError::OutOfBounds)?; - if end.div_ceil(8) > data_len { - return Err(ReportError::OutOfBounds); - } - - Ok(()) -} - -/// Reads `width` bits starting at bit `offset` (LSB of byte 0 is bit 0), returned right-aligned. -pub fn get_bits(data: &[u8], offset: usize, width: u32) -> Result { - check_bounds(data.len(), offset, width)?; - - let mut value = 0u32; - for bit in 0..width as usize { - let pos = offset + bit; - let byte = *data.get(pos / 8).ok_or(ReportError::OutOfBounds)?; - if byte & (1u8 << (pos % 8)) != 0 { - value |= 1u32 << bit; - } - } - - Ok(value) -} - -/// Writes the low `width` bits of `value` starting at bit `offset`. Bits outside the range are left alone. -pub fn set_bits(data: &mut [u8], offset: usize, width: u32, value: u32) -> Result<(), ReportError> { - check_bounds(data.len(), offset, width)?; - - if value & !mask(width) != 0 { - return Err(ReportError::ValueOutOfRange); - } - - for bit in 0..width as usize { - let pos = offset + bit; - let byte = data.get_mut(pos / 8).ok_or(ReportError::OutOfBounds)?; - let bit_mask = 1u8 << (pos % 8); - if value & (1u32 << bit) != 0 { - *byte |= bit_mask; - } else { - *byte &= !bit_mask; - } - } - - Ok(()) -} - -/// A type that can be stored in a HID report bitfield. -pub trait ReportField: Copy + Sized { - /// Width of the type this field is stored in; a field may not be declared wider than this. - const STORAGE_BITS: u32; - - /// Converts the right-aligned raw bits of a `width`-bit field into this type. - fn from_bits(raw: u32, width: u32) -> Result; - - /// Converts this value into the right-aligned raw bits of a `width`-bit field. - fn to_bits(self, width: u32) -> Result; -} - -fn check_width(width: u32) -> Result<(), ReportError> { - if width == 0 || width > u32::BITS || width > T::STORAGE_BITS { - Err(ReportError::InvalidWidth) - } else { - Ok(()) - } -} - -macro_rules! impl_unsigned_field { - ($($ty:ty),* $(,)?) => { - $( - impl ReportField for $ty { - const STORAGE_BITS: u32 = <$ty>::BITS; - - fn from_bits(raw: u32, width: u32) -> Result { - check_width::(width)?; - Ok((raw & mask(width)) as $ty) - } - - fn to_bits(self, width: u32) -> Result { - check_width::(width)?; - let value = u32::from(self); - if value & !mask(width) != 0 { - return Err(ReportError::ValueOutOfRange); - } - Ok(value) - } - } - )* - }; -} - -macro_rules! impl_signed_field { - ($($ty:ty),* $(,)?) => { - $( - impl ReportField for $ty { - const STORAGE_BITS: u32 = <$ty>::BITS; - - fn from_bits(raw: u32, width: u32) -> Result { - check_width::(width)?; - Ok(sign_extend(raw, width) as $ty) - } - - fn to_bits(self, width: u32) -> Result { - check_width::(width)?; - let value = i32::from(self); - let (min, max) = signed_range(width); - if value < min || value > max { - return Err(ReportError::ValueOutOfRange); - } - Ok((value as u32) & mask(width)) - } - } - )* - }; -} - -impl_unsigned_field!(u8, u16, u32); -impl_signed_field!(i8, i16, i32); - -impl ReportField for bool { - const STORAGE_BITS: u32 = 1; - - fn from_bits(raw: u32, width: u32) -> Result { - check_width::(width)?; - Ok(raw & 1 != 0) - } - - fn to_bits(self, width: u32) -> Result { - check_width::(width)?; - Ok(u32::from(self)) - } -} - -/// Declares a HID report struct and generates its bit-level (de)serialization. -/// -/// Fields are laid out LSB-first in declaration order, matching the order of `Input`/`Output`/ -/// `Feature` items in the report descriptor. Every field of the descriptor must be declared, -/// including constant/padding items, so that the offsets line up. -/// -/// Field syntax is `name: storage_type => bit_width`. Supported storage types are `u8`, `u16`, -/// `u32`, `i8`, `i16`, `i32` (two's complement) and `bool` (exactly one bit). -/// -/// The declared widths must sum to a whole number of bytes, which is checked at compile time; -/// declare the report descriptor's constant/padding items to make up any shortfall. -/// -/// The Windows HID APIs don't support field sizes larger than 32 bits, so if you need larger values, -/// you have to split them into multiple fields. -#[macro_export] -macro_rules! hid_report { - ( - $(#[$meta:meta])* - $vis:vis struct $name:ident { - $( - $(#[$field_meta:meta])* - $field_vis:vis $field:ident : $ty:ty => $bits:expr - ),* $(,)? - } - ) => { - $(#[$meta])* - #[derive(Clone, Copy, Debug, Default, PartialEq)] - #[cfg_attr(feature = "defmt", derive(defmt::Format))] - #[allow(dead_code)] - $vis struct $name { - $( - $(#[$field_meta])* - $field_vis $field: $ty, - )* - } - - #[allow(dead_code)] - impl $name { - /// Total width of the report in bits, including padding fields. - pub const BIT_LEN: usize = 0 $( + ($bits as usize) )*; - - /// Size of the packed report in bytes, excluding any report ID prefix. - pub const BYTE_LEN: usize = $name::BIT_LEN.div_ceil(8); - - /// Bit offset of each field, in declaration order. - #[allow(dead_code, clippy::indexing_slicing)] - pub const FIELD_OFFSETS: [usize; 0 $( + { let _ = $bits; 1 } )*] = { - let mut offsets = [0usize; 0 $( + { let _ = $bits; 1 } )*]; - let mut offset = 0usize; - let mut index = 0usize; - $( - offsets[index] = offset; - offset += ($bits as usize); - index += 1; - )* - let _ = (offset, index); - offsets - }; - - /// Packs the report into a new buffer. - pub fn pack(&self) -> Result<[u8; $name::BYTE_LEN], $crate::relay::hid::reports::ReportError> { - let mut buffer = [0u8; $name::BYTE_LEN]; - self.pack_into(&mut buffer)?; - Ok(buffer) - } - - /// Packs the report into the start of `buffer`, returning the number of bytes written. - /// - /// Bytes past the end of the report are left untouched. - pub fn pack_into(&self, buffer: &mut [u8]) -> Result { - #[allow(unused_mut, unused_variables)] - let mut offset: usize = 0; - $( - $crate::relay::hid::reports::set_bits( - buffer, - offset, - $bits, - $crate::relay::hid::reports::ReportField::to_bits(self.$field, $bits)?, - )?; - offset += ($bits as usize); - )* - let _ = offset; - Ok($name::BYTE_LEN) - } - - /// Unpacks the report from the start of `buffer`; trailing bytes are ignored. - pub fn unpack(buffer: &[u8]) -> Result { - #[allow(unused_mut, unused_variables)] - let mut offset: usize = 0; - $( - let $field = <$ty as $crate::relay::hid::reports::ReportField>::from_bits( - $crate::relay::hid::reports::get_bits(buffer, offset, $bits)?, - $bits, - )?; - offset += ($bits as usize); - )* - let _ = offset; - Ok(Self { $( $field, )* }) - } - } - - const _: () = assert!( - $name::BIT_LEN % 8 == 0, - concat!( - stringify!($name), - ": HID report field widths must sum to a whole number of bytes - declare the \ - descriptor's constant/padding items to make up the difference", - ), - ); - }; -} - -#[cfg(test)] -#[allow(clippy::unwrap_used, clippy::indexing_slicing)] -mod tests { - use super::*; - - crate::hid_report! { - pub struct TimeReport { - pub year: u16 => 14, - pub month: u8 => 4, - pub day: u8 => 5, - pub hour: u8 => 5, - pub minute: u8 => 6, - pub second: u8 => 6, - pub millisecond: u16 => 10, - pub time_zone: i16 => 11, - pub dst_observed: bool => 1, - pub dst_active: bool => 1, - pub reserved: u8 => 1, - } - } - - #[test] - fn report_geometry() { - assert_eq!(TimeReport::BIT_LEN, 64); - assert_eq!(TimeReport::BYTE_LEN, 8); - assert_eq!(TimeReport::FIELD_OFFSETS, [0, 14, 18, 23, 28, 34, 40, 50, 61, 62, 63]); - } - - #[test] - fn round_trip() { - let report = TimeReport { - year: 2026, - month: 9, - day: 1, - hour: 13, - minute: 45, - second: 59, - millisecond: 999, - time_zone: -480, - dst_observed: true, - dst_active: false, - reserved: 0, - }; - - let packed = report.pack().unwrap(); - assert_eq!(TimeReport::unpack(&packed).unwrap(), report); - } - - #[test] - fn unpack_ignores_trailing_bytes() { - let report = TimeReport { - year: 1900, - time_zone: 840, - ..Default::default() - }; - - let mut buffer = [0xAAu8; 12]; - assert_eq!(report.pack_into(&mut buffer).unwrap(), 8); - assert_eq!(TimeReport::unpack(&buffer).unwrap(), report); - assert_eq!(buffer[8..], [0xAA; 4]); - } - - #[test] - fn straddles_byte_boundaries() { - // year (14 bits) then month (4 bits) starting mid-byte. - let report = TimeReport { - year: 0x3FFF, - month: 0xF, - ..Default::default() - }; - - let packed = report.pack().unwrap(); - assert_eq!(packed[0], 0xFF); - assert_eq!(packed[1], 0xFF); - assert_eq!(packed[2], 0x03); - } - - #[test] - fn rejects_oversized_values() { - let report = TimeReport { - month: 16, - ..Default::default() - }; - assert_eq!(report.pack(), Err(ReportError::ValueOutOfRange)); - - let report = TimeReport { - time_zone: 1024, - ..Default::default() - }; - assert_eq!(report.pack(), Err(ReportError::ValueOutOfRange)); - } - - #[test] - fn rejects_short_buffers() { - let mut buffer = [0u8; 7]; - assert_eq!( - TimeReport::default().pack_into(&mut buffer), - Err(ReportError::OutOfBounds) - ); - assert_eq!(TimeReport::unpack(&buffer), Err(ReportError::OutOfBounds)); - } - - #[test] - fn bit_accessors() { - let data = [0b1010_0101u8, 0b0000_0011]; - assert_eq!(get_bits(&data, 0, 4).unwrap(), 0b0101); - assert_eq!(get_bits(&data, 4, 4).unwrap(), 0b1010); - assert_eq!(get_bits(&data, 6, 4).unwrap(), 0b1110); - assert_eq!(get_bits(&data, 0, 16).unwrap(), 0x03A5); - - let mut data = [0u8; 2]; - set_bits(&mut data, 6, 4, 0b1110).unwrap(); - assert_eq!(data, [0b1000_0000, 0b0000_0011]); - set_bits(&mut data, 6, 4, 0).unwrap(); - assert_eq!(data, [0, 0]); - } - - #[test] - fn bit_accessor_errors() { - let mut data = [0u8; 2]; - assert_eq!(get_bits(&data, 0, 0), Err(ReportError::InvalidWidth)); - assert_eq!(get_bits(&data, 0, 33), Err(ReportError::InvalidWidth)); - assert_eq!(get_bits(&data, 9, 8), Err(ReportError::OutOfBounds)); - assert_eq!(set_bits(&mut data, 0, 4, 0x10), Err(ReportError::ValueOutOfRange)); - } - - #[test] - fn signed_fields_sign_extend() { - assert_eq!(::from_bits(0b111_1010_0000, 11).unwrap(), -96); - assert_eq!(::to_bits(-96, 11).unwrap(), 0b111_1010_0000); - assert_eq!( - ::to_bits(-1025, 11), - Err(ReportError::ValueOutOfRange) - ); - } - - #[test] - fn width_must_fit_storage_type() { - assert_eq!(::from_bits(0, 9), Err(ReportError::InvalidWidth)); - assert_eq!(::to_bits(true, 2), Err(ReportError::InvalidWidth)); - } -} diff --git a/examples/rt685s-evk/Cargo.lock b/examples/rt685s-evk/Cargo.lock index 40401e58e..f00fb2786 100644 --- a/examples/rt685s-evk/Cargo.lock +++ b/examples/rt685s-evk/Cargo.lock @@ -1267,6 +1267,7 @@ dependencies = [ name = "time-alarm-service-relay" version = "0.1.0" dependencies = [ + "bitfield 0.17.0", "defmt 0.3.100", "embedded-mcu-hal", "embedded-services", diff --git a/time-alarm-service-relay/Cargo.toml b/time-alarm-service-relay/Cargo.toml index 82767f1c7..790d714af 100644 --- a/time-alarm-service-relay/Cargo.toml +++ b/time-alarm-service-relay/Cargo.toml @@ -9,6 +9,7 @@ repository.workspace = true ignored = ["log"] [dependencies] +bitfield.workspace = true defmt = { workspace = true, optional = true } log = { workspace = true, optional = true } embedded-services.workspace = true diff --git a/time-alarm-service-relay/src/hid/mod.rs b/time-alarm-service-relay/src/hid/mod.rs index 2f2f2049a..b6e116e30 100644 --- a/time-alarm-service-relay/src/hid/mod.rs +++ b/time-alarm-service-relay/src/hid/mod.rs @@ -3,7 +3,6 @@ use embedded_services::relay::hid::HidError; use embedded_services::relay::hid::{ GetHidReport, GetHidReportType, HidDevicePowerState, HidReport, HidReportDescriptor, ReportId, SetHidReport, - reports, }; use embedded_services::{error, info}; use time_alarm_service_interface::{AcpiTimerId, AlarmTimerSeconds, TimeAlarmService}; @@ -26,8 +25,8 @@ enum SetReportError { Service, } -impl From for SetReportError { - fn from(_: reports::ReportError) -> Self { +impl From for SetReportError { + fn from(_: serialization::ReportError) -> Self { Self::Malformed } } @@ -80,7 +79,7 @@ impl<'s, Service: TimeAlarmService> TimeAlarmHidRelay<'s, Service> { info!("Parsed SetAlarmReport: {:?}", set_alarm); self.service - .set_timer_value(timer_id, serialization::convert_timer(set_alarm.timer_seconds))?; + .set_timer_value(timer_id, serialization::convert_timer(set_alarm.timer_seconds()))?; Ok(()) } @@ -89,7 +88,7 @@ impl<'s, Service: TimeAlarmService> TimeAlarmHidRelay<'s, Service> { let set_debounce = serialization::SetDebounceReport::unpack(data)?; info!("Parsed SetDebounceReport: {:?}", set_debounce); - let wake_policy = serialization::convert_policy(set_debounce.power_source_change_debounce_seconds); + let wake_policy = serialization::convert_policy(set_debounce.power_source_change_debounce_seconds()); self.service .set_expired_timer_policy(AcpiTimerId::AcPower, wake_policy)?; self.service @@ -163,7 +162,7 @@ impl<'s, Service: TimeAlarmService> embedded_services::relay::hid::HidDevice for let vendor_current_state = 0; let report_payload = GetAlarmReport::new(ac_timer, dc_timer, wake_policy, current_state, vendor_current_state); - let report = report_payload.pack().map_err(|_| HidError::TriggerReset)?; + let report = report_payload.pack(); let report = HidReport::new(report_id, &report); Ok(process_report(GetHidReport::Input(report)).await) } @@ -179,7 +178,7 @@ impl<'s, Service: TimeAlarmService> embedded_services::relay::hid::HidDevice for serialization::GetTimeReport::failed(0) } }; - let report = report_payload.pack().map_err(|_| HidError::TriggerReset)?; + let report = report_payload.pack(); let report = HidReport::new(report_id, &report); Ok(process_report(GetHidReport::Input(report)).await) } @@ -196,9 +195,7 @@ impl<'s, Service: TimeAlarmService> embedded_services::relay::hid::HidDevice for } else { serialization::PowerState::S3 }; - let capabilities = serialization::CapabilitiesFeatureReport::new(deepest_power_state) - .pack() - .map_err(|_| HidError::TriggerReset)?; + let capabilities = serialization::CapabilitiesFeatureReport::new(deepest_power_state).pack(); Ok(process_report(GetHidReport::Feature(HidReport::new(report_id, &capabilities))).await) } } diff --git a/time-alarm-service-relay/src/hid/serialization.rs b/time-alarm-service-relay/src/hid/serialization.rs index 8119876ac..2cfac71f6 100644 --- a/time-alarm-service-relay/src/hid/serialization.rs +++ b/time-alarm-service-relay/src/hid/serialization.rs @@ -1,19 +1,25 @@ +use bitfield::bitfield; use embedded_mcu_hal::time::{Datetime, DatetimeFields}; -use embedded_services::hid_report; use embedded_services::relay::hid::ReportId; -use embedded_services::relay::hid::reports; use time_alarm_service_interface::{AcpiTimeZone, AcpiTimeZoneOffset}; +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub(crate) enum ReportError { + InvalidLength, + ValueOutOfRange, +} + #[allow(dead_code)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub(crate) enum TimestampConversionError { - Report(reports::ReportError), + Report(ReportError), Datetime(embedded_mcu_hal::time::DatetimeError), DatetimeClock(embedded_mcu_hal::time::DatetimeClockError), } -impl From for TimestampConversionError { - fn from(err: reports::ReportError) -> Self { +impl From for TimestampConversionError { + fn from(err: ReportError) -> Self { Self::Report(err) } } @@ -65,17 +71,16 @@ impl TryFrom for InputReportId { } } -hid_report! { - pub(crate) struct GetAlarmReport { - ac_timer: u32 => 31, - _padding1: u8 => 1, - dc_timer: u32 => 31, - power_source_change_debounce_seconds: u8 => 6, - current_state: u8 => 3, - vendor_current_state: u8 => 4, - _padding: u8 => 4 - // Define the fields for the GetAlarmReport here - } +bitfield! { + #[derive(Clone, Copy, PartialEq, Eq)] + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + pub(crate) struct GetAlarmReport([u8]); + impl Debug; + u32, ac_timer, set_ac_timer: 30, 0; + u32, dc_timer, set_dc_timer: 62, 32; + u8, power_source_change_debounce_seconds, set_power_source_change_debounce_seconds: 68, 63; + u8, current_state, set_current_state: 71, 69; + u8, vendor_current_state, set_vendor_current_state: 75, 72; } #[derive(Clone, Copy, Debug, PartialEq, Eq, num_enum::IntoPrimitive, num_enum::TryFromPrimitive)] @@ -89,7 +94,7 @@ pub(crate) enum AlarmCurrentState { Signaled = 5, } -impl GetAlarmReport { +impl GetAlarmReport<[u8; 10]> { pub(crate) fn new( ac_timer: time_alarm_service_interface::AlarmTimerSeconds, dc_timer: time_alarm_service_interface::AlarmTimerSeconds, @@ -97,15 +102,17 @@ impl GetAlarmReport { current_state: AlarmCurrentState, vendor_current_state: u8, ) -> Self { - Self { - ac_timer: convert_timer_to_wire(ac_timer), - _padding1: 0, - dc_timer: convert_timer_to_wire(dc_timer), - power_source_change_debounce_seconds: convert_policy_to_wire(power_policy), - current_state: current_state.into(), - vendor_current_state, - _padding: 0, - } + let mut report = Self([0; 10]); + report.set_ac_timer(convert_timer_to_wire(ac_timer)); + report.set_dc_timer(convert_timer_to_wire(dc_timer)); + report.set_power_source_change_debounce_seconds(convert_policy_to_wire(power_policy)); + report.set_current_state(current_state.into()); + report.set_vendor_current_state(vendor_current_state); + report + } + + pub(crate) fn pack(self) -> [u8; 10] { + self.0 } } @@ -116,69 +123,64 @@ pub(crate) enum TimeCurrentState { Running = 2, } -hid_report! { - pub(crate) struct GetTimeReport { - /// 1900 - 9999 - year: u16 => 14, - /// 1 - 12 - month: u8 => 4, - /// 1 - 31 - day: u8 => 5, - /// 0 - 23 - hour: u8 => 5, - /// 0 - 59 - minute: u8 => 6, - /// 0 - 59 - second: u8 => 6, - /// 0 - 999 - millisecond: u16 => 10, - /// -1440 - 1440, minutes from UTC - time_zone: i16 => 12, - dst_observed: bool => 1, - dst_active: bool => 1, - current_state: u8 => 2, - vendor_current_state: u8 => 4, - _reserved: u8 => 2 - } +bitfield! { + #[derive(Clone, Copy, PartialEq, Eq)] + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + pub(crate) struct GetTimeReport([u8]); + impl Debug; + u16, year, set_year: 13, 0; + u8, month, set_month: 17, 14; + u8, day, set_day: 22, 18; + u8, hour, set_hour: 27, 23; + u8, minute, set_minute: 33, 28; + u8, second, set_second: 39, 34; + u16, millisecond, set_millisecond: 49, 40; + i16, time_zone, set_time_zone: 61, 50; + dst_observed, set_dst_observed: 62; + dst_active, set_dst_active: 63; + u8, current_state, set_current_state: 65, 64; + u8, vendor_current_state, set_vendor_current_state: 69, 66; } -impl GetTimeReport { +impl GetTimeReport<[u8; 9]> { pub(crate) fn new( ts: time_alarm_service_interface::AcpiTimestamp, current_state: TimeCurrentState, vendor_state: u8, ) -> Self { - Self { - year: ts.datetime.year(), - month: ts.datetime.month().into(), - day: ts.datetime.day(), - hour: ts.datetime.hour(), - minute: ts.datetime.minute(), - second: ts.datetime.second(), - millisecond: (ts.datetime.nanoseconds() / 1_000_000) as u16, - time_zone: time_zone_to_wire(ts.time_zone), - dst_observed: matches!( - ts.dst_status, - time_alarm_service_interface::AcpiDaylightSavingsTimeStatus::NotAdjusted - | time_alarm_service_interface::AcpiDaylightSavingsTimeStatus::Adjusted - ), - dst_active: matches!( - ts.dst_status, - time_alarm_service_interface::AcpiDaylightSavingsTimeStatus::Adjusted - ), - current_state: current_state.into(), - vendor_current_state: vendor_state, - _reserved: 0, - } + let mut report = Self([0; 9]); + report.set_year(ts.datetime.year()); + report.set_month(ts.datetime.month().into()); + report.set_day(ts.datetime.day()); + report.set_hour(ts.datetime.hour()); + report.set_minute(ts.datetime.minute()); + report.set_second(ts.datetime.second()); + report.set_millisecond((ts.datetime.nanoseconds() / 1_000_000) as u16); + report.set_time_zone(time_zone_to_wire(ts.time_zone)); + report.set_dst_observed(matches!( + ts.dst_status, + time_alarm_service_interface::AcpiDaylightSavingsTimeStatus::NotAdjusted + | time_alarm_service_interface::AcpiDaylightSavingsTimeStatus::Adjusted + )); + report.set_dst_active(matches!( + ts.dst_status, + time_alarm_service_interface::AcpiDaylightSavingsTimeStatus::Adjusted + )); + report.set_current_state(current_state.into()); + report.set_vendor_current_state(vendor_state); + report } /// Report used when the current time is unavailable; the time fields carry no meaning. pub(crate) fn failed(vendor_state: u8) -> Self { - Self { - current_state: TimeCurrentState::Failed.into(), - vendor_current_state: vendor_state, - ..Default::default() - } + let mut report = Self([0; 9]); + report.set_current_state(TimeCurrentState::Failed.into()); + report.set_vendor_current_state(vendor_state); + report + } + + pub(crate) fn pack(self) -> [u8; 9] { + self.0 } } @@ -202,17 +204,37 @@ impl TryFrom for OutputReportId { } } -hid_report! { - pub(crate) struct SetAlarmReport { - pub timer_seconds: u32 => 31, - _padding: u8 => 1 +bitfield! { + #[derive(Clone, Copy, PartialEq, Eq)] + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + pub(crate) struct SetAlarmReport([u8]); + impl Debug; + pub u32, timer_seconds, _: 30, 0; +} + +impl<'a> SetAlarmReport<&'a [u8]> { + pub(crate) fn unpack(data: &'a [u8]) -> Result { + if data.len() < 4 { + return Err(ReportError::InvalidLength); + } + Ok(Self(data)) } } -hid_report! { - pub(crate) struct SetDebounceReport { - pub power_source_change_debounce_seconds: u8 => 6, - _padding: u8 => 2 +bitfield! { + #[derive(Clone, Copy, PartialEq, Eq)] + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + pub(crate) struct SetDebounceReport([u8]); + impl Debug; + pub u8, power_source_change_debounce_seconds, _: 5, 0; +} + +impl<'a> SetDebounceReport<&'a [u8]> { + pub(crate) fn unpack(data: &'a [u8]) -> Result { + if data.is_empty() { + return Err(ReportError::InvalidLength); + } + Ok(Self(data)) } } @@ -294,52 +316,55 @@ fn time_zone_from_wire(time_zone: i16) -> AcpiTimeZone { } } -hid_report! { - pub(crate) struct SetTimeReport { - /// 1900 - 9999 - pub year: u16 => 14, - /// 1 - 12 - pub month: u8 => 4, - /// 1 - 31 - pub day: u8 => 5, - /// 0 - 23 - pub hour: u8 => 5, - /// 0 - 59 - pub minute: u8 => 6, - /// 0 - 59 - pub second: u8 => 6, - /// 0 - 999 - pub millisecond: u16 => 10, - /// -1440 - 1440, minutes from UTC - pub time_zone: i16 => 12, - pub dst_observed: bool => 1, - pub dst_active: bool => 1, +bitfield! { + #[derive(Clone, Copy, PartialEq, Eq)] + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + pub(crate) struct SetTimeReport([u8]); + impl Debug; + pub u16, year, _: 13, 0; + pub u8, month, _: 17, 14; + pub u8, day, _: 22, 18; + pub u8, hour, _: 27, 23; + pub u8, minute, _: 33, 28; + pub u8, second, _: 39, 34; + pub u16, millisecond, _: 49, 40; + pub i16, time_zone, _: 61, 50; + pub dst_observed, _: 62; + pub dst_active, _: 63; +} + +impl<'a> SetTimeReport<&'a [u8]> { + pub(crate) fn unpack(data: &'a [u8]) -> Result { + if data.len() < 8 { + return Err(ReportError::InvalidLength); + } + Ok(Self(data)) } } -impl TryFrom for time_alarm_service_interface::AcpiTimestamp { +impl> TryFrom> for time_alarm_service_interface::AcpiTimestamp { type Error = TimestampConversionError; - fn try_from(report: SetTimeReport) -> Result { + fn try_from(report: SetTimeReport) -> Result { Ok(Self { datetime: Datetime::new(DatetimeFields { - year: report.year, + year: report.year(), month: report - .month + .month() .try_into() .map_err(|_| TimestampConversionError::Datetime(embedded_mcu_hal::time::DatetimeError::Month))?, - day: report.day, - hour: report.hour, - minute: report.minute, - second: report.second, - nanosecond: report.millisecond as u32 * 1_000_000, + day: report.day(), + hour: report.hour(), + minute: report.minute(), + second: report.second(), + nanosecond: u32::from(report.millisecond()) * 1_000_000, })?, - time_zone: time_zone_from_wire(report.time_zone), - dst_status: match (report.dst_observed, report.dst_active) { + time_zone: time_zone_from_wire(report.time_zone()), + dst_status: match (report.dst_observed(), report.dst_active()) { (false, false) => time_alarm_service_interface::AcpiDaylightSavingsTimeStatus::NotObserved, (true, false) => time_alarm_service_interface::AcpiDaylightSavingsTimeStatus::NotAdjusted, (true, true) => time_alarm_service_interface::AcpiDaylightSavingsTimeStatus::Adjusted, (false, true) => { - return Err(TimestampConversionError::Report(reports::ReportError::ValueOutOfRange)); + return Err(TimestampConversionError::Report(ReportError::ValueOutOfRange)); } }, }) @@ -370,19 +395,23 @@ pub(crate) enum PowerState { S5 = 3, } -hid_report! { - pub(crate) struct CapabilitiesFeatureReport { - pub power_state: u8 => 2, // Type: PowerState - _padding: u8 => 6 - } +bitfield! { + #[derive(Clone, Copy, PartialEq, Eq)] + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + pub(crate) struct CapabilitiesFeatureReport([u8]); + impl Debug; + pub u8, power_state, set_power_state: 1, 0; } -impl CapabilitiesFeatureReport { +impl CapabilitiesFeatureReport<[u8; 1]> { pub(crate) fn new(power_state: PowerState) -> Self { - Self { - power_state: power_state.into(), - _padding: 0, - } + let mut report = Self([0]); + report.set_power_state(power_state.into()); + report + } + + pub(crate) fn pack(self) -> [u8; 1] { + self.0 } } @@ -566,3 +595,49 @@ pub(crate) const TIME_ALARM_HID_DESCRIPTOR: &[u8] = &[ 0x81, 0x03, // Input(Constant, Variable, Absolute, NoWrap, Linear, PreferredState, NoNullPosition, BitField) 0xC0, // EndCollection() ]; + +#[cfg(test)] +#[allow(clippy::unwrap_used)] +mod tests { + use super::*; + + #[test] + fn get_alarm_report_matches_hid_bit_layout() { + let report = GetAlarmReport::new( + time_alarm_service_interface::AlarmTimerSeconds(0x1234_5678), + time_alarm_service_interface::AlarmTimerSeconds(0x2345_6789), + time_alarm_service_interface::AlarmExpiredWakePolicy(42), + AlarmCurrentState::Running, + 10, + ); + + assert_eq!( + report.pack(), + [0x78, 0x56, 0x34, 0x12, 0x89, 0x67, 0x45, 0x23, 0x75, 0x0A] + ); + } + + #[test] + fn set_time_report_matches_hid_bit_layout() { + let data = [0xEA, 0x47, 0x86, 0xD6, 0xEE, 0xE7, 0x83, 0x78]; + let report = SetTimeReport::unpack(&data).unwrap(); + + assert_eq!(report.year(), 2026); + assert_eq!(report.month(), 9); + assert_eq!(report.day(), 1); + assert_eq!(report.hour(), 13); + assert_eq!(report.minute(), 45); + assert_eq!(report.second(), 59); + assert_eq!(report.millisecond(), 999); + assert_eq!(report.time_zone(), -480); + assert!(report.dst_observed()); + assert!(!report.dst_active()); + } + + #[test] + fn unpack_rejects_short_reports() { + assert_eq!(SetAlarmReport::unpack(&[0; 3]), Err(ReportError::InvalidLength)); + assert_eq!(SetDebounceReport::unpack(&[]), Err(ReportError::InvalidLength)); + assert_eq!(SetTimeReport::unpack(&[0; 7]), Err(ReportError::InvalidLength)); + } +} From d61ef28738fc5abd326443dfd3ed59d942b4bd06 Mon Sep 17 00:00:00 2001 From: Billy Price Date: Fri, 2 Oct 2026 12:49:20 -0700 Subject: [PATCH 5/7] constrain bitpack types to serialization module --- time-alarm-service-relay/src/hid/mod.rs | 47 ++-- .../src/hid/serialization.rs | 205 ++++++++++-------- 2 files changed, 138 insertions(+), 114 deletions(-) diff --git a/time-alarm-service-relay/src/hid/mod.rs b/time-alarm-service-relay/src/hid/mod.rs index b6e116e30..2b8714e08 100644 --- a/time-alarm-service-relay/src/hid/mod.rs +++ b/time-alarm-service-relay/src/hid/mod.rs @@ -11,8 +11,6 @@ mod serialization; use serialization::{AlarmCurrentState, FeatureReportId, InputReportId, OutputReportId}; -use crate::hid::serialization::GetAlarmReport; - /// Anything that can make a Set report fail. The host is told about it through the Failed state of /// the matching Get report rather than through the HID transport. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -31,12 +29,6 @@ impl From for SetReportError { } } -impl From for SetReportError { - fn from(_: serialization::TimestampConversionError) -> Self { - Self::Malformed - } -} - impl From for SetReportError { fn from(_: embedded_mcu_hal::time::DatetimeClockError) -> Self { Self::Service @@ -75,20 +67,18 @@ impl<'s, Service: TimeAlarmService> TimeAlarmHidRelay<'s, Service> { } fn apply_set_alarm(&self, timer_id: AcpiTimerId, data: &[u8]) -> Result<(), SetReportError> { - let set_alarm = serialization::SetAlarmReport::unpack(data)?; - info!("Parsed SetAlarmReport: {:?}", set_alarm); + let timer = serialization::deserialize_set_alarm_report(data)?; + info!("Parsed alarm timer: {:?}", timer); - self.service - .set_timer_value(timer_id, serialization::convert_timer(set_alarm.timer_seconds()))?; + self.service.set_timer_value(timer_id, timer)?; Ok(()) } fn apply_set_policy(&self, data: &[u8]) -> Result<(), SetReportError> { - let set_debounce = serialization::SetDebounceReport::unpack(data)?; - info!("Parsed SetDebounceReport: {:?}", set_debounce); + let wake_policy = serialization::deserialize_set_debounce_report(data)?; + info!("Parsed alarm wake policy: {:?}", wake_policy); - let wake_policy = serialization::convert_policy(set_debounce.power_source_change_debounce_seconds()); self.service .set_expired_timer_policy(AcpiTimerId::AcPower, wake_policy)?; self.service @@ -98,8 +88,7 @@ impl<'s, Service: TimeAlarmService> TimeAlarmHidRelay<'s, Service> { } fn apply_set_time(&self, data: &[u8]) -> Result<(), SetReportError> { - let time: time_alarm_service_interface::AcpiTimestamp = - serialization::SetTimeReport::unpack(data)?.try_into()?; + let time = serialization::deserialize_set_time_report(data)?; info!("Setting time to {:?}", time); self.service.set_real_time(time)?; @@ -116,7 +105,6 @@ impl<'s, Service: TimeAlarmService> embedded_services::relay::hid::HidDevice for const MAX_DESCRIPTOR_LEN: usize = serialization::TIME_ALARM_HID_DESCRIPTOR.len(); fn report_descriptor(&self) -> &HidReportDescriptor<'_> { - info!("descriptor len: {}", self.descriptor.as_bytes().len()); &self.descriptor } @@ -160,25 +148,30 @@ impl<'s, Service: TimeAlarmService> embedded_services::relay::hid::HidDevice for }; let vendor_current_state = 0; - let report_payload = - GetAlarmReport::new(ac_timer, dc_timer, wake_policy, current_state, vendor_current_state); - let report = report_payload.pack(); + let report = serialization::serialize_get_alarm_report( + ac_timer, + dc_timer, + wake_policy, + current_state, + vendor_current_state, + ); let report = HidReport::new(report_id, &report); Ok(process_report(GetHidReport::Input(report)).await) } InputReportId::GetTime => { info!("Received command to get time report"); - let report_payload = match self.service.get_real_time() { + let report = match self.service.get_real_time() { Ok(time) if !self.time_failed => { - serialization::GetTimeReport::new(time, serialization::TimeCurrentState::Running, 0) + serialization::serialize_get_time_report(time, serialization::TimeCurrentState::Running, 0) + } + Ok(time) => { + serialization::serialize_get_time_report(time, serialization::TimeCurrentState::Failed, 0) } - Ok(time) => serialization::GetTimeReport::new(time, serialization::TimeCurrentState::Failed, 0), Err(_) => { error!("Failed to read the current time"); - serialization::GetTimeReport::failed(0) + serialization::serialize_failed_get_time_report(0) } }; - let report = report_payload.pack(); let report = HidReport::new(report_id, &report); Ok(process_report(GetHidReport::Input(report)).await) } @@ -195,7 +188,7 @@ impl<'s, Service: TimeAlarmService> embedded_services::relay::hid::HidDevice for } else { serialization::PowerState::S3 }; - let capabilities = serialization::CapabilitiesFeatureReport::new(deepest_power_state).pack(); + let capabilities = serialization::serialize_capabilities_feature_report(deepest_power_state); Ok(process_report(GetHidReport::Feature(HidReport::new(report_id, &capabilities))).await) } } diff --git a/time-alarm-service-relay/src/hid/serialization.rs b/time-alarm-service-relay/src/hid/serialization.rs index 2cfac71f6..c27c116d3 100644 --- a/time-alarm-service-relay/src/hid/serialization.rs +++ b/time-alarm-service-relay/src/hid/serialization.rs @@ -8,29 +8,12 @@ use time_alarm_service_interface::{AcpiTimeZone, AcpiTimeZoneOffset}; pub(crate) enum ReportError { InvalidLength, ValueOutOfRange, + InvalidDatetime, } -#[allow(dead_code)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub(crate) enum TimestampConversionError { - Report(ReportError), - Datetime(embedded_mcu_hal::time::DatetimeError), - DatetimeClock(embedded_mcu_hal::time::DatetimeClockError), -} - -impl From for TimestampConversionError { - fn from(err: ReportError) -> Self { - Self::Report(err) - } -} -impl From for TimestampConversionError { - fn from(err: embedded_mcu_hal::time::DatetimeError) -> Self { - Self::Datetime(err) - } -} -impl From for TimestampConversionError { - fn from(err: embedded_mcu_hal::time::DatetimeClockError) -> Self { - Self::DatetimeClock(err) +impl From for ReportError { + fn from(_: embedded_mcu_hal::time::DatetimeError) -> Self { + Self::InvalidDatetime } } @@ -74,7 +57,7 @@ impl TryFrom for InputReportId { bitfield! { #[derive(Clone, Copy, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] - pub(crate) struct GetAlarmReport([u8]); + struct GetAlarmReport([u8]); impl Debug; u32, ac_timer, set_ac_timer: 30, 0; u32, dc_timer, set_dc_timer: 62, 32; @@ -95,7 +78,7 @@ pub(crate) enum AlarmCurrentState { } impl GetAlarmReport<[u8; 10]> { - pub(crate) fn new( + fn new( ac_timer: time_alarm_service_interface::AlarmTimerSeconds, dc_timer: time_alarm_service_interface::AlarmTimerSeconds, power_policy: time_alarm_service_interface::AlarmExpiredWakePolicy, @@ -110,10 +93,16 @@ impl GetAlarmReport<[u8; 10]> { report.set_vendor_current_state(vendor_current_state); report } +} - pub(crate) fn pack(self) -> [u8; 10] { - self.0 - } +pub(crate) fn serialize_get_alarm_report( + ac_timer: time_alarm_service_interface::AlarmTimerSeconds, + dc_timer: time_alarm_service_interface::AlarmTimerSeconds, + power_policy: time_alarm_service_interface::AlarmExpiredWakePolicy, + current_state: AlarmCurrentState, + vendor_current_state: u8, +) -> [u8; 10] { + GetAlarmReport::new(ac_timer, dc_timer, power_policy, current_state, vendor_current_state).0 } #[derive(Clone, Copy, Debug, PartialEq, Eq, num_enum::IntoPrimitive, num_enum::TryFromPrimitive)] @@ -126,7 +115,7 @@ pub(crate) enum TimeCurrentState { bitfield! { #[derive(Clone, Copy, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] - pub(crate) struct GetTimeReport([u8]); + struct GetTimeReport([u8]); impl Debug; u16, year, set_year: 13, 0; u8, month, set_month: 17, 14; @@ -143,11 +132,7 @@ bitfield! { } impl GetTimeReport<[u8; 9]> { - pub(crate) fn new( - ts: time_alarm_service_interface::AcpiTimestamp, - current_state: TimeCurrentState, - vendor_state: u8, - ) -> Self { + fn new(ts: time_alarm_service_interface::AcpiTimestamp, current_state: TimeCurrentState, vendor_state: u8) -> Self { let mut report = Self([0; 9]); report.set_year(ts.datetime.year()); report.set_month(ts.datetime.month().into()); @@ -172,16 +157,24 @@ impl GetTimeReport<[u8; 9]> { } /// Report used when the current time is unavailable; the time fields carry no meaning. - pub(crate) fn failed(vendor_state: u8) -> Self { + fn failed(vendor_state: u8) -> Self { let mut report = Self([0; 9]); report.set_current_state(TimeCurrentState::Failed.into()); report.set_vendor_current_state(vendor_state); report } +} - pub(crate) fn pack(self) -> [u8; 9] { - self.0 - } +pub(crate) fn serialize_get_time_report( + timestamp: time_alarm_service_interface::AcpiTimestamp, + current_state: TimeCurrentState, + vendor_current_state: u8, +) -> [u8; 9] { + GetTimeReport::new(timestamp, current_state, vendor_current_state).0 +} + +pub(crate) fn serialize_failed_get_time_report(vendor_current_state: u8) -> [u8; 9] { + GetTimeReport::failed(vendor_current_state).0 } // -------- OUTPUT REPORTS -------- @@ -207,41 +200,54 @@ impl TryFrom for OutputReportId { bitfield! { #[derive(Clone, Copy, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] - pub(crate) struct SetAlarmReport([u8]); + struct SetAlarmReport([u8]); impl Debug; pub u32, timer_seconds, _: 30, 0; } -impl<'a> SetAlarmReport<&'a [u8]> { - pub(crate) fn unpack(data: &'a [u8]) -> Result { - if data.len() < 4 { - return Err(ReportError::InvalidLength); - } +impl<'a> SetAlarmReport<&'a [u8; 4]> { + fn unpack(data: &'a [u8]) -> Result { + let data = data + .get(..4) + .and_then(|data| data.try_into().ok()) + .ok_or(ReportError::InvalidLength)?; Ok(Self(data)) } } +pub(crate) fn deserialize_set_alarm_report( + data: &[u8], +) -> Result { + let report = SetAlarmReport::unpack(data)?; + Ok(convert_timer(report.timer_seconds())) +} + bitfield! { #[derive(Clone, Copy, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] - pub(crate) struct SetDebounceReport([u8]); + struct SetDebounceReport([u8]); impl Debug; pub u8, power_source_change_debounce_seconds, _: 5, 0; } -impl<'a> SetDebounceReport<&'a [u8]> { - pub(crate) fn unpack(data: &'a [u8]) -> Result { - if data.is_empty() { - return Err(ReportError::InvalidLength); - } +impl<'a> SetDebounceReport<&'a [u8; 1]> { + fn unpack(data: &'a [u8]) -> Result { + let data = data.first_chunk::<1>().ok_or(ReportError::InvalidLength)?; Ok(Self(data)) } } +pub(crate) fn deserialize_set_debounce_report( + data: &[u8], +) -> Result { + let report = SetDebounceReport::unpack(data)?; + Ok(convert_policy(report.power_source_change_debounce_seconds())) +} + /// Converts a timer value in seconds to an `AlarmTimerSeconds` enum, mapping the HID null value to /// disabled. See [`TIMER_NULL`]; if we change the supported logical value range, we'll need to /// update this too. -pub(crate) fn convert_timer(seconds: u32) -> time_alarm_service_interface::AlarmTimerSeconds { +fn convert_timer(seconds: u32) -> time_alarm_service_interface::AlarmTimerSeconds { if seconds == TIMER_NULL { time_alarm_service_interface::AlarmTimerSeconds::DISABLED } else { @@ -292,7 +298,7 @@ fn convert_policy_to_wire(policy: time_alarm_service_interface::AlarmExpiredWake /// Inverse of [`convert_policy_to_wire`]. The null value means the host isn't asking for a minimum /// expiration, so we wake instantly; NEVER has no wire encoding and so can't be round-tripped. -pub(crate) fn convert_policy(seconds: u8) -> time_alarm_service_interface::AlarmExpiredWakePolicy { +fn convert_policy(seconds: u8) -> time_alarm_service_interface::AlarmExpiredWakePolicy { match u32::from(seconds) { seconds @ DEBOUNCE_LOGICAL_MIN..=DEBOUNCE_LOGICAL_MAX => { time_alarm_service_interface::AlarmExpiredWakePolicy(seconds) @@ -319,7 +325,7 @@ fn time_zone_from_wire(time_zone: i16) -> AcpiTimeZone { bitfield! { #[derive(Clone, Copy, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] - pub(crate) struct SetTimeReport([u8]); + struct SetTimeReport([u8]); impl Debug; pub u16, year, _: 13, 0; pub u8, month, _: 17, 14; @@ -333,25 +339,29 @@ bitfield! { pub dst_active, _: 63; } -impl<'a> SetTimeReport<&'a [u8]> { - pub(crate) fn unpack(data: &'a [u8]) -> Result { - if data.len() < 8 { - return Err(ReportError::InvalidLength); - } +impl<'a> SetTimeReport<&'a [u8; 8]> { + fn unpack(data: &'a [u8]) -> Result { + let data = data + .get(..8) + .and_then(|data| data.try_into().ok()) + .ok_or(ReportError::InvalidLength)?; Ok(Self(data)) } } +pub(crate) fn deserialize_set_time_report( + data: &[u8], +) -> Result { + SetTimeReport::unpack(data)?.try_into() +} + impl> TryFrom> for time_alarm_service_interface::AcpiTimestamp { - type Error = TimestampConversionError; + type Error = ReportError; fn try_from(report: SetTimeReport) -> Result { Ok(Self { datetime: Datetime::new(DatetimeFields { year: report.year(), - month: report - .month() - .try_into() - .map_err(|_| TimestampConversionError::Datetime(embedded_mcu_hal::time::DatetimeError::Month))?, + month: report.month().try_into().map_err(|_| ReportError::InvalidDatetime)?, day: report.day(), hour: report.hour(), minute: report.minute(), @@ -364,7 +374,7 @@ impl> TryFrom> for time_alarm_service_interface: (true, false) => time_alarm_service_interface::AcpiDaylightSavingsTimeStatus::NotAdjusted, (true, true) => time_alarm_service_interface::AcpiDaylightSavingsTimeStatus::Adjusted, (false, true) => { - return Err(TimestampConversionError::Report(ReportError::ValueOutOfRange)); + return Err(ReportError::ValueOutOfRange); } }, }) @@ -398,21 +408,21 @@ pub(crate) enum PowerState { bitfield! { #[derive(Clone, Copy, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] - pub(crate) struct CapabilitiesFeatureReport([u8]); + struct CapabilitiesFeatureReport([u8]); impl Debug; pub u8, power_state, set_power_state: 1, 0; } impl CapabilitiesFeatureReport<[u8; 1]> { - pub(crate) fn new(power_state: PowerState) -> Self { + fn new(power_state: PowerState) -> Self { let mut report = Self([0]); report.set_power_state(power_state.into()); report } +} - pub(crate) fn pack(self) -> [u8; 1] { - self.0 - } +pub(crate) fn serialize_capabilities_feature_report(power_state: PowerState) -> [u8; 1] { + CapabilitiesFeatureReport::new(power_state).0 } // Generated from Waratah - don't hand-edit this. Instead, modify the .wara file and rerun Waratah. @@ -603,7 +613,7 @@ mod tests { #[test] fn get_alarm_report_matches_hid_bit_layout() { - let report = GetAlarmReport::new( + let report = serialize_get_alarm_report( time_alarm_service_interface::AlarmTimerSeconds(0x1234_5678), time_alarm_service_interface::AlarmTimerSeconds(0x2345_6789), time_alarm_service_interface::AlarmExpiredWakePolicy(42), @@ -611,33 +621,54 @@ mod tests { 10, ); - assert_eq!( - report.pack(), - [0x78, 0x56, 0x34, 0x12, 0x89, 0x67, 0x45, 0x23, 0x75, 0x0A] - ); + assert_eq!(report, [0x78, 0x56, 0x34, 0x12, 0x89, 0x67, 0x45, 0x23, 0x75, 0x0A]); } #[test] fn set_time_report_matches_hid_bit_layout() { let data = [0xEA, 0x47, 0x86, 0xD6, 0xEE, 0xE7, 0x83, 0x78]; - let report = SetTimeReport::unpack(&data).unwrap(); - - assert_eq!(report.year(), 2026); - assert_eq!(report.month(), 9); - assert_eq!(report.day(), 1); - assert_eq!(report.hour(), 13); - assert_eq!(report.minute(), 45); - assert_eq!(report.second(), 59); - assert_eq!(report.millisecond(), 999); - assert_eq!(report.time_zone(), -480); - assert!(report.dst_observed()); - assert!(!report.dst_active()); + let timestamp = deserialize_set_time_report(&data).unwrap(); + + assert_eq!(timestamp.datetime.year(), 2026); + assert_eq!(u8::from(timestamp.datetime.month()), 9); + assert_eq!(timestamp.datetime.day(), 1); + assert_eq!(timestamp.datetime.hour(), 13); + assert_eq!(timestamp.datetime.minute(), 45); + assert_eq!(timestamp.datetime.second(), 59); + assert_eq!(timestamp.datetime.nanoseconds(), 999_000_000); + assert_eq!( + timestamp.time_zone, + AcpiTimeZone::MinutesFromUtc(AcpiTimeZoneOffset::new(-480).unwrap()) + ); + assert_eq!( + timestamp.dst_status, + time_alarm_service_interface::AcpiDaylightSavingsTimeStatus::NotAdjusted + ); + } + + #[test] + fn set_reports_deserialize_to_interface_types() { + assert_eq!( + deserialize_set_alarm_report(&[42, 0, 0, 0]).unwrap(), + time_alarm_service_interface::AlarmTimerSeconds(42) + ); + assert_eq!( + deserialize_set_alarm_report(&[0, 0, 0, 0]).unwrap(), + time_alarm_service_interface::AlarmTimerSeconds::DISABLED + ); + assert_eq!( + deserialize_set_debounce_report(&[42]).unwrap(), + time_alarm_service_interface::AlarmExpiredWakePolicy(42) + ); } #[test] fn unpack_rejects_short_reports() { - assert_eq!(SetAlarmReport::unpack(&[0; 3]), Err(ReportError::InvalidLength)); - assert_eq!(SetDebounceReport::unpack(&[]), Err(ReportError::InvalidLength)); - assert_eq!(SetTimeReport::unpack(&[0; 7]), Err(ReportError::InvalidLength)); + assert_eq!(deserialize_set_alarm_report(&[0; 3]), Err(ReportError::InvalidLength)); + assert_eq!(deserialize_set_debounce_report(&[]), Err(ReportError::InvalidLength)); + assert!(matches!( + deserialize_set_time_report(&[0; 7]), + Err(ReportError::InvalidLength) + )); } } From fe1b1f8d4bf7b15b9f8060cb95e2f32ee3ed31e3 Mon Sep 17 00:00:00 2001 From: Billy Price Date: Fri, 2 Oct 2026 13:19:30 -0700 Subject: [PATCH 6/7] conversion cleanup --- .../src/hid/SystemWakeTimerAndRtc.wara | 4 +- .../src/hid/serialization.rs | 46 +++++++++---------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/time-alarm-service-relay/src/hid/SystemWakeTimerAndRtc.wara b/time-alarm-service-relay/src/hid/SystemWakeTimerAndRtc.wara index 3873b3b55..abc72ca46 100644 --- a/time-alarm-service-relay/src/hid/SystemWakeTimerAndRtc.wara +++ b/time-alarm-service-relay/src/hid/SystemWakeTimerAndRtc.wara @@ -226,7 +226,7 @@ usage = ['Generic Desktop', 'System Wake Timer and Real Time Clock'] [[applicationCollection.outputReport.variableItem]] usage = ['Time and Date', 'Year'] - logicalValueRange = [1900,9999] + logicalValueRange = [1970,9999] [[applicationCollection.outputReport.variableItem]] usage = ['Time and Date', 'Month'] @@ -269,7 +269,7 @@ usage = ['Generic Desktop', 'System Wake Timer and Real Time Clock'] [[applicationCollection.inputReport.variableItem]] usage = ['Time and Date', 'Year'] - logicalValueRange = [1900,9999] + logicalValueRange = [1970,9999] [[applicationCollection.inputReport.variableItem]] usage = ['Time and Date', 'Month'] diff --git a/time-alarm-service-relay/src/hid/serialization.rs b/time-alarm-service-relay/src/hid/serialization.rs index c27c116d3..4fc0d5020 100644 --- a/time-alarm-service-relay/src/hid/serialization.rs +++ b/time-alarm-service-relay/src/hid/serialization.rs @@ -86,9 +86,9 @@ impl GetAlarmReport<[u8; 10]> { vendor_current_state: u8, ) -> Self { let mut report = Self([0; 10]); - report.set_ac_timer(convert_timer_to_wire(ac_timer)); - report.set_dc_timer(convert_timer_to_wire(dc_timer)); - report.set_power_source_change_debounce_seconds(convert_policy_to_wire(power_policy)); + report.set_ac_timer(timer_to_wire(ac_timer)); + report.set_dc_timer(timer_to_wire(dc_timer)); + report.set_power_source_change_debounce_seconds(policy_to_wire(power_policy)); report.set_current_state(current_state.into()); report.set_vendor_current_state(vendor_current_state); report @@ -219,7 +219,7 @@ pub(crate) fn deserialize_set_alarm_report( data: &[u8], ) -> Result { let report = SetAlarmReport::unpack(data)?; - Ok(convert_timer(report.timer_seconds())) + Ok(timer_from_wire(report.timer_seconds())) } bitfield! { @@ -241,18 +241,7 @@ pub(crate) fn deserialize_set_debounce_report( data: &[u8], ) -> Result { let report = SetDebounceReport::unpack(data)?; - Ok(convert_policy(report.power_source_change_debounce_seconds())) -} - -/// Converts a timer value in seconds to an `AlarmTimerSeconds` enum, mapping the HID null value to -/// disabled. See [`TIMER_NULL`]; if we change the supported logical value range, we'll need to -/// update this too. -fn convert_timer(seconds: u32) -> time_alarm_service_interface::AlarmTimerSeconds { - if seconds == TIMER_NULL { - time_alarm_service_interface::AlarmTimerSeconds::DISABLED - } else { - time_alarm_service_interface::AlarmTimerSeconds(seconds) - } + Ok(policy_from_wire(report.power_source_change_debounce_seconds())) } /// A value in the physical range but out of the logical range for our timer. Must agree with report descriptor. @@ -267,9 +256,9 @@ const TIMER_LOGICAL_MAX: u32 = 0x7FFF_FFFF; const DEBOUNCE_LOGICAL_MIN: u32 = 1; const DEBOUNCE_LOGICAL_MAX: u32 = 60; -/// Inverse of [`convert_timer`]: a disabled timer is reported as the null value, and values the +/// Inverse of [`timer_from_wire`]: a disabled timer is reported as the null value, and values the /// descriptor can't express are clamped rather than failing the whole report. -fn convert_timer_to_wire(timer: time_alarm_service_interface::AlarmTimerSeconds) -> u32 { +fn timer_to_wire(timer: time_alarm_service_interface::AlarmTimerSeconds) -> u32 { if timer == time_alarm_service_interface::AlarmTimerSeconds::DISABLED { TIMER_NULL } else if timer == time_alarm_service_interface::AlarmTimerSeconds(0) { @@ -284,7 +273,18 @@ fn convert_timer_to_wire(timer: time_alarm_service_interface::AlarmTimerSeconds) } } -fn convert_policy_to_wire(policy: time_alarm_service_interface::AlarmExpiredWakePolicy) -> u8 { +/// Converts a timer value in seconds to an `AlarmTimerSeconds` enum, mapping the HID null value to +/// disabled. See [`TIMER_NULL`]; if we change the supported logical value range, we'll need to +/// update this too. +fn timer_from_wire(seconds: u32) -> time_alarm_service_interface::AlarmTimerSeconds { + if seconds == TIMER_NULL { + time_alarm_service_interface::AlarmTimerSeconds::DISABLED + } else { + time_alarm_service_interface::AlarmTimerSeconds(seconds) + } +} + +fn policy_to_wire(policy: time_alarm_service_interface::AlarmExpiredWakePolicy) -> u8 { match policy { time_alarm_service_interface::AlarmExpiredWakePolicy::INSTANTLY => 0, // TODO "never" isn't expressible with the current HID interface; need to circle back with time and hid folks @@ -296,9 +296,9 @@ fn convert_policy_to_wire(policy: time_alarm_service_interface::AlarmExpiredWake } } -/// Inverse of [`convert_policy_to_wire`]. The null value means the host isn't asking for a minimum +/// Inverse of [`policy_to_wire`]. The null value means the host isn't asking for a minimum /// expiration, so we wake instantly; NEVER has no wire encoding and so can't be round-tripped. -fn convert_policy(seconds: u8) -> time_alarm_service_interface::AlarmExpiredWakePolicy { +fn policy_from_wire(seconds: u8) -> time_alarm_service_interface::AlarmExpiredWakePolicy { match u32::from(seconds) { seconds @ DEBOUNCE_LOGICAL_MIN..=DEBOUNCE_LOGICAL_MAX => { time_alarm_service_interface::AlarmExpiredWakePolicy(seconds) @@ -502,7 +502,7 @@ pub(crate) const TIME_ALARM_HID_DESCRIPTOR: &[u8] = &[ 0x85, 0x04, // ReportId(4) 0x05, 0x13, // UsagePage(Time and Date[0x0013]) 0x09, 0x01, // UsageId(Year[0x0001]) - 0x16, 0x6C, 0x07, // LogicalMinimum(1,900) + 0x16, 0xB2, 0x07, // LogicalMinimum(1,970) 0x26, 0x0F, 0x27, // LogicalMaximum(9,999) 0x75, 0x0E, // ReportSize(14) 0x91, 0x02, // Output(Data, Variable, Absolute, NoWrap, Linear, PreferredState, NoNullPosition, NonVolatile, BitField) @@ -544,7 +544,7 @@ pub(crate) const TIME_ALARM_HID_DESCRIPTOR: &[u8] = &[ 0x91, 0x02, // Output(Data, Variable, Absolute, NoWrap, Linear, PreferredState, NoNullPosition, NonVolatile, BitField) 0x85, 0x02, // ReportId(2) 0x09, 0x01, // UsageId(Year[0x0001]) - 0x16, 0x6C, 0x07, // LogicalMinimum(1,900) + 0x16, 0xB2, 0x07, // LogicalMinimum(1,970) 0x26, 0x0F, 0x27, // LogicalMaximum(9,999) 0x95, 0x01, // ReportCount(1) 0x75, 0x0E, // ReportSize(14) From a24ceb83bcc11d8def9d3685c3bbe299c13322d7 Mon Sep 17 00:00:00 2001 From: Billy Price Date: Fri, 2 Oct 2026 13:39:19 -0700 Subject: [PATCH 7/7] use newtypes for fields --- .../src/hid/serialization.rs | 232 +++++++++++------- 1 file changed, 140 insertions(+), 92 deletions(-) diff --git a/time-alarm-service-relay/src/hid/serialization.rs b/time-alarm-service-relay/src/hid/serialization.rs index 4fc0d5020..2f5d88253 100644 --- a/time-alarm-service-relay/src/hid/serialization.rs +++ b/time-alarm-service-relay/src/hid/serialization.rs @@ -1,7 +1,7 @@ use bitfield::bitfield; use embedded_mcu_hal::time::{Datetime, DatetimeFields}; use embedded_services::relay::hid::ReportId; -use time_alarm_service_interface::{AcpiTimeZone, AcpiTimeZoneOffset}; +use time_alarm_service_interface::AcpiTimeZone; #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] @@ -54,14 +54,109 @@ impl TryFrom for InputReportId { } } +/// A value in the physical range but out of the logical range for our timer. Must agree with report descriptor. +const TIMER_NULL: u32 = 0; + +/// LogicalMaximum of the alarm timer items in our report descriptor; also the widest value the +/// 31-bit timer fields can hold. +const TIMER_LOGICAL_MIN: u32 = 1; +const TIMER_LOGICAL_MAX: u32 = 0x7FFF_FFFF; + +#[derive(Clone, Copy, Debug, PartialEq)] +struct HidTimerSeconds(u32); + +impl From for HidTimerSeconds { + fn from(value: u32) -> Self { + Self(value) + } +} + +impl From for HidTimerSeconds { + fn from(value: time_alarm_service_interface::AlarmTimerSeconds) -> Self { + Self(match value { + time_alarm_service_interface::AlarmTimerSeconds::DISABLED => TIMER_NULL, + + // TODO - there's currently a disagreement between HID and ACPI on what a logical value of "0" means. + // ACPI says it means "this timer is expired" but HID says it's not a valid value, which in + // effect means "this timer is disabled". + // For now, we treat it as disabled, but there may be a case to be made that it should mean + // the same thing as in ACPI. + time_alarm_service_interface::AlarmTimerSeconds(0) => TIMER_NULL, + time_alarm_service_interface::AlarmTimerSeconds(seconds) => { + seconds.clamp(TIMER_LOGICAL_MIN, TIMER_LOGICAL_MAX) + } + }) + } +} + +impl From for u32 { + fn from(value: HidTimerSeconds) -> Self { + value.0 + } +} + +impl From for time_alarm_service_interface::AlarmTimerSeconds { + fn from(value: HidTimerSeconds) -> Self { + if value.0 == TIMER_NULL { + Self::DISABLED + } else { + Self(value.0) + } + } +} + +/// LogicalMaximum of the power source change debounce items in our report descriptor. +const DEBOUNCE_LOGICAL_MIN: u32 = 1; +const DEBOUNCE_LOGICAL_MAX: u32 = 60; + +#[derive(Clone, Copy, Debug, PartialEq)] +struct HidWakePolicy(u8); + +impl From for HidWakePolicy { + fn from(value: u8) -> Self { + Self(value) + } +} + +impl From for HidWakePolicy { + fn from(value: time_alarm_service_interface::AlarmExpiredWakePolicy) -> Self { + Self(match value { + time_alarm_service_interface::AlarmExpiredWakePolicy::INSTANTLY => 0, + // TODO "never" isn't expressible with the current HID interface; need to circle back with time and hid folks + // on if this was a deliberate design decision or an oversight. For now, report it as our logical max. + time_alarm_service_interface::AlarmExpiredWakePolicy::NEVER => DEBOUNCE_LOGICAL_MAX as u8, + time_alarm_service_interface::AlarmExpiredWakePolicy(seconds) => { + seconds.clamp(DEBOUNCE_LOGICAL_MIN, DEBOUNCE_LOGICAL_MAX) as u8 + } + }) + } +} + +impl From for u8 { + fn from(value: HidWakePolicy) -> Self { + value.0 + } +} + +impl From for time_alarm_service_interface::AlarmExpiredWakePolicy { + fn from(value: HidWakePolicy) -> Self { + // The HID null value means no minimum delay; other values outside the logical range are + // treated the same way because they carry no valid policy. + match u32::from(value.0) { + seconds @ DEBOUNCE_LOGICAL_MIN..=DEBOUNCE_LOGICAL_MAX => Self(seconds), + _ => Self::INSTANTLY, + } + } +} + bitfield! { #[derive(Clone, Copy, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] struct GetAlarmReport([u8]); impl Debug; - u32, ac_timer, set_ac_timer: 30, 0; - u32, dc_timer, set_dc_timer: 62, 32; - u8, power_source_change_debounce_seconds, set_power_source_change_debounce_seconds: 68, 63; + u32, from into HidTimerSeconds, ac_timer, set_ac_timer: 30, 0; + u32, from into HidTimerSeconds, dc_timer, set_dc_timer: 62, 32; + u8, from into HidWakePolicy, power_source_change_debounce, set_power_source_change_debounce: 68, 63; u8, current_state, set_current_state: 71, 69; u8, vendor_current_state, set_vendor_current_state: 75, 72; } @@ -86,9 +181,9 @@ impl GetAlarmReport<[u8; 10]> { vendor_current_state: u8, ) -> Self { let mut report = Self([0; 10]); - report.set_ac_timer(timer_to_wire(ac_timer)); - report.set_dc_timer(timer_to_wire(dc_timer)); - report.set_power_source_change_debounce_seconds(policy_to_wire(power_policy)); + report.set_ac_timer(ac_timer.into()); + report.set_dc_timer(dc_timer.into()); + report.set_power_source_change_debounce(power_policy.into()); report.set_current_state(current_state.into()); report.set_vendor_current_state(vendor_current_state); report @@ -141,7 +236,7 @@ impl GetTimeReport<[u8; 9]> { report.set_minute(ts.datetime.minute()); report.set_second(ts.datetime.second()); report.set_millisecond((ts.datetime.nanoseconds() / 1_000_000) as u16); - report.set_time_zone(time_zone_to_wire(ts.time_zone)); + report.set_time_zone(ts.time_zone.into()); report.set_dst_observed(matches!( ts.dst_status, time_alarm_service_interface::AcpiDaylightSavingsTimeStatus::NotAdjusted @@ -202,7 +297,7 @@ bitfield! { #[cfg_attr(feature = "defmt", derive(defmt::Format))] struct SetAlarmReport([u8]); impl Debug; - pub u32, timer_seconds, _: 30, 0; + pub u32, into HidTimerSeconds, timer_seconds, _: 30, 0; } impl<'a> SetAlarmReport<&'a [u8; 4]> { @@ -219,7 +314,7 @@ pub(crate) fn deserialize_set_alarm_report( data: &[u8], ) -> Result { let report = SetAlarmReport::unpack(data)?; - Ok(timer_from_wire(report.timer_seconds())) + Ok(report.timer_seconds().into()) } bitfield! { @@ -227,7 +322,7 @@ bitfield! { #[cfg_attr(feature = "defmt", derive(defmt::Format))] struct SetDebounceReport([u8]); impl Debug; - pub u8, power_source_change_debounce_seconds, _: 5, 0; + pub u8, into HidWakePolicy, power_source_change_debounce, _: 5, 0; } impl<'a> SetDebounceReport<&'a [u8; 1]> { @@ -241,85 +336,7 @@ pub(crate) fn deserialize_set_debounce_report( data: &[u8], ) -> Result { let report = SetDebounceReport::unpack(data)?; - Ok(policy_from_wire(report.power_source_change_debounce_seconds())) -} - -/// A value in the physical range but out of the logical range for our timer. Must agree with report descriptor. -const TIMER_NULL: u32 = 0; - -/// LogicalMaximum of the alarm timer items in our report descriptor; also the widest value the -/// 31-bit timer fields can hold. -const TIMER_LOGICAL_MIN: u32 = 1; -const TIMER_LOGICAL_MAX: u32 = 0x7FFF_FFFF; - -/// LogicalMaximum of the power source change debounce items in our report descriptor. -const DEBOUNCE_LOGICAL_MIN: u32 = 1; -const DEBOUNCE_LOGICAL_MAX: u32 = 60; - -/// Inverse of [`timer_from_wire`]: a disabled timer is reported as the null value, and values the -/// descriptor can't express are clamped rather than failing the whole report. -fn timer_to_wire(timer: time_alarm_service_interface::AlarmTimerSeconds) -> u32 { - if timer == time_alarm_service_interface::AlarmTimerSeconds::DISABLED { - TIMER_NULL - } else if timer == time_alarm_service_interface::AlarmTimerSeconds(0) { - // TODO - there's currently a disagreement between HID and ACPI on what a logical value of "0" means. - // ACPI says it means "this timer is expired" but HID says it's not a valid value, which in - // effect means "this timer is disabled". - // For now, we treat it as disabled, but there may be a case to be made that it should mean - // the same thing as in ACPI. - TIMER_NULL - } else { - timer.0.clamp(TIMER_LOGICAL_MIN, TIMER_LOGICAL_MAX) - } -} - -/// Converts a timer value in seconds to an `AlarmTimerSeconds` enum, mapping the HID null value to -/// disabled. See [`TIMER_NULL`]; if we change the supported logical value range, we'll need to -/// update this too. -fn timer_from_wire(seconds: u32) -> time_alarm_service_interface::AlarmTimerSeconds { - if seconds == TIMER_NULL { - time_alarm_service_interface::AlarmTimerSeconds::DISABLED - } else { - time_alarm_service_interface::AlarmTimerSeconds(seconds) - } -} - -fn policy_to_wire(policy: time_alarm_service_interface::AlarmExpiredWakePolicy) -> u8 { - match policy { - time_alarm_service_interface::AlarmExpiredWakePolicy::INSTANTLY => 0, - // TODO "never" isn't expressible with the current HID interface; need to circle back with time and hid folks - // on if this was a deliberate design decision or an oversight. For now, report it as our logical max. - time_alarm_service_interface::AlarmExpiredWakePolicy::NEVER => DEBOUNCE_LOGICAL_MAX as u8, - time_alarm_service_interface::AlarmExpiredWakePolicy(seconds) => { - seconds.clamp(DEBOUNCE_LOGICAL_MIN, DEBOUNCE_LOGICAL_MAX) as u8 - } - } -} - -/// Inverse of [`policy_to_wire`]. The null value means the host isn't asking for a minimum -/// expiration, so we wake instantly; NEVER has no wire encoding and so can't be round-tripped. -fn policy_from_wire(seconds: u8) -> time_alarm_service_interface::AlarmExpiredWakePolicy { - match u32::from(seconds) { - seconds @ DEBOUNCE_LOGICAL_MIN..=DEBOUNCE_LOGICAL_MAX => { - time_alarm_service_interface::AlarmExpiredWakePolicy(seconds) - } - _ => time_alarm_service_interface::AlarmExpiredWakePolicy::INSTANTLY, - } -} - -const NULL_HID_TIME_ZONE: i16 = 2047; -fn time_zone_to_wire(time_zone: AcpiTimeZone) -> i16 { - match time_zone { - AcpiTimeZone::Unknown => NULL_HID_TIME_ZONE, - AcpiTimeZone::MinutesFromUtc(offset) => offset.minutes_from_utc(), - } -} - -fn time_zone_from_wire(time_zone: i16) -> AcpiTimeZone { - match AcpiTimeZoneOffset::new(time_zone) { - Ok(offset) => AcpiTimeZone::MinutesFromUtc(offset), - Err(_) => AcpiTimeZone::Unknown, - } + Ok(report.power_source_change_debounce().into()) } bitfield! { @@ -368,7 +385,7 @@ impl> TryFrom> for time_alarm_service_interface: second: report.second(), nanosecond: u32::from(report.millisecond()) * 1_000_000, })?, - time_zone: time_zone_from_wire(report.time_zone()), + time_zone: AcpiTimeZone::try_from(report.time_zone()).unwrap_or(AcpiTimeZone::Unknown), dst_status: match (report.dst_observed(), report.dst_active()) { (false, false) => time_alarm_service_interface::AcpiDaylightSavingsTimeStatus::NotObserved, (true, false) => time_alarm_service_interface::AcpiDaylightSavingsTimeStatus::NotAdjusted, @@ -638,7 +655,7 @@ mod tests { assert_eq!(timestamp.datetime.nanoseconds(), 999_000_000); assert_eq!( timestamp.time_zone, - AcpiTimeZone::MinutesFromUtc(AcpiTimeZoneOffset::new(-480).unwrap()) + AcpiTimeZone::MinutesFromUtc(time_alarm_service_interface::AcpiTimeZoneOffset::new(-480).unwrap()) ); assert_eq!( timestamp.dst_status, @@ -662,6 +679,37 @@ mod tests { ); } + #[test] + fn wire_newtypes_preserve_null_and_clamping_semantics() { + assert_eq!( + u32::from(HidTimerSeconds::from( + time_alarm_service_interface::AlarmTimerSeconds::DISABLED + )), + TIMER_NULL + ); + assert_eq!( + u32::from(HidTimerSeconds::from(time_alarm_service_interface::AlarmTimerSeconds( + u32::MAX - 1 + ))), + TIMER_LOGICAL_MAX + ); + assert_eq!( + time_alarm_service_interface::AlarmTimerSeconds::from(HidTimerSeconds::from(TIMER_NULL)), + time_alarm_service_interface::AlarmTimerSeconds::DISABLED + ); + + assert_eq!( + u8::from(HidWakePolicy::from( + time_alarm_service_interface::AlarmExpiredWakePolicy::NEVER + )), + DEBOUNCE_LOGICAL_MAX as u8 + ); + assert_eq!( + time_alarm_service_interface::AlarmExpiredWakePolicy::from(HidWakePolicy::from(0)), + time_alarm_service_interface::AlarmExpiredWakePolicy::INSTANTLY + ); + } + #[test] fn unpack_rejects_short_reports() { assert_eq!(deserialize_set_alarm_report(&[0; 3]), Err(ReportError::InvalidLength));