#devices #byte #protocols #byte-buffer #server #cortex-m #hid-io

nightly no-std hid-io-protocol

适用于服务器和设备的HID-IO协议实现。设备针对Cortex-M设备进行了优化,但可能适用于其他平台

7个版本

0.1.6 2023年9月7日
0.1.5 2023年6月5日
0.1.4 2022年11月29日
0.1.2 2022年6月12日
0.1.0 2021年11月12日

#2081嵌入式开发

38 每月下载次数
用于 6 个crate(直接使用3个)

MIT/Apache

175KB
3.5K SLoC

hid-io协议

docs.rs Crates.io Crates.io Crates.io

HID-IO服务器和设备协议实现

此库可以使用deviceserver功能标志集成到嵌入式和完整用户空间应用程序中。

hid-io协议库处理以下功能

  • 缓冲区分包和重组
  • 所有不同的数据包类型(数据、ACK、NAK、无ACK数据、同步以及继续的变体)
  • HID-IO命令处理(发送和接收)
  • 16位和32位命令ID

规范

HID-IO协议规范

API文档

请参阅docs.rs

构建

服务器库

cargo build
cargo build --release

设备库

cargo build --no-default-features --features device
cargo build --no-default-features --features device --release

使用方法

可以使用两种不同的方式将hid-io-protocol作为服务器库使用。

  1. 接收字节,组装缓冲区,处理消息,创建响应缓冲区,序列化缓冲区,发送字节
  2. 接收消息缓冲区,处理消息,创建响应缓冲区,发送缓冲区

在每种方式中,都会创建一个CommandInterface结构,并实现Commands trait。CommandInterface的实现是两种选项之间的区别。


选项1更简单,因为hid-io-protocol可以处理从hidraw接口的所有处理。设备库通常选择这种方式。

const BufChunk: usize = U64;
const IdLen: usize = U10;
const MessageLen: usize = U256;
const RxBuf: usize = U8;
const SerializationLen: usize = U276;
const TxBuf: usize = U8;

let ids = [
    HidIoCommandID::SupportedIDs,
    /* Add supported ids */
    /* This is the master list, if it's not listed here comamnds will not work */
];

let intf = CommandInterface::<TxBuf, RxBuf, BufChunk, MessageLen, {MessageLen - 1}, {MessageLen - 4}, SerializationLen, IdLen>::new(&ids).unwrap();
}

// The max length must equal BufChunk (e.g. 64 bytes)
// This may not be 64 bytes depending on your use-case and situation
// 63 bytes is common when you need to use a hid report id
let hidraw_buffer = read_hidraw();

// Enqueue bytes into buffer
intf.rx_bytebuf.enqueue(match Vec::from_slice(slice) {
    Ok(vec) => vec,
    Err(_) => {
        return HidioStatus::ErrorBufSizeTooSmall;
    }
}).unwrap();

// Process messages
// If any responses are created, they'll be sent out with intf.tx_bytebuf
intf.process_rx();

// Copy a single chunk from the tx_buffer
// You'll likely want to do this repeatedly until the buffer is empty
match intf.tx_bytebuf.dequeue() {
    Some(chunk) => {
        // Write to hidraw output buffer
        // Same size restrictions apply as above
        write_hidraw(chunk);
    }
    None => {}
}

struct CommandInterface<
    const TX: usize,
    const RX: usize,
    const N: usize,
    const H: usize,
    const HSUB1: usize,
    const HSUB4: usize,
    const S: usize,
    const ID: usize,
> {
    ids: Vec<HidIoCommandID, ID>,
    rx_bytebuf: buffer::Buffer<RX, N>,
    rx_packetbuf: HidIoPacketBuffer<H>,
    tx_bytebuf: buffer::Buffer<TX, N>,
    serial_buf: Vec<u8, S>,
}

impl<
        const TX: usize,
        const RX: usize,
        const N: usize,
        const H: usize,
        const HSUB1: usize,
        const HSUB4: usize,
        const S: usize,
        const ID: usize,
    > CommandInterface<TX, RX, N, H, HSUB1, HSUB4, S, ID>
{
    fn new(
        ids: &[HidIoCommandID],
    ) -> Result<CommandInterface<TX, RX, N, H, HSUB1, HSUB4, S, ID>, CommandError> {
        // Make sure we have a large enough id vec
        let ids = match Vec::from_slice(ids) {
            Ok(ids) => ids,
            Err(_) => {
                return Err(CommandError::IdVecTooSmall);
            }
        };

        let tx_bytebuf = buffer::Buffer::new();
        let rx_bytebuf = buffer::Buffer::new();
        let rx_packetbuf = HidIoPacketBuffer::new();
        let serial_buf = Vec::new();

        Ok(CommandInterface {
            ids,
            rx_bytebuf,
            rx_packetbuf,
            tx_bytebuf,
            serial_buf,
        })
    }

    /// Decode rx_bytebuf into a HidIoPacketBuffer
    /// Returns true if buffer ready, false if not
    fn rx_packetbuffer_decode(&mut self) -> Result<bool, CommandError> {
        loop {
            // Retrieve vec chunk
            if let Some(buf) = self.rx_bytebuf.dequeue() {
                // Decode chunk
                match self.rx_packetbuf.decode_packet(&buf) {
                    Ok(_recv) => {
                        // Only handle buffer if ready
                        if self.rx_packetbuf.done {
                            // Handle sync packet type
                            match self.rx_packetbuf.ptype {
                                HidIoPacketType::Sync => {
                                    // Clear buffer, packet missing
                                    self.rx_packetbuf.clear();
                                }
                                _ => {
                                    return Ok(true);
                                }
                            }
                        }
                    }
                    Err(e) => {
                        return Err(CommandError::PacketDecodeError(e));
                    }
                }
            } else {
                return Ok(false);
            }
        }
    }

    /// Process rx buffer until empty
    /// Handles flushing tx->rx, decoding, then processing buffers
    /// Returns the number of buffers processed
    pub fn process_rx(&mut self) -> Result<u8, CommandError> {
        // Decode bytes into buffer
        while (self.rx_packetbuffer_decode()? {
            // Process rx buffer
            self.rx_message_handling(self.rx_packetbuf.clone())?;

            // Clear buffer
            self.rx_packetbuf.clear();
        }

        Ok(cur)
    }
}

/// CommandInterface for Commands
/// TX - tx byte buffer size (in multiples of N)
/// RX - tx byte buffer size (in multiples of N)
/// N - Max payload length (HidIoPacketBuffer), used for default values
/// H - Max data payload length (HidIoPacketBuffer)
/// S - Serialization buffer size
/// ID - Max number of HidIoCommandIDs
impl<
        const TX: usize,
        const RX: usize,
        const N: usize,
        const H: usize,
        const HSUB1: usize,
        const HSUB4: usize,
        const S: usize,
        const ID: usize,
    > Commands<H, ID> for CommandInterface<TX, RX, N, H, HSUB1, HSUB4, S, ID> {
    fn default_packet_chunk(&self) -> u32 {
        N as u32
    }

    fn tx_packetbuffer_send(&mut self, buf: &mut HidIoPacketBuffer<H>) -> Result<(), CommandError> {
        let size = buf.serialized_len() as usize;
        if self.serial_buf.resize_default(size).is_err() {
            return Err(CommandError::SerializationVecTooSmall);
        }
        match buf.serialize_buffer(&mut self.serial_buf) {
            Ok(data) => data,
            Err(err) => {
                return Err(CommandError::SerializationFailed(err));
            }
        };

        // Add serialized data to buffer
        // May need to enqueue multiple packets depending how much
        // was serialized
        // The first byte is a serde type and is dropped
        let data = &self.serial_buf;
        for pos in (1..data.len()).step_by(N) {
            let len = core::cmp::min(N, data.len() - pos);
            match self
                .tx_bytebuf
                .enqueue(match Vec::from_slice(&data[pos..len + pos]) {
                    Ok(vec) => vec,
                    Err(_) => {
                        return Err(CommandError::TxBufferVecTooSmall);
                    }
                }) {
                Ok(_) => {}
                Err(_) => {
                    return Err(CommandError::TxBufferSendFailed);
                }
            }
        }
        Ok(())
    }
    fn supported_id(&self, id: HidIoCommandID) -> bool {
        /* Your implementation */
    }

    fn h0000_supported_ids_cmd(&mut self, _data: h0000::Cmd) -> Result<h0000::Ack<ID>, h0000::Nak> {
        /* Message specific commands are optional to implement */
    }
}

hid-io-core使用选项2,因为不同的线程处理字节接收和消息处理。您需要自己处理缓冲区的组装/解组装。

        struct CommandInterface {}
        impl Commands<mailbox::HidIoPacketBufferDataSize, 0> for CommandInterface {
            fn tx_packetbuffer_send(
                &mut self,
                buf: &mut mailbox::HidIoPacketBuffer,
            ) -> Result<(), CommandError> {
                /* Send command and wait for a reply */
                /* If sending an ACK/NAK or NA Data then there won't be a reply */

                if let Some(rcvmsg) = /* send buffer */ {
                    // Handle ack/nak
                    self.rx_message_handling(rcvmsg.data)?;
                }
                Ok(())
            }
            fn h0016_flashmode_ack(
                &mut self,
                data: h0016::Ack,
            ) -> Result<(), CommandError> {
                /* ACK */
                Ok(())
            }
            fn h0016_flashmode_nak(
                &mut self,
                data: h0016::Nak,
            ) -> Result<(), CommandError> {
                /* NAK */
                Ok(())
            }
        }
        let mut intf = CommandInterface {};

        // Send command
        intf.h0016_flashmode(h0016::Cmd {}).unwrap();

hidraw设置

当使用hid-io-protocol与设备固件一起使用时,您需要设置一个hidraw接口。对于USB、蓝牙(以及其他支持USB HID规范的设备),这应该是一样的。使用示例中的选项1有存根,指示hidraw rx和tx发生的位置。

HID描述符

以下是使用64字节数据包的USB 2.0 FS的hidraw HID描述符示例。

    0x06, 0x1C, 0xFF,    // Usage Page (Vendor Defined) 0xFF1C
    0x0A, 0x00, 0x11,    // Usage 0x1100
    0xA1, 0x01,          // Collection (Application)
    0x75, 0x08,          //   Report Size (8)
    0x15, 0x00,          //   Logical Minimum (0)
    0x26, 0xFF, 0x00,    //   Logical Maximum (255)

    0x95, 0x40,          //     Report Count (64)
    0x09, 0x01,          //     Usage (Output)
    0x91, 0x02,          //     Output (Data,Var,Abs)

    0x95, 0x40,          //     Report Count (64)
    0x09, 0x02,          //     Usage (Input)
    0x81, 0x02,          //     Input (Data,Var,Abs)

    0xC0,                // End Collection

也可以使用报告ID;但是报告计数应该调整,以确保您不会为单个hidraw报告发送多个USB数据包。相应地调整报告计数字段。

USB端点设置

以下是如何为上述hid描述符设置USB端点的示例。

// --- Vendor Specific / RAW I/O ---
// - 9 bytes -
	// interface descriptor, USB spec 9.6.5, page 267-269, Table 9-12
	9,                                      // bLength
	4,                                      // bDescriptorType
	RAWIO_INTERFACE,                        // bInterfaceNumber
	0,                                      // bAlternateSetting
	2,                                      // bNumEndpoints
	0x03,                                   // bInterfaceClass (0x03)
	0x00,                                   // bInterfaceSubClass
	0x00,                                   // bInterfaceProtocol
	0,                                      // iInterface (can point to a string name if desired)

// - 9 bytes -
	// HID interface descriptor, HID 1.11 spec, section 6.2.1
	9,                                      // bLength
	0x21,                                   // bDescriptorType
	0x11, 0x01,                             // bcdHID
	0,                                      // bCountryCode
	1,                                      // bNumDescriptors
	0x22,                                   // bDescriptorType
	LSB(sizeof(rawio_report_desc)),         // wDescriptorLength
	MSB(sizeof(rawio_report_desc)),

// - 7 bytes -
	// endpoint descriptor, USB spec 9.6.6, page 269-271, Table 9-13
	7,                                      // bLength
	5,                                      // bDescriptorType
	RAWIO_TX_ENDPOINT | 0x80,               // bEndpointAddress
	0x03,                                   // bmAttributes (0x03=intr)
	0x40, 0,                                // wMaxPacketSize (64 bytes)
	1,                                      // bInterval

// - 7 bytes -
	// endpoint descriptor, USB spec 9.6.6, page 269-271, Table 9-13
	7,                                      // bLength
	5,                                      // bDescriptorType
	RAWIO_RX_ENDPOINT,                      // bEndpointAddress
	0x03,                                   // bmAttributes (0x03=intr)
	0x40, 0,                                // wMaxPacketSize (64 bytes)
	1,                                      // bInterval

然后您只需向指定的端点发送和接收数据RAWIO_TX_ENDPOINTRAWIO_RX_ENDPOINT

C固件使用

请参阅hid-io-kiibohd

测试

cargo test

一些测试使用额外的日志记录,因此您也可以这样做

RUST_LOG=info cargo test

依赖项

  • Rust nightly(可能随着时间的推移而放宽)
  • 注意bincode-core还很新,还没有正式发布,可能随时会出错。

支持的服务器应用程序

支持的设备固件

贡献

  • 拉取请求运行各种测试
  • 在添加新消息时,请确保添加单元测试验证
  • 一些推荐测试
    • cargotest
    • cargobuild
    • cargobuild --no-default-features --featuresdevice

依赖项

~2.5MB
~58K SLoC