diff options
| author | alexmoon <[email protected]> | 2022-03-29 15:18:43 -0400 |
|---|---|---|
| committer | Dario Nieuwenhuis <[email protected]> | 2022-04-06 05:38:11 +0200 |
| commit | 5ee7a85b33f83131fd42ce229d3aadaf2054f44a (patch) | |
| tree | f20b27dfedab8f97b7fb0f5f969c8ac41a41bab2 | |
| parent | 8fe3b44d82f4f53491520898148c4ad337073593 (diff) | |
Async USB HID class
| -rw-r--r-- | embassy-usb-hid/Cargo.toml | 18 | ||||
| -rw-r--r-- | embassy-usb-hid/src/fmt.rs | 225 | ||||
| -rw-r--r-- | embassy-usb-hid/src/lib.rs | 529 | ||||
| -rw-r--r-- | examples/nrf/Cargo.toml | 6 | ||||
| -rw-r--r-- | examples/nrf/src/bin/usb_hid.rs | 131 |
5 files changed, 907 insertions, 2 deletions
diff --git a/embassy-usb-hid/Cargo.toml b/embassy-usb-hid/Cargo.toml new file mode 100644 index 000000000..dc3d3cd88 --- /dev/null +++ b/embassy-usb-hid/Cargo.toml | |||
| @@ -0,0 +1,18 @@ | |||
| 1 | [package] | ||
| 2 | name = "embassy-usb-hid" | ||
| 3 | version = "0.1.0" | ||
| 4 | edition = "2021" | ||
| 5 | |||
| 6 | [features] | ||
| 7 | default = ["usbd-hid"] | ||
| 8 | usbd-hid = ["dep:usbd-hid", "ssmarshal"] | ||
| 9 | |||
| 10 | [dependencies] | ||
| 11 | embassy = { version = "0.1.0", path = "../embassy" } | ||
| 12 | embassy-usb = { version = "0.1.0", path = "../embassy-usb" } | ||
| 13 | |||
| 14 | defmt = { version = "0.3", optional = true } | ||
| 15 | log = { version = "0.4.14", optional = true } | ||
| 16 | usbd-hid = { version = "0.5.2", optional = true } | ||
| 17 | ssmarshal = { version = "1.0", default-features = false, optional = true } | ||
| 18 | futures-util = { version = "0.3.21", default-features = false } | ||
diff --git a/embassy-usb-hid/src/fmt.rs b/embassy-usb-hid/src/fmt.rs new file mode 100644 index 000000000..066970813 --- /dev/null +++ b/embassy-usb-hid/src/fmt.rs | |||
| @@ -0,0 +1,225 @@ | |||
| 1 | #![macro_use] | ||
| 2 | #![allow(unused_macros)] | ||
| 3 | |||
| 4 | #[cfg(all(feature = "defmt", feature = "log"))] | ||
| 5 | compile_error!("You may not enable both `defmt` and `log` features."); | ||
| 6 | |||
| 7 | macro_rules! assert { | ||
| 8 | ($($x:tt)*) => { | ||
| 9 | { | ||
| 10 | #[cfg(not(feature = "defmt"))] | ||
| 11 | ::core::assert!($($x)*); | ||
| 12 | #[cfg(feature = "defmt")] | ||
| 13 | ::defmt::assert!($($x)*); | ||
| 14 | } | ||
| 15 | }; | ||
| 16 | } | ||
| 17 | |||
| 18 | macro_rules! assert_eq { | ||
| 19 | ($($x:tt)*) => { | ||
| 20 | { | ||
| 21 | #[cfg(not(feature = "defmt"))] | ||
| 22 | ::core::assert_eq!($($x)*); | ||
| 23 | #[cfg(feature = "defmt")] | ||
| 24 | ::defmt::assert_eq!($($x)*); | ||
| 25 | } | ||
| 26 | }; | ||
| 27 | } | ||
| 28 | |||
| 29 | macro_rules! assert_ne { | ||
| 30 | ($($x:tt)*) => { | ||
| 31 | { | ||
| 32 | #[cfg(not(feature = "defmt"))] | ||
| 33 | ::core::assert_ne!($($x)*); | ||
| 34 | #[cfg(feature = "defmt")] | ||
| 35 | ::defmt::assert_ne!($($x)*); | ||
| 36 | } | ||
| 37 | }; | ||
| 38 | } | ||
| 39 | |||
| 40 | macro_rules! debug_assert { | ||
| 41 | ($($x:tt)*) => { | ||
| 42 | { | ||
| 43 | #[cfg(not(feature = "defmt"))] | ||
| 44 | ::core::debug_assert!($($x)*); | ||
| 45 | #[cfg(feature = "defmt")] | ||
| 46 | ::defmt::debug_assert!($($x)*); | ||
| 47 | } | ||
| 48 | }; | ||
| 49 | } | ||
| 50 | |||
| 51 | macro_rules! debug_assert_eq { | ||
| 52 | ($($x:tt)*) => { | ||
| 53 | { | ||
| 54 | #[cfg(not(feature = "defmt"))] | ||
| 55 | ::core::debug_assert_eq!($($x)*); | ||
| 56 | #[cfg(feature = "defmt")] | ||
| 57 | ::defmt::debug_assert_eq!($($x)*); | ||
| 58 | } | ||
| 59 | }; | ||
| 60 | } | ||
| 61 | |||
| 62 | macro_rules! debug_assert_ne { | ||
| 63 | ($($x:tt)*) => { | ||
| 64 | { | ||
| 65 | #[cfg(not(feature = "defmt"))] | ||
| 66 | ::core::debug_assert_ne!($($x)*); | ||
| 67 | #[cfg(feature = "defmt")] | ||
| 68 | ::defmt::debug_assert_ne!($($x)*); | ||
| 69 | } | ||
| 70 | }; | ||
| 71 | } | ||
| 72 | |||
| 73 | macro_rules! todo { | ||
| 74 | ($($x:tt)*) => { | ||
| 75 | { | ||
| 76 | #[cfg(not(feature = "defmt"))] | ||
| 77 | ::core::todo!($($x)*); | ||
| 78 | #[cfg(feature = "defmt")] | ||
| 79 | ::defmt::todo!($($x)*); | ||
| 80 | } | ||
| 81 | }; | ||
| 82 | } | ||
| 83 | |||
| 84 | macro_rules! unreachable { | ||
| 85 | ($($x:tt)*) => { | ||
| 86 | { | ||
| 87 | #[cfg(not(feature = "defmt"))] | ||
| 88 | ::core::unreachable!($($x)*); | ||
| 89 | #[cfg(feature = "defmt")] | ||
| 90 | ::defmt::unreachable!($($x)*); | ||
| 91 | } | ||
| 92 | }; | ||
| 93 | } | ||
| 94 | |||
| 95 | macro_rules! panic { | ||
| 96 | ($($x:tt)*) => { | ||
| 97 | { | ||
| 98 | #[cfg(not(feature = "defmt"))] | ||
| 99 | ::core::panic!($($x)*); | ||
| 100 | #[cfg(feature = "defmt")] | ||
| 101 | ::defmt::panic!($($x)*); | ||
| 102 | } | ||
| 103 | }; | ||
| 104 | } | ||
| 105 | |||
| 106 | macro_rules! trace { | ||
| 107 | ($s:literal $(, $x:expr)* $(,)?) => { | ||
| 108 | { | ||
| 109 | #[cfg(feature = "log")] | ||
| 110 | ::log::trace!($s $(, $x)*); | ||
| 111 | #[cfg(feature = "defmt")] | ||
| 112 | ::defmt::trace!($s $(, $x)*); | ||
| 113 | #[cfg(not(any(feature = "log", feature="defmt")))] | ||
| 114 | let _ = ($( & $x ),*); | ||
| 115 | } | ||
| 116 | }; | ||
| 117 | } | ||
| 118 | |||
| 119 | macro_rules! debug { | ||
| 120 | ($s:literal $(, $x:expr)* $(,)?) => { | ||
| 121 | { | ||
| 122 | #[cfg(feature = "log")] | ||
| 123 | ::log::debug!($s $(, $x)*); | ||
| 124 | #[cfg(feature = "defmt")] | ||
| 125 | ::defmt::debug!($s $(, $x)*); | ||
| 126 | #[cfg(not(any(feature = "log", feature="defmt")))] | ||
| 127 | let _ = ($( & $x ),*); | ||
| 128 | } | ||
| 129 | }; | ||
| 130 | } | ||
| 131 | |||
| 132 | macro_rules! info { | ||
| 133 | ($s:literal $(, $x:expr)* $(,)?) => { | ||
| 134 | { | ||
| 135 | #[cfg(feature = "log")] | ||
| 136 | ::log::info!($s $(, $x)*); | ||
| 137 | #[cfg(feature = "defmt")] | ||
| 138 | ::defmt::info!($s $(, $x)*); | ||
| 139 | #[cfg(not(any(feature = "log", feature="defmt")))] | ||
| 140 | let _ = ($( & $x ),*); | ||
| 141 | } | ||
| 142 | }; | ||
| 143 | } | ||
| 144 | |||
| 145 | macro_rules! warn { | ||
| 146 | ($s:literal $(, $x:expr)* $(,)?) => { | ||
| 147 | { | ||
| 148 | #[cfg(feature = "log")] | ||
| 149 | ::log::warn!($s $(, $x)*); | ||
| 150 | #[cfg(feature = "defmt")] | ||
| 151 | ::defmt::warn!($s $(, $x)*); | ||
| 152 | #[cfg(not(any(feature = "log", feature="defmt")))] | ||
| 153 | let _ = ($( & $x ),*); | ||
| 154 | } | ||
| 155 | }; | ||
| 156 | } | ||
| 157 | |||
| 158 | macro_rules! error { | ||
| 159 | ($s:literal $(, $x:expr)* $(,)?) => { | ||
| 160 | { | ||
| 161 | #[cfg(feature = "log")] | ||
| 162 | ::log::error!($s $(, $x)*); | ||
| 163 | #[cfg(feature = "defmt")] | ||
| 164 | ::defmt::error!($s $(, $x)*); | ||
| 165 | #[cfg(not(any(feature = "log", feature="defmt")))] | ||
| 166 | let _ = ($( & $x ),*); | ||
| 167 | } | ||
| 168 | }; | ||
| 169 | } | ||
| 170 | |||
| 171 | #[cfg(feature = "defmt")] | ||
| 172 | macro_rules! unwrap { | ||
| 173 | ($($x:tt)*) => { | ||
| 174 | ::defmt::unwrap!($($x)*) | ||
| 175 | }; | ||
| 176 | } | ||
| 177 | |||
| 178 | #[cfg(not(feature = "defmt"))] | ||
| 179 | macro_rules! unwrap { | ||
| 180 | ($arg:expr) => { | ||
| 181 | match $crate::fmt::Try::into_result($arg) { | ||
| 182 | ::core::result::Result::Ok(t) => t, | ||
| 183 | ::core::result::Result::Err(e) => { | ||
| 184 | ::core::panic!("unwrap of `{}` failed: {:?}", ::core::stringify!($arg), e); | ||
| 185 | } | ||
| 186 | } | ||
| 187 | }; | ||
| 188 | ($arg:expr, $($msg:expr),+ $(,)? ) => { | ||
| 189 | match $crate::fmt::Try::into_result($arg) { | ||
| 190 | ::core::result::Result::Ok(t) => t, | ||
| 191 | ::core::result::Result::Err(e) => { | ||
| 192 | ::core::panic!("unwrap of `{}` failed: {}: {:?}", ::core::stringify!($arg), ::core::format_args!($($msg,)*), e); | ||
| 193 | } | ||
| 194 | } | ||
| 195 | } | ||
| 196 | } | ||
| 197 | |||
| 198 | #[derive(Debug, Copy, Clone, Eq, PartialEq)] | ||
| 199 | pub struct NoneError; | ||
| 200 | |||
| 201 | pub trait Try { | ||
| 202 | type Ok; | ||
| 203 | type Error; | ||
| 204 | fn into_result(self) -> Result<Self::Ok, Self::Error>; | ||
| 205 | } | ||
| 206 | |||
| 207 | impl<T> Try for Option<T> { | ||
| 208 | type Ok = T; | ||
| 209 | type Error = NoneError; | ||
| 210 | |||
| 211 | #[inline] | ||
| 212 | fn into_result(self) -> Result<T, NoneError> { | ||
| 213 | self.ok_or(NoneError) | ||
| 214 | } | ||
| 215 | } | ||
| 216 | |||
| 217 | impl<T, E> Try for Result<T, E> { | ||
| 218 | type Ok = T; | ||
| 219 | type Error = E; | ||
| 220 | |||
| 221 | #[inline] | ||
| 222 | fn into_result(self) -> Self { | ||
| 223 | self | ||
| 224 | } | ||
| 225 | } | ||
diff --git a/embassy-usb-hid/src/lib.rs b/embassy-usb-hid/src/lib.rs new file mode 100644 index 000000000..c1f70c32a --- /dev/null +++ b/embassy-usb-hid/src/lib.rs | |||
| @@ -0,0 +1,529 @@ | |||
| 1 | #![no_std] | ||
| 2 | #![feature(generic_associated_types)] | ||
| 3 | #![feature(type_alias_impl_trait)] | ||
| 4 | |||
| 5 | //! Implements HID functionality for a usb-device device. | ||
| 6 | |||
| 7 | // This mod MUST go first, so that the others see its macros. | ||
| 8 | pub(crate) mod fmt; | ||
| 9 | |||
| 10 | use core::mem::MaybeUninit; | ||
| 11 | |||
| 12 | use embassy::channel::signal::Signal; | ||
| 13 | use embassy::time::Duration; | ||
| 14 | use embassy_usb::driver::{EndpointOut, ReadError}; | ||
| 15 | use embassy_usb::{ | ||
| 16 | control::{ControlHandler, InResponse, OutResponse, Request, RequestType}, | ||
| 17 | driver::{Driver, Endpoint, EndpointIn, WriteError}, | ||
| 18 | UsbDeviceBuilder, | ||
| 19 | }; | ||
| 20 | use futures_util::future::{select, Either}; | ||
| 21 | use futures_util::pin_mut; | ||
| 22 | #[cfg(feature = "usbd-hid")] | ||
| 23 | use ssmarshal::serialize; | ||
| 24 | #[cfg(feature = "usbd-hid")] | ||
| 25 | use usbd_hid::descriptor::AsInputReport; | ||
| 26 | |||
| 27 | const USB_CLASS_HID: u8 = 0x03; | ||
| 28 | const USB_SUBCLASS_NONE: u8 = 0x00; | ||
| 29 | const USB_PROTOCOL_NONE: u8 = 0x00; | ||
| 30 | |||
| 31 | // HID | ||
| 32 | const HID_DESC_DESCTYPE_HID: u8 = 0x21; | ||
| 33 | const HID_DESC_DESCTYPE_HID_REPORT: u8 = 0x22; | ||
| 34 | const HID_DESC_SPEC_1_10: [u8; 2] = [0x10, 0x01]; | ||
| 35 | const HID_DESC_COUNTRY_UNSPEC: u8 = 0x00; | ||
| 36 | |||
| 37 | const HID_REQ_SET_IDLE: u8 = 0x0a; | ||
| 38 | const HID_REQ_GET_IDLE: u8 = 0x02; | ||
| 39 | const HID_REQ_GET_REPORT: u8 = 0x01; | ||
| 40 | const HID_REQ_SET_REPORT: u8 = 0x09; | ||
| 41 | const HID_REQ_GET_PROTOCOL: u8 = 0x03; | ||
| 42 | const HID_REQ_SET_PROTOCOL: u8 = 0x0b; | ||
| 43 | |||
| 44 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||
| 45 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] | ||
| 46 | pub enum ReportId { | ||
| 47 | In(u8), | ||
| 48 | Out(u8), | ||
| 49 | Feature(u8), | ||
| 50 | } | ||
| 51 | |||
| 52 | impl ReportId { | ||
| 53 | fn try_from(value: u16) -> Result<Self, ()> { | ||
| 54 | match value >> 8 { | ||
| 55 | 1 => Ok(ReportId::In(value as u8)), | ||
| 56 | 2 => Ok(ReportId::Out(value as u8)), | ||
| 57 | 3 => Ok(ReportId::Feature(value as u8)), | ||
| 58 | _ => Err(()), | ||
| 59 | } | ||
| 60 | } | ||
| 61 | } | ||
| 62 | |||
| 63 | pub struct State<'a, const IN_N: usize, const OUT_N: usize, const FEATURE_N: usize> { | ||
| 64 | control: MaybeUninit<Control<'a, OUT_N, FEATURE_N>>, | ||
| 65 | out_signal: Signal<(usize, [u8; OUT_N])>, | ||
| 66 | feature_signal: Signal<(usize, [u8; FEATURE_N])>, | ||
| 67 | } | ||
| 68 | |||
| 69 | impl<'a, const IN_N: usize, const OUT_N: usize, const FEATURE_N: usize> | ||
| 70 | State<'a, IN_N, OUT_N, FEATURE_N> | ||
| 71 | { | ||
| 72 | pub fn new() -> Self { | ||
| 73 | State { | ||
| 74 | control: MaybeUninit::uninit(), | ||
| 75 | out_signal: Signal::new(), | ||
| 76 | feature_signal: Signal::new(), | ||
| 77 | } | ||
| 78 | } | ||
| 79 | } | ||
| 80 | |||
| 81 | pub struct HidClass< | ||
| 82 | 'd, | ||
| 83 | D: Driver<'d>, | ||
| 84 | const IN_N: usize, | ||
| 85 | const OUT_N: usize, | ||
| 86 | const FEATURE_N: usize, | ||
| 87 | > { | ||
| 88 | input: ReportWriter<'d, D, IN_N>, | ||
| 89 | output: ReportReader<'d, D, OUT_N>, | ||
| 90 | feature: ReportReader<'d, D, FEATURE_N>, | ||
| 91 | } | ||
| 92 | |||
| 93 | impl<'d, D: Driver<'d>, const IN_N: usize, const OUT_N: usize, const FEATURE_N: usize> | ||
| 94 | HidClass<'d, D, IN_N, OUT_N, FEATURE_N> | ||
| 95 | { | ||
| 96 | /// Creates a new HidClass. | ||
| 97 | /// | ||
| 98 | /// poll_ms configures how frequently the host should poll for reading/writing | ||
| 99 | /// HID reports. A lower value means better throughput & latency, at the expense | ||
| 100 | /// of CPU on the device & bandwidth on the bus. A value of 10 is reasonable for | ||
| 101 | /// high performance uses, and a value of 255 is good for best-effort usecases. | ||
| 102 | /// | ||
| 103 | /// This allocates two endpoints (IN and OUT). | ||
| 104 | /// See new_ep_in (IN endpoint only) and new_ep_out (OUT endpoint only) to only create a single | ||
| 105 | /// endpoint. | ||
| 106 | pub fn new( | ||
| 107 | builder: &mut UsbDeviceBuilder<'d, D>, | ||
| 108 | state: &'d mut State<'d, IN_N, OUT_N, FEATURE_N>, | ||
| 109 | report_descriptor: &'static [u8], | ||
| 110 | request_handler: Option<&'d dyn RequestHandler>, | ||
| 111 | poll_ms: u8, | ||
| 112 | ) -> Self { | ||
| 113 | let ep_out = Some(builder.alloc_interrupt_endpoint_out(64, poll_ms)); | ||
| 114 | let ep_in = Some(builder.alloc_interrupt_endpoint_in(64, poll_ms)); | ||
| 115 | Self::new_inner( | ||
| 116 | builder, | ||
| 117 | state, | ||
| 118 | report_descriptor, | ||
| 119 | request_handler, | ||
| 120 | ep_out, | ||
| 121 | ep_in, | ||
| 122 | ) | ||
| 123 | } | ||
| 124 | |||
| 125 | /// Creates a new HidClass with the provided UsbBus & HID report descriptor. | ||
| 126 | /// See new() for more details. | ||
| 127 | pub fn new_ep_in( | ||
| 128 | builder: &mut UsbDeviceBuilder<'d, D>, | ||
| 129 | state: &'d mut State<'d, IN_N, OUT_N, FEATURE_N>, | ||
| 130 | report_descriptor: &'static [u8], | ||
| 131 | request_handler: Option<&'d dyn RequestHandler>, | ||
| 132 | poll_ms: u8, | ||
| 133 | ) -> Self { | ||
| 134 | let ep_out = None; | ||
| 135 | let ep_in = Some(builder.alloc_interrupt_endpoint_in(64, poll_ms)); | ||
| 136 | Self::new_inner( | ||
| 137 | builder, | ||
| 138 | state, | ||
| 139 | report_descriptor, | ||
| 140 | request_handler, | ||
| 141 | ep_out, | ||
| 142 | ep_in, | ||
| 143 | ) | ||
| 144 | } | ||
| 145 | |||
| 146 | /// Creates a new HidClass with the provided UsbBus & HID report descriptor. | ||
| 147 | /// See new() for more details. | ||
| 148 | pub fn new_ep_out( | ||
| 149 | builder: &mut UsbDeviceBuilder<'d, D>, | ||
| 150 | state: &'d mut State<'d, IN_N, OUT_N, FEATURE_N>, | ||
| 151 | report_descriptor: &'static [u8], | ||
| 152 | request_handler: Option<&'d dyn RequestHandler>, | ||
| 153 | poll_ms: u8, | ||
| 154 | ) -> Self { | ||
| 155 | let ep_out = Some(builder.alloc_interrupt_endpoint_out(64, poll_ms)); | ||
| 156 | let ep_in = None; | ||
| 157 | Self::new_inner( | ||
| 158 | builder, | ||
| 159 | state, | ||
| 160 | report_descriptor, | ||
| 161 | request_handler, | ||
| 162 | ep_out, | ||
| 163 | ep_in, | ||
| 164 | ) | ||
| 165 | } | ||
| 166 | |||
| 167 | fn new_inner( | ||
| 168 | builder: &mut UsbDeviceBuilder<'d, D>, | ||
| 169 | state: &'d mut State<'d, IN_N, OUT_N, FEATURE_N>, | ||
| 170 | report_descriptor: &'static [u8], | ||
| 171 | request_handler: Option<&'d dyn RequestHandler>, | ||
| 172 | ep_out: Option<D::EndpointOut>, | ||
| 173 | ep_in: Option<D::EndpointIn>, | ||
| 174 | ) -> Self { | ||
| 175 | let control = state.control.write(Control::new( | ||
| 176 | report_descriptor, | ||
| 177 | &state.out_signal, | ||
| 178 | &state.feature_signal, | ||
| 179 | request_handler, | ||
| 180 | )); | ||
| 181 | |||
| 182 | control.build(builder, ep_out.as_ref(), ep_in.as_ref()); | ||
| 183 | |||
| 184 | Self { | ||
| 185 | input: ReportWriter { ep_in }, | ||
| 186 | output: ReportReader { | ||
| 187 | ep_out, | ||
| 188 | receiver: &state.out_signal, | ||
| 189 | }, | ||
| 190 | feature: ReportReader { | ||
| 191 | ep_out: None, | ||
| 192 | receiver: &state.feature_signal, | ||
| 193 | }, | ||
| 194 | } | ||
| 195 | } | ||
| 196 | |||
| 197 | /// Gets the [`ReportWriter`] for input reports. | ||
| 198 | /// | ||
| 199 | /// **Note:** If the `HidClass` was created with [`new_ep_out()`](Self::new_ep_out) | ||
| 200 | /// this writer will be useless as no endpoint is availabe to send reports. | ||
| 201 | pub fn input(&mut self) -> &mut ReportWriter<'d, D, IN_N> { | ||
| 202 | &mut self.input | ||
| 203 | } | ||
| 204 | |||
| 205 | /// Gets the [`ReportReader`] for output reports. | ||
| 206 | pub fn output(&mut self) -> &mut ReportReader<'d, D, OUT_N> { | ||
| 207 | &mut self.output | ||
| 208 | } | ||
| 209 | |||
| 210 | /// Gets the [`ReportReader`] for feature reports. | ||
| 211 | pub fn feature(&mut self) -> &mut ReportReader<'d, D, FEATURE_N> { | ||
| 212 | &mut self.feature | ||
| 213 | } | ||
| 214 | |||
| 215 | /// Splits this `HidClass` into seperate readers/writers for each report type. | ||
| 216 | pub fn split( | ||
| 217 | self, | ||
| 218 | ) -> ( | ||
| 219 | ReportWriter<'d, D, IN_N>, | ||
| 220 | ReportReader<'d, D, OUT_N>, | ||
| 221 | ReportReader<'d, D, FEATURE_N>, | ||
| 222 | ) { | ||
| 223 | (self.input, self.output, self.feature) | ||
| 224 | } | ||
| 225 | } | ||
| 226 | |||
| 227 | pub struct ReportWriter<'d, D: Driver<'d>, const N: usize> { | ||
| 228 | ep_in: Option<D::EndpointIn>, | ||
| 229 | } | ||
| 230 | |||
| 231 | pub struct ReportReader<'d, D: Driver<'d>, const N: usize> { | ||
| 232 | ep_out: Option<D::EndpointOut>, | ||
| 233 | receiver: &'d Signal<(usize, [u8; N])>, | ||
| 234 | } | ||
| 235 | |||
| 236 | impl<'d, D: Driver<'d>, const N: usize> ReportWriter<'d, D, N> { | ||
| 237 | /// Tries to write an input report by serializing the given report structure. | ||
| 238 | /// | ||
| 239 | /// Panics if no endpoint is available. | ||
| 240 | #[cfg(feature = "usbd-hid")] | ||
| 241 | pub async fn serialize<IR: AsInputReport>(&mut self, r: &IR) -> Result<(), WriteError> { | ||
| 242 | let mut buf: [u8; N] = [0; N]; | ||
| 243 | let size = match serialize(&mut buf, r) { | ||
| 244 | Ok(size) => size, | ||
| 245 | Err(_) => return Err(WriteError::BufferOverflow), | ||
| 246 | }; | ||
| 247 | self.write(&buf[0..size]).await | ||
| 248 | } | ||
| 249 | |||
| 250 | /// Writes `report` to its interrupt endpoint. | ||
| 251 | /// | ||
| 252 | /// Panics if no endpoint is available. | ||
| 253 | pub async fn write(&mut self, report: &[u8]) -> Result<(), WriteError> { | ||
| 254 | assert!(report.len() <= N); | ||
| 255 | |||
| 256 | let ep = self | ||
| 257 | .ep_in | ||
| 258 | .as_mut() | ||
| 259 | .expect("An IN endpoint must be allocated to write input reports."); | ||
| 260 | |||
| 261 | let max_packet_size = usize::from(ep.info().max_packet_size); | ||
| 262 | let zlp_needed = report.len() < N && (report.len() % max_packet_size == 0); | ||
| 263 | for chunk in report.chunks(max_packet_size) { | ||
| 264 | ep.write(chunk).await?; | ||
| 265 | } | ||
| 266 | |||
| 267 | if zlp_needed { | ||
| 268 | ep.write(&[]).await?; | ||
| 269 | } | ||
| 270 | |||
| 271 | Ok(()) | ||
| 272 | } | ||
| 273 | } | ||
| 274 | |||
| 275 | impl<'d, D: Driver<'d>, const N: usize> ReportReader<'d, D, N> { | ||
| 276 | pub async fn read(&mut self, buf: &mut [u8]) -> Result<usize, ReadError> { | ||
| 277 | assert!(buf.len() >= N); | ||
| 278 | if let Some(ep) = &mut self.ep_out { | ||
| 279 | let max_packet_size = usize::from(ep.info().max_packet_size); | ||
| 280 | |||
| 281 | let mut chunks = buf.chunks_mut(max_packet_size); | ||
| 282 | |||
| 283 | // Wait until we've received a chunk from the endpoint or a report from a SET_REPORT control request | ||
| 284 | let (mut total, data) = { | ||
| 285 | let chunk = unwrap!(chunks.next()); | ||
| 286 | let fut1 = ep.read(chunk); | ||
| 287 | pin_mut!(fut1); | ||
| 288 | match select(fut1, self.receiver.wait()).await { | ||
| 289 | Either::Left((Ok(size), _)) => (size, None), | ||
| 290 | Either::Left((Err(err), _)) => return Err(err), | ||
| 291 | Either::Right(((size, data), _)) => (size, Some(data)), | ||
| 292 | } | ||
| 293 | }; | ||
| 294 | |||
| 295 | if let Some(data) = data { | ||
| 296 | buf[0..total].copy_from_slice(&data[0..total]); | ||
| 297 | Ok(total) | ||
| 298 | } else { | ||
| 299 | for chunk in chunks { | ||
| 300 | let size = ep.read(chunk).await?; | ||
| 301 | total += size; | ||
| 302 | if size < max_packet_size || total == N { | ||
| 303 | break; | ||
| 304 | } | ||
| 305 | } | ||
| 306 | Ok(total) | ||
| 307 | } | ||
| 308 | } else { | ||
| 309 | let (total, data) = self.receiver.wait().await; | ||
| 310 | buf[0..total].copy_from_slice(&data[0..total]); | ||
| 311 | Ok(total) | ||
| 312 | } | ||
| 313 | } | ||
| 314 | } | ||
| 315 | |||
| 316 | pub trait RequestHandler { | ||
| 317 | /// Read the value of report `id` into `buf` returning the size. | ||
| 318 | /// | ||
| 319 | /// Returns `None` if `id` is invalid or no data is available. | ||
| 320 | fn get_report(&self, id: ReportId, buf: &mut [u8]) -> Option<usize> { | ||
| 321 | let _ = (id, buf); | ||
| 322 | None | ||
| 323 | } | ||
| 324 | |||
| 325 | /// Set the idle rate for `id` to `dur`. | ||
| 326 | /// | ||
| 327 | /// If `id` is `None`, set the idle rate of all input reports to `dur`. If | ||
| 328 | /// an indefinite duration is requested, `dur` will be set to `Duration::MAX`. | ||
| 329 | fn set_idle(&self, id: Option<ReportId>, dur: Duration) { | ||
| 330 | let _ = (id, dur); | ||
| 331 | } | ||
| 332 | |||
| 333 | /// Get the idle rate for `id`. | ||
| 334 | /// | ||
| 335 | /// If `id` is `None`, get the idle rate for all reports. Returning `None` | ||
| 336 | /// will reject the control request. Any duration above 1.020 seconds or 0 | ||
| 337 | /// will be returned as an indefinite idle rate. | ||
| 338 | fn get_idle(&self, id: Option<ReportId>) -> Option<Duration> { | ||
| 339 | let _ = id; | ||
| 340 | None | ||
| 341 | } | ||
| 342 | } | ||
| 343 | |||
| 344 | pub struct Control<'d, const OUT_N: usize, const FEATURE_N: usize> { | ||
| 345 | report_descriptor: &'static [u8], | ||
| 346 | out_signal: &'d Signal<(usize, [u8; OUT_N])>, | ||
| 347 | feature_signal: &'d Signal<(usize, [u8; FEATURE_N])>, | ||
| 348 | request_handler: Option<&'d dyn RequestHandler>, | ||
| 349 | hid_descriptor: [u8; 9], | ||
| 350 | } | ||
| 351 | |||
| 352 | impl<'a, const OUT_N: usize, const FEATURE_N: usize> Control<'a, OUT_N, FEATURE_N> { | ||
| 353 | fn new( | ||
| 354 | report_descriptor: &'static [u8], | ||
| 355 | out_signal: &'a Signal<(usize, [u8; OUT_N])>, | ||
| 356 | feature_signal: &'a Signal<(usize, [u8; FEATURE_N])>, | ||
| 357 | request_handler: Option<&'a dyn RequestHandler>, | ||
| 358 | ) -> Self { | ||
| 359 | Control { | ||
| 360 | report_descriptor, | ||
| 361 | out_signal, | ||
| 362 | feature_signal, | ||
| 363 | request_handler, | ||
| 364 | hid_descriptor: [ | ||
| 365 | // Length of buf inclusive of size prefix | ||
| 366 | 9, | ||
| 367 | // Descriptor type | ||
| 368 | HID_DESC_DESCTYPE_HID, | ||
| 369 | // HID Class spec version | ||
| 370 | HID_DESC_SPEC_1_10[0], | ||
| 371 | HID_DESC_SPEC_1_10[1], | ||
| 372 | // Country code not supported | ||
| 373 | HID_DESC_COUNTRY_UNSPEC, | ||
| 374 | // Number of following descriptors | ||
| 375 | 1, | ||
| 376 | // We have a HID report descriptor the host should read | ||
| 377 | HID_DESC_DESCTYPE_HID_REPORT, | ||
| 378 | // HID report descriptor size, | ||
| 379 | (report_descriptor.len() & 0xFF) as u8, | ||
| 380 | (report_descriptor.len() >> 8 & 0xFF) as u8, | ||
| 381 | ], | ||
| 382 | } | ||
| 383 | } | ||
| 384 | |||
| 385 | fn build<'d, D: Driver<'d>>( | ||
| 386 | &'d mut self, | ||
| 387 | builder: &mut UsbDeviceBuilder<'d, D>, | ||
| 388 | ep_out: Option<&D::EndpointOut>, | ||
| 389 | ep_in: Option<&D::EndpointIn>, | ||
| 390 | ) { | ||
| 391 | let len = self.report_descriptor.len(); | ||
| 392 | let if_num = builder.alloc_interface_with_handler(self); | ||
| 393 | |||
| 394 | builder.config_descriptor.interface( | ||
| 395 | if_num, | ||
| 396 | USB_CLASS_HID, | ||
| 397 | USB_SUBCLASS_NONE, | ||
| 398 | USB_PROTOCOL_NONE, | ||
| 399 | ); | ||
| 400 | |||
| 401 | // HID descriptor | ||
| 402 | builder.config_descriptor.write( | ||
| 403 | HID_DESC_DESCTYPE_HID, | ||
| 404 | &[ | ||
| 405 | // HID Class spec version | ||
| 406 | HID_DESC_SPEC_1_10[0], | ||
| 407 | HID_DESC_SPEC_1_10[1], | ||
| 408 | // Country code not supported | ||
| 409 | HID_DESC_COUNTRY_UNSPEC, | ||
| 410 | // Number of following descriptors | ||
| 411 | 1, | ||
| 412 | // We have a HID report descriptor the host should read | ||
| 413 | HID_DESC_DESCTYPE_HID_REPORT, | ||
| 414 | // HID report descriptor size, | ||
| 415 | (len & 0xFF) as u8, | ||
| 416 | (len >> 8 & 0xFF) as u8, | ||
| 417 | ], | ||
| 418 | ); | ||
| 419 | |||
| 420 | if let Some(ep) = ep_out { | ||
| 421 | builder.config_descriptor.endpoint(ep.info()); | ||
| 422 | } | ||
| 423 | if let Some(ep) = ep_in { | ||
| 424 | builder.config_descriptor.endpoint(ep.info()); | ||
| 425 | } | ||
| 426 | } | ||
| 427 | } | ||
| 428 | |||
| 429 | impl<'d, const OUT_N: usize, const FEATURE_N: usize> ControlHandler | ||
| 430 | for Control<'d, OUT_N, FEATURE_N> | ||
| 431 | { | ||
| 432 | fn reset(&mut self) {} | ||
| 433 | |||
| 434 | fn control_out(&mut self, req: embassy_usb::control::Request, data: &[u8]) -> OutResponse { | ||
| 435 | trace!("HID control_out {:?} {=[u8]:x}", req, data); | ||
| 436 | if let RequestType::Class = req.request_type { | ||
| 437 | match req.request { | ||
| 438 | HID_REQ_SET_IDLE => { | ||
| 439 | if let Some(handler) = self.request_handler.as_ref() { | ||
| 440 | let id = req.value as u8; | ||
| 441 | let id = (id != 0).then(|| ReportId::In(id)); | ||
| 442 | let dur = u64::from(req.value >> 8); | ||
| 443 | let dur = if dur == 0 { | ||
| 444 | Duration::MAX | ||
| 445 | } else { | ||
| 446 | Duration::from_millis(4 * dur) | ||
| 447 | }; | ||
| 448 | handler.set_idle(id, dur); | ||
| 449 | } | ||
| 450 | OutResponse::Accepted | ||
| 451 | } | ||
| 452 | HID_REQ_SET_REPORT => match ReportId::try_from(req.value) { | ||
| 453 | Ok(ReportId::In(_)) => OutResponse::Rejected, | ||
| 454 | Ok(ReportId::Out(_id)) => { | ||
| 455 | let mut buf = [0; OUT_N]; | ||
| 456 | buf[0..data.len()].copy_from_slice(data); | ||
| 457 | self.out_signal.signal((data.len(), buf)); | ||
| 458 | OutResponse::Accepted | ||
| 459 | } | ||
| 460 | Ok(ReportId::Feature(_id)) => { | ||
| 461 | let mut buf = [0; FEATURE_N]; | ||
| 462 | buf[0..data.len()].copy_from_slice(data); | ||
| 463 | self.feature_signal.signal((data.len(), buf)); | ||
| 464 | OutResponse::Accepted | ||
| 465 | } | ||
| 466 | Err(_) => OutResponse::Rejected, | ||
| 467 | }, | ||
| 468 | HID_REQ_SET_PROTOCOL => { | ||
| 469 | if req.value == 1 { | ||
| 470 | OutResponse::Accepted | ||
| 471 | } else { | ||
| 472 | warn!("HID Boot Protocol is unsupported."); | ||
| 473 | OutResponse::Rejected // UNSUPPORTED: Boot Protocol | ||
| 474 | } | ||
| 475 | } | ||
| 476 | _ => OutResponse::Rejected, | ||
| 477 | } | ||
| 478 | } else { | ||
| 479 | OutResponse::Rejected // UNSUPPORTED: SET_DESCRIPTOR | ||
| 480 | } | ||
| 481 | } | ||
| 482 | |||
| 483 | fn control_in<'a>(&'a mut self, req: Request, buf: &'a mut [u8]) -> InResponse<'a> { | ||
| 484 | trace!("HID control_in {:?}", req); | ||
| 485 | match (req.request_type, req.request) { | ||
| 486 | (RequestType::Standard, Request::GET_DESCRIPTOR) => match (req.value >> 8) as u8 { | ||
| 487 | HID_DESC_DESCTYPE_HID_REPORT => InResponse::Accepted(self.report_descriptor), | ||
| 488 | HID_DESC_DESCTYPE_HID => InResponse::Accepted(&self.hid_descriptor), | ||
| 489 | _ => InResponse::Rejected, | ||
| 490 | }, | ||
| 491 | (RequestType::Class, HID_REQ_GET_REPORT) => { | ||
| 492 | let size = match ReportId::try_from(req.value) { | ||
| 493 | Ok(id) => self | ||
| 494 | .request_handler | ||
| 495 | .as_ref() | ||
| 496 | .and_then(|x| x.get_report(id, buf)), | ||
| 497 | Err(_) => None, | ||
| 498 | }; | ||
| 499 | |||
| 500 | if let Some(size) = size { | ||
| 501 | InResponse::Accepted(&buf[0..size]) | ||
| 502 | } else { | ||
| 503 | InResponse::Rejected | ||
| 504 | } | ||
| 505 | } | ||
| 506 | (RequestType::Class, HID_REQ_GET_IDLE) => { | ||
| 507 | if let Some(handler) = self.request_handler.as_ref() { | ||
| 508 | let id = req.value as u8; | ||
| 509 | let id = (id != 0).then(|| ReportId::In(id)); | ||
| 510 | if let Some(dur) = handler.get_idle(id) { | ||
| 511 | let dur = u8::try_from(dur.as_millis() / 4).unwrap_or(0); | ||
| 512 | buf[0] = dur; | ||
| 513 | InResponse::Accepted(&buf[0..1]) | ||
| 514 | } else { | ||
| 515 | InResponse::Rejected | ||
| 516 | } | ||
| 517 | } else { | ||
| 518 | InResponse::Rejected | ||
| 519 | } | ||
| 520 | } | ||
| 521 | (RequestType::Class, HID_REQ_GET_PROTOCOL) => { | ||
| 522 | // UNSUPPORTED: Boot Protocol | ||
| 523 | buf[0] = 1; | ||
| 524 | InResponse::Accepted(&buf[0..1]) | ||
| 525 | } | ||
| 526 | _ => InResponse::Rejected, | ||
| 527 | } | ||
| 528 | } | ||
| 529 | } | ||
diff --git a/examples/nrf/Cargo.toml b/examples/nrf/Cargo.toml index aa30f3fa9..e944c171a 100644 --- a/examples/nrf/Cargo.toml +++ b/examples/nrf/Cargo.toml | |||
| @@ -6,13 +6,14 @@ version = "0.1.0" | |||
| 6 | 6 | ||
| 7 | [features] | 7 | [features] |
| 8 | default = ["nightly"] | 8 | default = ["nightly"] |
| 9 | nightly = ["embassy-nrf/nightly", "embassy-nrf/unstable-traits", "embassy-usb", "embassy-usb-serial"] | 9 | nightly = ["embassy-nrf/nightly", "embassy-nrf/unstable-traits", "embassy-usb", "embassy-usb-serial", "embassy-usb-hid"] |
| 10 | 10 | ||
| 11 | [dependencies] | 11 | [dependencies] |
| 12 | embassy = { version = "0.1.0", path = "../../embassy", features = ["defmt", "defmt-timestamp-uptime"] } | 12 | embassy = { version = "0.1.0", path = "../../embassy", features = ["defmt", "defmt-timestamp-uptime"] } |
| 13 | embassy-nrf = { version = "0.1.0", path = "../../embassy-nrf", features = ["defmt", "nrf52840", "time-driver-rtc1", "gpiote", "unstable-pac"] } | 13 | embassy-nrf = { version = "0.1.0", path = "../../embassy-nrf", features = ["defmt", "nrf52840", "time-driver-rtc1", "gpiote", "unstable-pac"] } |
| 14 | embassy-usb = { version = "0.1.0", path = "../../embassy-usb", features = ["defmt"], optional = true } | 14 | embassy-usb = { version = "0.1.0", path = "../../embassy-usb", features = ["defmt"], optional = true } |
| 15 | embassy-usb-serial = { version = "0.1.0", path = "../../embassy-usb-serial", features = ["defmt"], optional = true } | 15 | embassy-usb-serial = { version = "0.1.0", path = "../../embassy-usb-serial", features = ["defmt"], optional = true } |
| 16 | embassy-usb-hid = { version = "0.1.0", path = "../../embassy-usb-hid", features = ["defmt"], optional = true } | ||
| 16 | 17 | ||
| 17 | defmt = "0.3" | 18 | defmt = "0.3" |
| 18 | defmt-rtt = "0.3" | 19 | defmt-rtt = "0.3" |
| @@ -23,4 +24,5 @@ panic-probe = { version = "0.3", features = ["print-defmt"] } | |||
| 23 | futures = { version = "0.3.17", default-features = false, features = ["async-await"] } | 24 | futures = { version = "0.3.17", default-features = false, features = ["async-await"] } |
| 24 | rand = { version = "0.8.4", default-features = false } | 25 | rand = { version = "0.8.4", default-features = false } |
| 25 | embedded-storage = "0.3.0" | 26 | embedded-storage = "0.3.0" |
| 26 | 27 | usbd-hid = "0.5.2" | |
| 28 | serde = { version = "1.0.136", default-features = false } | ||
diff --git a/examples/nrf/src/bin/usb_hid.rs b/examples/nrf/src/bin/usb_hid.rs new file mode 100644 index 000000000..1fd056d00 --- /dev/null +++ b/examples/nrf/src/bin/usb_hid.rs | |||
| @@ -0,0 +1,131 @@ | |||
| 1 | #![no_std] | ||
| 2 | #![no_main] | ||
| 3 | #![feature(generic_associated_types)] | ||
| 4 | #![feature(type_alias_impl_trait)] | ||
| 5 | |||
| 6 | #[path = "../example_common.rs"] | ||
| 7 | mod example_common; | ||
| 8 | |||
| 9 | use core::mem; | ||
| 10 | use defmt::*; | ||
| 11 | use embassy::executor::Spawner; | ||
| 12 | use embassy::time::{Duration, Timer}; | ||
| 13 | use embassy_nrf::interrupt; | ||
| 14 | use embassy_nrf::pac; | ||
| 15 | use embassy_nrf::usb::Driver; | ||
| 16 | use embassy_nrf::Peripherals; | ||
| 17 | use embassy_usb::{Config, UsbDeviceBuilder}; | ||
| 18 | use embassy_usb_hid::{HidClass, ReportId, RequestHandler, State}; | ||
| 19 | use futures::future::join; | ||
| 20 | use usbd_hid::descriptor::{MouseReport, SerializedDescriptor}; | ||
| 21 | |||
| 22 | #[embassy::main] | ||
| 23 | async fn main(_spawner: Spawner, p: Peripherals) { | ||
| 24 | let clock: pac::CLOCK = unsafe { mem::transmute(()) }; | ||
| 25 | let power: pac::POWER = unsafe { mem::transmute(()) }; | ||
| 26 | |||
| 27 | info!("Enabling ext hfosc..."); | ||
| 28 | clock.tasks_hfclkstart.write(|w| unsafe { w.bits(1) }); | ||
| 29 | while clock.events_hfclkstarted.read().bits() != 1 {} | ||
| 30 | |||
| 31 | info!("Waiting for vbus..."); | ||
| 32 | while !power.usbregstatus.read().vbusdetect().is_vbus_present() {} | ||
| 33 | info!("vbus OK"); | ||
| 34 | |||
| 35 | // Create the driver, from the HAL. | ||
| 36 | let irq = interrupt::take!(USBD); | ||
| 37 | let driver = Driver::new(p.USBD, irq); | ||
| 38 | |||
| 39 | // Create embassy-usb Config | ||
| 40 | let mut config = Config::new(0xc0de, 0xcafe); | ||
| 41 | config.manufacturer = Some("Tactile Engineering"); | ||
| 42 | config.product = Some("Testy"); | ||
| 43 | config.serial_number = Some("12345678"); | ||
| 44 | config.max_power = 100; | ||
| 45 | |||
| 46 | // Create embassy-usb DeviceBuilder using the driver and config. | ||
| 47 | // It needs some buffers for building the descriptors. | ||
| 48 | let mut device_descriptor = [0; 256]; | ||
| 49 | let mut config_descriptor = [0; 256]; | ||
| 50 | let mut bos_descriptor = [0; 256]; | ||
| 51 | let mut control_buf = [0; 16]; | ||
| 52 | let request_handler = MyRequestHandler {}; | ||
| 53 | |||
| 54 | let mut state = State::<5, 0, 0>::new(); | ||
| 55 | |||
| 56 | let mut builder = UsbDeviceBuilder::new( | ||
| 57 | driver, | ||
| 58 | config, | ||
| 59 | &mut device_descriptor, | ||
| 60 | &mut config_descriptor, | ||
| 61 | &mut bos_descriptor, | ||
| 62 | &mut control_buf, | ||
| 63 | ); | ||
| 64 | |||
| 65 | // Create classes on the builder. | ||
| 66 | // let mut class = CdcAcmClass::new(&mut builder, &mut state, 64); | ||
| 67 | let mut hid = HidClass::new( | ||
| 68 | &mut builder, | ||
| 69 | &mut state, | ||
| 70 | MouseReport::desc(), | ||
| 71 | Some(&request_handler), | ||
| 72 | 60, | ||
| 73 | ); | ||
| 74 | |||
| 75 | // Build the builder. | ||
| 76 | let mut usb = builder.build(); | ||
| 77 | |||
| 78 | // Run the USB device. | ||
| 79 | let usb_fut = usb.run(); | ||
| 80 | |||
| 81 | // Do stuff with the class! | ||
| 82 | let hid_fut = async { | ||
| 83 | loop { | ||
| 84 | Timer::after(Duration::from_millis(500)).await; | ||
| 85 | hid.input() | ||
| 86 | .serialize(&MouseReport { | ||
| 87 | buttons: 0, | ||
| 88 | x: 0, | ||
| 89 | y: 4, | ||
| 90 | wheel: 0, | ||
| 91 | pan: 0, | ||
| 92 | }) | ||
| 93 | .await | ||
| 94 | .unwrap(); | ||
| 95 | |||
| 96 | Timer::after(Duration::from_millis(500)).await; | ||
| 97 | hid.input() | ||
| 98 | .serialize(&MouseReport { | ||
| 99 | buttons: 0, | ||
| 100 | x: 0, | ||
| 101 | y: -4, | ||
| 102 | wheel: 0, | ||
| 103 | pan: 0, | ||
| 104 | }) | ||
| 105 | .await | ||
| 106 | .unwrap(); | ||
| 107 | } | ||
| 108 | }; | ||
| 109 | |||
| 110 | // Run everything concurrently. | ||
| 111 | // If we had made everything `'static` above instead, we could do this using separate tasks instead. | ||
| 112 | join(usb_fut, hid_fut).await; | ||
| 113 | } | ||
| 114 | |||
| 115 | struct MyRequestHandler {} | ||
| 116 | |||
| 117 | impl RequestHandler for MyRequestHandler { | ||
| 118 | fn get_report(&self, id: ReportId, _buf: &mut [u8]) -> Option<usize> { | ||
| 119 | info!("Get report for {:?}", id); | ||
| 120 | None | ||
| 121 | } | ||
| 122 | |||
| 123 | fn set_idle(&self, id: Option<ReportId>, dur: Duration) { | ||
| 124 | info!("Set idle rate for {:?} to {:?}", id, dur); | ||
| 125 | } | ||
| 126 | |||
| 127 | fn get_idle(&self, id: Option<ReportId>) -> Option<Duration> { | ||
| 128 | info!("Get idle rate for {:?}", id); | ||
| 129 | None | ||
| 130 | } | ||
| 131 | } | ||
