diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b00796..62cf216 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Breaking * Acess numeric form of `EndpointType` variants now require a `.to_bm_attributes()`. ([#60](https://github.com/rust-embedded-community/usb-device/pull/60)) * `DescriptorWriter::iad()` now requires a `Option` to optionally specify a string for describing the function ([#121](https://github.com/rust-embedded-community/usb-device/pull/121)) -* `.manufacturer()`, `.product()` and `.serial_number()` of `UsbDeviceBuilder` now require `&[&str]` to specify strings match with each LANGIDs supported by device. ([#122](https://github.com/rust-embedded-community/usb-device/pull/122)) +* `.manufacturer()`, `.product()` and `.serial_number()` of `UsbDeviceBuilder` are now replaced with the `strings()` function that accepts a `StringDescriptor` list to allow multilanguage support ([#122](https://github.com/rust-embedded-community/usb-device/pull/122)) +* Various methods of the `UsbDeviceBuilder` now return `Result<>` types instead of internally panicking. ### Changed * `EndpointType` enum now has fields for isochronous synchronization and usage ([#60](https://github.com/rust-embedded-community/usb-device/pull/60)). diff --git a/Cargo.toml b/Cargo.toml index 61ccf65..f24e810 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ repository = "https://github.com/mvirkkunen/usb-device" defmt = { version = "0.3", optional = true } portable-atomic = { version = "1.2.0", default-features = false } num_enum = { version = "0.6.1", default-features = false } +heapless = "0.7" [dev-dependencies] rusb = "0.9.1" diff --git a/src/descriptor.rs b/src/descriptor.rs index 765f076..a1a2a17 100644 --- a/src/descriptor.rs +++ b/src/descriptor.rs @@ -116,11 +116,29 @@ impl DescriptorWriter<'_> { config.product_id as u8, (config.product_id >> 8) as u8, // idProduct config.device_release as u8, - (config.device_release >> 8) as u8, // bcdDevice - config.manufacturer.map_or(0, |_| 1), // iManufacturer - config.product.map_or(0, |_| 2), // iProduct - config.serial_number.map_or(0, |_| 3), // iSerialNumber - 1, // bNumConfigurations + (config.device_release >> 8) as u8, // bcdDevice + config.string_descriptors.first().map_or(0, |lang| { + if lang.manufacturer.is_some() { + 1 + } else { + 0 + } + }), + config.string_descriptors.first().map_or(0, |lang| { + if lang.product.is_some() { + 2 + } else { + 0 + } + }), + config.string_descriptors.first().map_or(0, |lang| { + if lang.serial.is_some() { + 3 + } else { + 0 + } + }), + 1, // bNumConfigurations ], ) } diff --git a/src/descriptor/lang_id.rs b/src/descriptor/lang_id.rs index e56a466..588c99b 100644 --- a/src/descriptor/lang_id.rs +++ b/src/descriptor/lang_id.rs @@ -15,7 +15,7 @@ impl From for u16 { } #[allow(missing_docs)] -#[derive(Clone, Copy, PartialEq, TryFromPrimitive)] +#[derive(Clone, Copy, PartialEq, Debug, TryFromPrimitive)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] #[repr(u16)] pub enum LangID { diff --git a/src/device.rs b/src/device.rs index 06cb55b..e1ca323 100644 --- a/src/device.rs +++ b/src/device.rs @@ -3,7 +3,7 @@ use crate::class::{ControlIn, ControlOut, UsbClass}; use crate::control; use crate::control_pipe::ControlPipe; use crate::descriptor::{descriptor_type, lang_id::LangID, BosWriter, DescriptorWriter}; -pub use crate::device_builder::{UsbDeviceBuilder, UsbVidPid}; +pub use crate::device_builder::{StringDescriptors, UsbDeviceBuilder, UsbVidPid}; use crate::endpoint::{EndpointAddress, EndpointType}; use crate::{Result, UsbDirection}; use core::convert::TryFrom; @@ -64,10 +64,7 @@ pub(crate) struct Config<'a> { pub product_id: u16, pub usb_rev: UsbRev, pub device_release: u16, - pub extra_lang_ids: Option<&'a [LangID]>, - pub manufacturer: Option<&'a [&'a str]>, - pub product: Option<&'a [&'a str]>, - pub serial_number: Option<&'a [&'a str]>, + pub string_descriptors: heapless::Vec, 16>, pub self_powered: bool, pub supports_remote_wakeup: bool, pub composite_with_iads: bool, @@ -548,97 +545,66 @@ impl UsbDevice<'_, B> { descriptor_type::STRING => match index { // first STRING Request 0 => { - if let Some(extra_lang_ids) = config.extra_lang_ids { - let mut lang_id_bytes = [0u8; 32]; - - lang_id_bytes - .chunks_exact_mut(2) - .zip([LangID::EN_US].iter().chain(extra_lang_ids.iter())) - .for_each(|(buffer, lang_id)| { - buffer.copy_from_slice(&u16::from(lang_id).to_le_bytes()); - }); - - accept_writer(xfer, |w| { - w.write( - descriptor_type::STRING, - &lang_id_bytes[0..(1 + extra_lang_ids.len()) * 2], - ) - }) - } else { - accept_writer(xfer, |w| { - w.write( - descriptor_type::STRING, - &u16::from(LangID::EN_US).to_le_bytes(), - ) - }) + let mut lang_id_bytes = [0u8; 32]; + for (lang, buf) in config + .string_descriptors + .iter() + .zip(lang_id_bytes.chunks_exact_mut(2)) + { + buf.copy_from_slice(&u16::from(lang.id).to_le_bytes()); } + accept_writer(xfer, |w| { + w.write( + descriptor_type::STRING, + &lang_id_bytes[..config.string_descriptors.len() * 2], + ) + }) } // rest STRING Requests _ => { - let s = match LangID::try_from(req.index) { + let lang_id = match LangID::try_from(req.index) { Err(_err) => { #[cfg(feature = "defmt")] defmt::warn!( "Receive unknown LANGID {:#06X}, reject the request", _err.number ); - None + xfer.reject().ok(); + return; } - Ok(req_lang_id) => { - if index <= 3 { - // for Manufacture, Product and Serial - - // construct the list of lang_ids full supported by device - let mut lang_id_list: [Option; 16] = [None; 16]; - match config.extra_lang_ids { - None => lang_id_list[0] = Some(LangID::EN_US), - Some(extra_lang_ids) => { - lang_id_list - .iter_mut() - .zip( - [LangID::EN_US].iter().chain(extra_lang_ids.iter()), - ) - .for_each(|(item, lang_id)| *item = Some(*lang_id)); - } - }; - - let position = - lang_id_list.iter().fuse().position(|list_lang_id| { - matches!(*list_lang_id, Some(list_lang_id) if req_lang_id == list_lang_id) - }); - #[cfg(feature = "defmt")] - if position.is_none() { - // Since we construct the list of full supported lang_ids previously, - // we can safely reject requests which ask for other lang_id. - defmt::warn!( - "Receive unknown LANGID {:#06X}, reject the request", - req_lang_id - ); - } - position.and_then(|lang_id_list_index| { - match index { - 1 => config.manufacturer, - 2 => config.product, - 3 => config.serial_number, - _ => unreachable!(), - } - .map(|str_list| str_list[lang_id_list_index]) - }) - } else { - // for other custom STRINGs - - let index = StringIndex::new(index); - classes - .iter() - .find_map(|cls| cls.get_string(index, req_lang_id)) + Ok(req_lang_id) => req_lang_id, + }; + let string = match index { + // Manufacturer, product, and serial are handled directly here. + 1..=3 => { + let Some(lang) = config + .string_descriptors + .iter() + .find(|lang| lang.id == lang_id) + else { + xfer.reject().ok(); + return; + }; + + match index { + 1 => lang.manufacturer, + 2 => lang.product, + 3 => lang.serial, + _ => unreachable!(), } } + _ => { + let index = StringIndex::new(index); + classes + .iter() + .find_map(|cls| cls.get_string(index, lang_id)) + } }; - if let Some(s) = s { - accept_writer(xfer, |w| w.string(s)); + if let Some(string_descriptor) = string { + accept_writer(xfer, |w| w.string(string_descriptor)); } else { let _ = xfer.reject(); } diff --git a/src/device_builder.rs b/src/device_builder.rs index e2041b5..6b7cd0d 100644 --- a/src/device_builder.rs +++ b/src/device_builder.rs @@ -23,6 +23,59 @@ macro_rules! builder_fields { } } +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum BuilderError { + TooManyLanguages, + InvalidPacketSize, + PowerTooHigh, +} + +/// Provides basic string descriptors about the device, including the manufacturer, product name, +/// and serial number of the device in a specified language. +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct StringDescriptors<'a> { + pub(crate) id: LangID, + pub(crate) serial: Option<&'a str>, + pub(crate) product: Option<&'a str>, + pub(crate) manufacturer: Option<&'a str>, +} + +impl<'a> Default for StringDescriptors<'a> { + fn default() -> Self { + Self::new(LangID::EN_US) + } +} + +impl<'a> StringDescriptors<'a> { + /// Create a new descriptor list with the provided language. + pub fn new(lang_id: LangID) -> Self { + Self { + id: lang_id, + serial: None, + product: None, + manufacturer: None, + } + } + + /// Specify the serial number for this language. + pub fn serial_number(mut self, serial: &'a str) -> Self { + self.serial.replace(serial); + self + } + + /// Specify the manufacturer name for this language. + pub fn manufacturer(mut self, manufacturer: &'a str) -> Self { + self.manufacturer.replace(manufacturer); + self + } + + /// Specify the product name for this language. + pub fn product(mut self, product: &'a str) -> Self { + self.product.replace(product); + self + } +} + impl<'a, B: UsbBus> UsbDeviceBuilder<'a, B> { /// Creates a builder for constructing a new [`UsbDevice`]. pub fn new(alloc: &'a UsbBusAllocator, vid_pid: UsbVidPid) -> UsbDeviceBuilder<'a, B> { @@ -37,10 +90,7 @@ impl<'a, B: UsbBus> UsbDeviceBuilder<'a, B> { product_id: vid_pid.1, usb_rev: UsbRev::Usb210, device_release: 0x0010, - extra_lang_ids: None, - manufacturer: None, - product: None, - serial_number: None, + string_descriptors: heapless::Vec::new(), self_powered: false, supports_remote_wakeup: false, composite_with_iads: false, @@ -108,133 +158,17 @@ impl<'a, B: UsbBus> UsbDeviceBuilder<'a, B> { self } - /// Sets **extra** Language ID for device. + /// Specify the strings for the device. /// - /// Since "en_US"(0x0409) is implicitly embedded, you just need to fill other LangIDs - /// - /// Default: (none) - pub fn set_extra_lang_ids(mut self, extra_lang_ids: &'a [LangID]) -> Self { - if extra_lang_ids.is_empty() { - self.config.extra_lang_ids = None; - return self; - } - - assert!( - extra_lang_ids.len() < 16, - "Not support more than 15 extra LangIDs" - ); - - [ - self.config.manufacturer, - self.config.product, - self.config.serial_number, - ] - .iter() - .zip(["manufacturer", "product", "serial_number"].iter()) - .for_each(|(list, field_name)| { - // do list length check only if user already specify "manufacturer", "product" or "serial_number" - if let Some(list) = list { - assert!( - extra_lang_ids.len() == list.len() - 1, - "The length of \"extra_lang_id\" list should be one less than \"{}\" list", - field_name - ) - } - }); - - self.config.extra_lang_ids = Some(extra_lang_ids); - - self - } - - /// Sets the manufacturer name string descriptor. - /// - /// the first string should always be in English, the language of rest strings - /// should be pair with what inside [.extra_lang_ids()](Self::extra_lang_ids) - /// - /// Default: (none) - pub fn manufacturer(mut self, manufacturer_ls: &'a [&'a str]) -> Self { - if manufacturer_ls.is_empty() { - self.config.manufacturer = None; - return self; - } - - assert!( - manufacturer_ls.len() <= 16, - "Not support more than 16 \"manufacturer\"s" - ); - - // do list length check only if user already specify "extra_lang_ids" - if let Some(extra_lang_ids) = self.config.extra_lang_ids { - assert!( - manufacturer_ls.len() == extra_lang_ids.len() + 1, - "The length of \"product\" list should be one more than \"extra_lang_ids\" list", - ) - } - - self.config.manufacturer = Some(manufacturer_ls); - - self - } - - /// Sets the product name string descriptor. - /// - /// the first string should always be in English, the language of rest strings - /// should be pair with what inside [.extra_lang_ids()](Self::extra_lang_ids) - /// - /// Default: (none) - pub fn product(mut self, product_ls: &'a [&'a str]) -> Self { - if product_ls.is_empty() { - self.config.product = None; - return self; - } - - assert!( - product_ls.len() <= 16, - "Not support more than 16 \"product\"s" - ); - - // do list length check only if user already specify "extra_lang_ids" - if let Some(extra_lang_ids) = self.config.extra_lang_ids { - assert!( - product_ls.len() == extra_lang_ids.len() + 1, - "The length of \"product\" list should be one more than \"extra_lang_ids\" list", - ) - } - - self.config.product = Some(product_ls); - - self - } - - /// Sets the serial number string descriptor. - /// - /// the first string should always be in English, the language of rest strings - /// should be pair with what inside [.extra_lang_ids()](Self::extra_lang_ids) - /// - /// Default: (none) - pub fn serial_number(mut self, serial_number_ls: &'a [&'a str]) -> Self { - if serial_number_ls.is_empty() { - self.config.serial_number = None; - return self; - } - - assert!( - serial_number_ls.len() <= 16, - "Not support more than 16 \"serial_number\"s" - ); - - // do list length check only if user already specify "extra_lang_ids" - if let Some(extra_lang_ids) = self.config.extra_lang_ids { - assert!( - serial_number_ls.len() == extra_lang_ids.len() + 1, - "The length of \"serial_number\" list should be one more than \"extra_lang_ids\" list", - ) - } - - self.config.serial_number = Some(serial_number_ls); - - self + /// # Note + /// Up to 16 languages may be provided. + pub fn strings(mut self, descriptors: &[StringDescriptors<'a>]) -> Result { + // The 16 language limit comes from the size of the buffer used to provide the list of + // language descriptors to the host. + self.config.string_descriptors = + heapless::Vec::from_slice(descriptors).map_err(|_| BuilderError::TooManyLanguages)?; + + Ok(self) } /// Sets the maximum packet size in bytes for the control endpoint 0. @@ -244,14 +178,14 @@ impl<'a, B: UsbBus> UsbDeviceBuilder<'a, B> { /// which case using a larger packet size may be more efficient. /// /// Default: 8 bytes - pub fn max_packet_size_0(mut self, max_packet_size_0: u8) -> Self { + pub fn max_packet_size_0(mut self, max_packet_size_0: u8) -> Result { match max_packet_size_0 { 8 | 16 | 32 | 64 => {} - _ => panic!("invalid max_packet_size_0"), + _ => return Err(BuilderError::InvalidPacketSize), } self.config.max_packet_size_0 = max_packet_size_0; - self + Ok(self) } /// Sets the maximum current drawn from the USB bus by the device in milliamps. @@ -262,12 +196,12 @@ impl<'a, B: UsbBus> UsbDeviceBuilder<'a, B> { /// See also: `self_powered` /// /// Default: 100mA - pub fn max_power(mut self, max_power_ma: usize) -> Self { + pub fn max_power(mut self, max_power_ma: usize) -> Result { if max_power_ma > 500 { - panic!("max_power is too much") + return Err(BuilderError::PowerTooHigh); } self.config.max_power = (max_power_ma / 2) as u8; - self + Ok(self) } } diff --git a/src/lib.rs b/src/lib.rs index d50954d..f57e930 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -169,6 +169,8 @@ pub mod device; /// Creating USB descriptors pub mod descriptor; +pub use descriptor::lang_id::LangID; + /// Test USB class for testing USB driver implementations. Peripheral driver implementations should /// include an example called "test_class" that creates a device with this class to enable the /// driver to be tested with the test_class_host example in this crate. diff --git a/src/test_class.rs b/src/test_class.rs index ed3f63e..ec26f77 100644 --- a/src/test_class.rs +++ b/src/test_class.rs @@ -2,7 +2,7 @@ use crate::class_prelude::*; use crate::descriptor::lang_id::LangID; -use crate::device::{UsbDevice, UsbDeviceBuilder, UsbVidPid}; +use crate::device::{StringDescriptors, UsbDevice, UsbDeviceBuilder, UsbVidPid}; use crate::Result; use core::cmp; @@ -113,10 +113,13 @@ impl TestClass<'_, B> { usb_bus: &'a UsbBusAllocator, ) -> UsbDeviceBuilder<'a, B> { UsbDeviceBuilder::new(usb_bus, UsbVidPid(VID, PID)) - .manufacturer(&[MANUFACTURER]) - .product(&[PRODUCT]) - .serial_number(&[SERIAL_NUMBER]) + .strings(&[StringDescriptors::default() + .manufacturer(MANUFACTURER) + .product(PRODUCT) + .serial_number(SERIAL_NUMBER)]) + .unwrap() .max_packet_size_0(sizes::CONTROL_ENDPOINT) + .unwrap() } /// Must be called after polling the UsbDevice.