Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 13 additions & 25 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,16 @@ jobs:
runs-on: ubuntu-latest
env:
DEBIAN_FRONTEND: noninteractive
strategy:
fail-fast: false
matrix:
backend:
- linux-native
- linux-native-basic-udev
async:
- ''
- async-io
- tokio
steps:
- name: Checkout repository and submodules
uses: actions/checkout@v4
Expand All @@ -83,33 +93,11 @@ jobs:
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Build
run: cargo build --no-default-features --features linux-native --verbose
- name: Run tests
run: cargo test --no-default-features --features linux-native --verbose
- name: Verify package
run: cargo package --no-default-features --features linux-native --verbose

build-linux-native-basic-udev:
runs-on: ubuntu-latest
env:
DEBIAN_FRONTEND: noninteractive
steps:
- name: Checkout repository and submodules
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install dependencies
run: |
sudo apt-get update -y
sudo apt-get install -y libudev-dev
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Build
run: cargo build --no-default-features --features linux-native-basic-udev --verbose
run: cargo build --no-default-features --features ${{ matrix.backend }},${{ matrix.async }} --verbose
- name: Run tests
run: cargo test --no-default-features --features linux-native-basic-udev --verbose
run: cargo test --no-default-features --features ${{ matrix.backend }},${{ matrix.async }} --verbose
- name: Verify package
run: cargo package --no-default-features --features linux-native-basic-udev --verbose
run: cargo package --no-default-features --features ${{ matrix.backend }},${{ matrix.async }} --verbose

build-windows:
runs-on: windows-latest
Expand Down
19 changes: 19 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,21 @@ windows-native = [
"windows-sys/Win32_UI_Shell_PropertiesSystem"
]

# Enable async via the async-io crate
async-io = ["__async", "dep:async-io"]

#Enable async via the tokio crate
tokio = ["__async", "dep:tokio"]

# Enables some common code to do with async
__async = ["dep:futures"]

[dependencies]
libc = "0.2"
cfg-if = "1"
futures = { version = "0.3", optional = true }
async-io = { version = "2", optional = true }
tokio = { version = "1", optional = true, features = ["net"] }

[target.'cfg(target_os = "linux")'.dependencies]
udev = { version = "0.8", optional = true }
Expand All @@ -76,9 +88,16 @@ nix = { version = "0.27", optional = true, features = ["fs", "ioctl", "poll"] }
[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.48", features = ["Win32_Foundation"] }

[target.'cfg(target_os = "linux")'.dev-dependencies]
tokio = { version = "1", features = ["rt", "macros", "time"] }

[build-dependencies]
cc = "1.0"
pkg-config = "0.3"

[[example]]
name = "co2mon-tokio"
required-features = ["__async"]

[package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
63 changes: 63 additions & 0 deletions examples/co2mon-tokio.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/****************************************************************************
Copyright (c) 2015 Artyom Pavlov All Rights Reserved.

This file is part of hidapi-rs, based on hidapi_rust by Roland Ruckerbauer.
It's also based on the Oleg Bulatov's work (https://github.com/dmage/co2mon)
****************************************************************************/

//! Opens a KIT MT 8057 CO2 detector and reads data from it. This
//! example will not work unless such an HID is plugged in to your system.

extern crate hidapi;

use hidapi::{HidApi, HidError};
use std::time::Duration;

use tokio::time::timeout;

// Reuse the code from the co2mon example so we're not writing the decoding and
// decrypting code multiple times.
#[allow(dead_code)]
#[path = "co2mon.rs"]
mod co2mon;

use co2mon::*;

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), HidError> {
let api = HidApi::new().expect("HID API object creation failed");
let dev = api.open(DEV_VID, DEV_PID)?;
dev.send_feature_report(&[0; PACKET_SIZE])?;

if let Some(manufacturer) = dev.get_manufacturer_string()? {
println!("Manufacurer:\t{manufacturer}");
}
if let Some(product) = dev.get_product_string()? {
println!("Product:\t{product}");
}
if let Some(serial_number) = dev.get_serial_number_string()? {
println!("Serial number:\t{serial_number}");
}

loop {
let mut buf = [0; PACKET_SIZE];
let n = timeout(
Duration::from_millis(HID_TIMEOUT as u64),
dev.async_read(&mut buf[..]),
)
.await
.map_err(|_| invalid_data_err("timeout"))??;
if n != PACKET_SIZE {
let msg = format!("unexpected packet length: {n}/{PACKET_SIZE}");
return Err(invalid_data_err(msg));
}
match decode_buf(buf) {
CO2Result::Temperature(val) => println!("Temp:\t{:?}", val),
CO2Result::Concentration(val) => println!("Conc:\t{:?}", val),
CO2Result::Unknown(kind, val) => eprintln!("Unknown({kind:x}):\t{val}"),
CO2Result::Error(msg) => {
return Err(invalid_data_err(msg));
}
}
}
}
24 changes: 12 additions & 12 deletions examples/co2mon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,27 @@
use hidapi::{HidApi, HidError};
use std::io;

const CODE_TEMPERATURE: u8 = 0x42;
const CODE_CONCENTRATION: u8 = 0x50;
const HID_TIMEOUT: i32 = 5000;
const DEV_VID: u16 = 0x04d9;
const DEV_PID: u16 = 0xa052;
const PACKET_SIZE: usize = 8;
pub const CODE_TEMPERATURE: u8 = 0x42;
pub const CODE_CONCENTRATION: u8 = 0x50;
pub const HID_TIMEOUT: i32 = 5000;
pub const DEV_VID: u16 = 0x04d9;
pub const DEV_PID: u16 = 0xa052;
pub const PACKET_SIZE: usize = 8;

type Packet = [u8; PACKET_SIZE];
pub type Packet = [u8; PACKET_SIZE];

enum CO2Result {
pub enum CO2Result {
Temperature(f32),
Concentration(u16),
Unknown(u8, u16),
Error(&'static str),
}

fn decode_temperature(value: u16) -> f32 {
pub fn decode_temperature(value: u16) -> f32 {
(value as f32) * 0.0625 - 273.15
}

fn decrypt(buf: Packet) -> Packet {
pub fn decrypt(buf: Packet) -> Packet {
let mut res: [u8; PACKET_SIZE] = [
(buf[3] << 5) | (buf[2] >> 3),
(buf[2] << 5) | (buf[4] >> 3),
Expand All @@ -52,7 +52,7 @@ fn decrypt(buf: Packet) -> Packet {
res
}

fn decode_buf(buf: Packet) -> CO2Result {
pub fn decode_buf(buf: Packet) -> CO2Result {
// Do we need to decrypt the data?
let res = if buf[4] == 0x0d { buf } else { decrypt(buf) };

Expand Down Expand Up @@ -82,7 +82,7 @@ fn decode_buf(buf: Packet) -> CO2Result {
}
}

fn invalid_data_err(msg: impl Into<String>) -> HidError {
pub fn invalid_data_err(msg: impl Into<String>) -> HidError {
HidError::IoError {
error: io::Error::new(io::ErrorKind::InvalidData, msg.into()),
}
Expand Down
76 changes: 70 additions & 6 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
//!
//! # Feature flags
//!
//! ## Backends
//!
//! - `linux-static-libusb`: uses statically linked `libusb` backend on Linux
//! - `linux-static-hidraw`: uses statically linked `hidraw` backend on Linux (default)
//! - `linux-shared-libusb`: uses dynamically linked `libusb` backend on Linux
Expand All @@ -49,6 +51,15 @@
//! - `macos-shared-device`: enables shared access to HID devices on MacOS
//! - `windows-native`: talks to hid.dll directly without using the `hidapi` C library
//!
//! ## Async backends
//!
//! For some backends (`linux-native` for now) we support some operations
//! running async. You can choose what to use. If you are not using tokio
//! yourself, select `async-io`
//!
//! - `async-io`: use smol-rs's [async-io](https://crates.io/crates/async-io) to enable async support
//! - `tokio`: use [tokio](https://crates.io/crates/tokio) to enable async support
//!
//! ## Linux backends
//!
//! On linux the libusb backends do not support [`DeviceInfo::usage()`] and [`DeviceInfo::usage_page()`].
Expand All @@ -65,13 +76,25 @@ mod error;
mod ffi;

use cfg_if::cfg_if;

// You can only select a single async backend for us
#[cfg(all(feature = "async-io", feature = "tokio"))]
compile_error!("A maximum of one async backend can be selected");

// Catch async being enabled with an unsupported backend
#[cfg(all(feature = "__async", not(feature = "linux-native")))]
compile_error!("async is only supported for some backends");

use libc::wchar_t;
use std::ffi::CStr;
use std::ffi::CString;
use std::fmt;
use std::fmt::Debug;
use std::sync::Mutex;

#[cfg(feature = "__async")]
use futures::task::{Context, Poll};

pub use error::HidError;

cfg_if! {
Expand All @@ -92,6 +115,8 @@ cfg_if! {
}

// Automatically implement the top trait
//
// In this block we set up what the trait should be for the sync version
cfg_if! {
if #[cfg(target_os = "windows")] {
#[cfg_attr(docsrs, doc(cfg(target_os = "windows")))]
Expand All @@ -102,8 +127,8 @@ cfg_if! {
/// Get the container ID for a HID device
fn get_container_id(&self) -> HidResult<GUID>;
}
trait HidDeviceBackend: HidDeviceBackendBase + HidDeviceBackendWindows + Send {}
impl<T> HidDeviceBackend for T where T: HidDeviceBackendBase + HidDeviceBackendWindows + Send {}
trait HidDeviceBackendSync: HidDeviceBackendBase + HidDeviceBackendWindows + Send {}
impl<T> HidDeviceBackendSync for T where T: HidDeviceBackendBase + HidDeviceBackendWindows + Send {}
} else if #[cfg(target_os = "macos")] {
#[cfg_attr(docsrs, doc(cfg(target_os = "macos")))]
mod macos;
Expand All @@ -115,11 +140,24 @@ cfg_if! {
/// Check if the device was opened in exclusive mode.
fn is_open_exclusive(&self) -> HidResult<bool>;
}
trait HidDeviceBackend: HidDeviceBackendBase + HidDeviceBackendMacos + Send {}
impl<T> HidDeviceBackend for T where T: HidDeviceBackendBase + HidDeviceBackendMacos + Send {}
trait HidDeviceBackendSync: HidDeviceBackendBase + HidDeviceBackendMacos + Send {}
impl<T> HidDeviceBackendSync for T where T: HidDeviceBackendBase + HidDeviceBackendMacos + Send {}
} else {
trait HidDeviceBackendSync: HidDeviceBackendBase + Send {}
impl<T> HidDeviceBackendSync for T where T: HidDeviceBackendBase + Send {}
}
}

// Automatically implement the top trait
//
// In this block we set up whether the top-level trait should include the async trait
cfg_if! {
if #[cfg(feature = "__async")] {
trait HidDeviceBackend: HidDeviceBackendSync + HidDeviceBackendBaseAsync {}
impl<T> HidDeviceBackend for T where T: HidDeviceBackendSync + HidDeviceBackendBaseAsync {}
} else {
trait HidDeviceBackend: HidDeviceBackendBase + Send {}
impl<T> HidDeviceBackend for T where T: HidDeviceBackendBase + Send {}
trait HidDeviceBackend: HidDeviceBackendSync {}
impl<T> HidDeviceBackend for T where T: HidDeviceBackendSync {}
}
}

Expand Down Expand Up @@ -509,6 +547,13 @@ trait HidDeviceBackendBase {
}
}

/// Trait which the different backends must implement if they support async code
#[cfg(feature = "__async")]
trait HidDeviceBackendBaseAsync {
fn poll_write(&mut self, cx: &mut Context<'_>, buf: &[u8]) -> Poll<HidResult<usize>>;
fn poll_read(&self, cx: &mut Context<'_>, buf: &mut [u8]) -> Poll<HidResult<usize>>;
}

pub struct HidDevice {
inner: Box<dyn HidDeviceBackend>,
}
Expand Down Expand Up @@ -685,3 +730,22 @@ impl HidDevice {
self.inner.get_device_info()
}
}

#[cfg(feature = "__async")]
impl HidDevice {
/// Write asynchronously to the device.
///
/// See [`write`][`Self::write`] for more information.
#[cfg_attr(docsrs, doc(cfg(feature = "__async")))]
pub async fn async_write(&mut self, buf: &[u8]) -> HidResult<usize> {
futures::future::poll_fn(|cx| self.inner.poll_write(cx, buf)).await
}

/// Read asynchronously from the device
///
/// See [`read`][`Self::read`] for more information.
#[cfg_attr(docsrs, doc(cfg(feature = "__async")))]
pub async fn async_read(&self, buf: &mut [u8]) -> HidResult<usize> {
futures::future::poll_fn(|cx| self.inner.poll_read(cx, buf)).await
}
}
Loading