Skip to content

Commit ef5bc29

Browse files
irobotpax
andauthored
Feat/optional python (#2)
* feat: make python bindings optional * annotate pyo3 references to allow conditional compilation * address minor linting issues The new feature flag (`python`) allows turning the python bindings off. This may be desirable in projects that don't interoperate with Python. If the `python` feature is not explicitly disabled, it remains on by default. * chore: remove commented out code --------- Co-authored-by: pax <pm@paxinvict.us>
1 parent 9263149 commit ef5bc29

File tree

8 files changed

+104
-50
lines changed

8 files changed

+104
-50
lines changed

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,9 @@ strum_macros = "0.27.2"
2121

2222
[dependencies.pyo3]
2323
version = "0.26.0"
24-
features = ["abi3-py38"] # "abi3-py38" tells pyo3 (and maturin) to build using the stable ABI with minimum Python version 3.8.
24+
features = ["abi3-py38", "multiple-pymethods"] # "abi3-py38" tells pyo3 (and maturin) to build using the stable ABI with minimum Python version 3.8.
25+
optional = true
26+
27+
[features]
28+
default = ["python"]
29+
python = ["dep:pyo3"]

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,15 @@ fn main() {
3434
}
3535
```
3636

37+
### Disable Python Bindings
38+
39+
The `python` feature flag is enabled by default. In projects that don't interoperate with Python, the Python bindings can be disabled by turning off default features. In `Cargo.toml`:
40+
41+
```toml
42+
[dependencies]
43+
qmk-via-api = { version = "0.3.0", default-features = false }
44+
```
45+
3746
## Python
3847

3948
Install with pip:

src/api.rs

Lines changed: 58 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@ use crate::api_commands::{
44
};
55
use crate::utils;
66
use hidapi::HidApi;
7-
use pyo3::prelude::*;
87
use std::str::FromStr;
98
use std::vec;
9+
use core::result::Result;
10+
11+
#[cfg(feature = "python")]
12+
use pyo3::prelude::*;
1013

1114
const COMMAND_START: u8 = 0x00;
1215

@@ -21,14 +24,14 @@ pub type Layer = u8;
2124
pub type Row = u8;
2225
pub type Column = u8;
2326

24-
#[pyclass]
27+
#[cfg_attr(feature = "python", pyclass)]
2528
#[derive(Clone, Copy, Debug)]
2629
pub struct MatrixInfo {
2730
pub rows: u8,
2831
pub cols: u8,
2932
}
3033

31-
#[pyclass]
34+
#[cfg_attr(feature = "python", pyclass)]
3235
#[derive(Clone, Copy, Debug)]
3336
pub enum KeyboardValue {
3437
Uptime = 0x01,
@@ -53,18 +56,50 @@ impl FromStr for KeyboardValue {
5356
}
5457
}
5558

56-
#[pyclass(unsendable)]
59+
#[cfg_attr(feature = "python", pyclass(unsendable))]
5760
pub struct KeyboardApi {
5861
device: hidapi::HidDevice,
5962
}
6063

64+
#[cfg(feature = "python")]
6165
#[pymethods]
6266
impl KeyboardApi {
6367
#[new]
64-
pub fn new(vid: u16, pid: u16, usage_page: u16) -> PyResult<Self> {
65-
let api = HidApi::new().map_err(|e| {
66-
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Error: {}", e))
67-
})?;
68+
pub fn py_new(vid: u16, pid: u16, usage_page: u16) -> Result<Self, Error> {
69+
KeyboardApi::new(vid, pid, usage_page)
70+
}
71+
}
72+
73+
#[cfg(feature = "python")]
74+
impl From<Error> for PyErr {
75+
fn from(err: Error) -> Self {
76+
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(err.0)
77+
}
78+
}
79+
80+
pub struct Error(pub String);
81+
82+
impl std::fmt::Display for Error {
83+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84+
f.write_str(self.0.as_str())
85+
}
86+
}
87+
88+
impl From<String> for Error {
89+
fn from(err: String) -> Self {
90+
Error(err)
91+
}
92+
}
93+
94+
impl From<&str> for Error {
95+
fn from(err: &str) -> Self {
96+
Error(err.to_string())
97+
}
98+
}
99+
100+
impl KeyboardApi {
101+
pub fn new(vid: u16, pid: u16, usage_page: u16) -> Result<KeyboardApi, Error> {
102+
let api = HidApi::new().map_err(|e| format!("Error: {e}"))?;
68103

69104
let device = api
70105
.device_list()
@@ -73,16 +108,16 @@ impl KeyboardApi {
73108
&& device.product_id() == pid
74109
&& device.usage_page() == usage_page
75110
})
76-
.ok_or_else(|| {
77-
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>("Could not find keyboard.")
78-
})?
111+
.ok_or("Could not find keyboard.")?
79112
.open_device(&api)
80-
.map_err(|_| {
81-
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>("Could not open HID device.")
82-
})?;
113+
.map_err(|_| "Could not open HID device.")?;
83114

84115
Ok(KeyboardApi { device })
85116
}
117+
}
118+
119+
#[cfg_attr(feature = "python", pymethods)]
120+
impl KeyboardApi {
86121

87122
/// Sends a raw HID command prefixed with the command byte and returns the response if successful.
88123
pub fn hid_command(&self, command: ViaCommandId, bytes: Vec<u8>) -> Option<Vec<u8>> {
@@ -188,23 +223,21 @@ impl KeyboardApi {
188223
fn fast_read_raw_matrix(&self, matrix_info: MatrixInfo, layer: Layer) -> Option<Vec<u16>> {
189224
const MAX_KEYCODES_PARTIAL: usize = 14;
190225
let length = matrix_info.rows as usize * matrix_info.cols as usize;
191-
let buffer_list = vec![0; length.div_ceil(MAX_KEYCODES_PARTIAL) as usize];
226+
let buffer_len = length.div_ceil(MAX_KEYCODES_PARTIAL);
192227
let mut remaining = length;
193228
let mut result = Vec::new();
194-
for _ in 0..buffer_list.len() {
229+
for _ in 0..buffer_len {
195230
if remaining < MAX_KEYCODES_PARTIAL {
196-
self.get_keymap_buffer(
231+
if let Some(val) = self.get_keymap_buffer(
197232
layer as u16 * length as u16 * 2 + 2 * (length - remaining) as u16,
198233
(remaining * 2) as u8,
199-
)
200-
.map(|val| result.extend(val));
234+
) { result.extend(val) }
201235
remaining = 0;
202236
} else {
203-
self.get_keymap_buffer(
237+
if let Some(val) = self.get_keymap_buffer(
204238
layer as u16 * length as u16 * 2 + 2 * (length - remaining) as u16,
205239
(MAX_KEYCODES_PARTIAL * 2) as u8,
206-
)
207-
.map(|val| result.extend(val));
240+
) { result.extend(val) }
208241
remaining -= MAX_KEYCODES_PARTIAL;
209242
}
210243
}
@@ -217,7 +250,7 @@ impl KeyboardApi {
217250
for i in 0..length {
218251
let row = (i as u16 / matrix_info.cols as u16) as u8;
219252
let col = (i as u16 % matrix_info.cols as u16) as u8;
220-
self.get_key(layer, row, col).map(|val| res.push(val));
253+
if let Some(val) = self.get_key(layer, row, col) { res.push(val) }
221254
}
222255
Some(res)
223256
}
@@ -238,8 +271,7 @@ impl KeyboardApi {
238271
for (key_idx, keycode) in layer.iter().enumerate() {
239272
let row = (key_idx as u16 / matrix_info.cols as u16) as u8;
240273
let col = (key_idx as u16 % matrix_info.cols as u16) as u8;
241-
self.set_key(layer_idx as u8, row, col, *keycode)
242-
.map(|_| ());
274+
self.set_key(layer_idx as u8, row, col, *keycode);
243275
}
244276
}
245277
Some(())
@@ -257,8 +289,7 @@ impl KeyboardApi {
257289
let buffer = shifted_data[offset..end].to_vec();
258290
let mut bytes = vec![offset_bytes.0, offset_bytes.1, buffer.len() as u8];
259291
bytes.extend(buffer);
260-
self.hid_command(ViaCommandId::DynamicKeymapSetBuffer, bytes)
261-
.map(|_| ());
292+
self.hid_command(ViaCommandId::DynamicKeymapSetBuffer, bytes);
262293
}
263294
Some(())
264295
}

src/api_commands.rs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
#[cfg(feature = "python")]
12
use pyo3::prelude::*;
23

3-
#[pyclass]
4+
#[cfg_attr(feature = "python", pyclass)]
45
#[derive(Clone, Copy, Debug, PartialEq)]
56
pub enum ViaCommandId {
67
GetProtocolVersion = 0x01,
@@ -26,7 +27,7 @@ pub enum ViaCommandId {
2627
DynamicKeymapSetEncoder = 0x15,
2728
}
2829

29-
#[pyclass]
30+
#[cfg_attr(feature = "python", pyclass)]
3031
#[derive(Clone, Copy, Debug, PartialEq)]
3132
pub enum ViaChannelId {
3233
IdCustomChannel = 0,
@@ -37,14 +38,14 @@ pub enum ViaChannelId {
3738
IdQmkLedMatrixChannel = 5,
3839
}
3940

40-
#[pyclass]
41+
#[cfg_attr(feature = "python", pyclass)]
4142
#[derive(Clone, Copy, Debug, PartialEq)]
4243
pub enum ViaQmkBacklightValue {
4344
IdQmkBacklightBrightness = 1,
4445
IdQmkBacklightEffect = 2,
4546
}
4647

47-
#[pyclass]
48+
#[cfg_attr(feature = "python", pyclass)]
4849
#[derive(Clone, Copy, Debug, PartialEq)]
4950
pub enum ViaQmkRgblightValue {
5051
IdQmkRgblightBrightness = 1,
@@ -53,7 +54,7 @@ pub enum ViaQmkRgblightValue {
5354
IdQmkRgblightColor = 4,
5455
}
5556

56-
#[pyclass]
57+
#[cfg_attr(feature = "python", pyclass)]
5758
#[derive(Clone, Copy, Debug, PartialEq)]
5859
pub enum ViaQmkRgbMatrixValue {
5960
IdQmkRgbMatrixBrightness = 1,
@@ -62,15 +63,15 @@ pub enum ViaQmkRgbMatrixValue {
6263
IdQmkRgbMatrixColor = 4,
6364
}
6465

65-
#[pyclass]
66+
#[cfg_attr(feature = "python", pyclass)]
6667
#[derive(Clone, Copy, Debug, PartialEq)]
6768
pub enum ViaQmkLedMatrixValue {
6869
IdQmkLedMatrixBrightness = 1,
6970
IdQmkLedMatrixEffect = 2,
7071
IdQmkLedMatrixEffectSpeed = 3,
7172
}
7273

73-
#[pyclass]
74+
#[cfg_attr(feature = "python", pyclass)]
7475
#[derive(Clone, Copy, Debug, PartialEq)]
7576
pub enum ViaQmkAudioValue {
7677
IdQmkAudioEnable = 1,

src/lib.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
1-
use pyo3::prelude::*;
2-
31
pub mod api;
42
pub mod api_commands;
53
pub mod keycodes;
64
pub mod scan;
75
pub mod utils;
86

7+
#[cfg(feature = "python")]
8+
use pyo3::prelude::*;
9+
10+
#[cfg(feature = "python")]
911
#[pymodule]
1012
fn qmk_via_api(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
1113
m.add_class::<api::KeyboardApi>()?;
1214
m.add_class::<scan::KeyboardDeviceInfo>()?;
1315
m.add_function(wrap_pyfunction!(scan::scan_keyboards, m)?)?;
1416
Ok(())
15-
}
17+
}

src/scan.rs

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,30 @@
11
use hidapi::HidApi;
2+
3+
#[cfg(feature = "python")]
24
use pyo3::prelude::*;
35

46
const VIA_USAGE_PAGE: u16 = 0xff60;
57

68
/// Information about a connected VIA-compatible keyboard.
7-
#[pyclass]
9+
#[cfg_attr(feature = "python", pyclass(get_all))]
810
#[derive(Clone, Debug)]
911
pub struct KeyboardDeviceInfo {
1012
/// USB vendor ID
11-
#[pyo3(get)]
1213
pub vendor_id: u16,
1314
/// USB product ID
14-
#[pyo3(get)]
1515
pub product_id: u16,
1616
/// HID usage page (expected to be 0xFF60 for VIA)
17-
#[pyo3(get)]
1817
pub usage_page: u16,
1918
/// Optional manufacturer string
20-
#[pyo3(get)]
2119
pub manufacturer: Option<String>,
2220
/// Optional product string
23-
#[pyo3(get)]
2421
pub product: Option<String>,
2522
/// Optional serial number string
26-
#[pyo3(get)]
2723
pub serial_number: Option<String>,
2824
}
2925

3026
/// Scan for connected VIA keyboards.
31-
#[pyfunction]
27+
#[cfg_attr(feature = "python", pyfunction)]
3228
pub fn scan_keyboards() -> Vec<KeyboardDeviceInfo> {
3329
let api = match HidApi::new() {
3430
Ok(a) => a,
@@ -37,15 +33,15 @@ pub fn scan_keyboards() -> Vec<KeyboardDeviceInfo> {
3733

3834
api.device_list()
3935
.filter(|d| d.usage_page() == VIA_USAGE_PAGE)
40-
.filter_map(|d| {
41-
Some(KeyboardDeviceInfo {
36+
.map(|d| {
37+
KeyboardDeviceInfo {
4238
vendor_id: d.vendor_id(),
4339
product_id: d.product_id(),
4440
usage_page: d.usage_page(),
4541
manufacturer: d.manufacturer_string().map(|s| s.to_string()),
4642
product: d.product_string().map(|s| s.to_string()),
4743
serial_number: d.serial_number().map(|s| s.to_string()),
48-
})
44+
}
4945
})
5046
.collect()
5147
}

src/utils.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ pub fn shift_buffer_to_16_bit(buffer: &[u8]) -> Vec<u16> {
3232
shifted_buffer
3333
}
3434

35-
pub fn shift_buffer_from_16_bit(buffer: &Vec<u16>) -> Vec<u8> {
35+
pub fn shift_buffer_from_16_bit(buffer: &[u16]) -> Vec<u8> {
3636
let mut flattened = Vec::new();
3737
for value in buffer.iter() {
3838
let (hi, lo) = shift_from_16_bit(*value);

0 commit comments

Comments
 (0)