Skip to content

Commit

Permalink
Add support for multiple devices in config (#8)
Browse files Browse the repository at this point in the history
  • Loading branch information
zzeneg authored Oct 4, 2024
1 parent 8587c12 commit 7803b63
Show file tree
Hide file tree
Showing 15 changed files with 403 additions and 293 deletions.
53 changes: 37 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ Application is written in Rust which gives easy access to HID libraries, low-lev

## Supported platforms/providers

| | Windows | Linux | macos |
| ------------ | ------------------ | ------------------------------- |--------------------|
| | Windows | Linux | MacOS |
| ------------ | ------------------ | ------------------------------- | ------------------ |
| Time | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Volume | :heavy_check_mark: | :heavy_check_mark: (PulseAudio) | :heavy_check_mark: |
| Input layout | :heavy_check_mark: | :heavy_check_mark: (X11) | :heavy_check_mark: |
Expand All @@ -27,11 +27,25 @@ All files are available in [latest release](https://github.com/zzeneg/qmk-hid-ho

Default configuration is set to [stront](https://github.com/zzeneg/stront). For other keyboards you need to modify the configuration file (`qmk-hid-host.json`).

- `device` section contains information about keyboard. All values are **decimal**, make sure to convert them from hex using a [converter](https://tools.keycdn.com/hex-converter).
- `devices` section contains a list of keyboards
- `productId` - `pid` from your keyboard's `info.json`
- `usage` and `usagePage` - default values from QMK (`RAW_USAGE_ID` and `RAW_USAGE_PAGE`). No need to modify them unless they were redefined in firmware
- `name` - keyboard's name (optional, visible only in logs)
- `usage` and `usagePage` - optional, override only if `RAW_USAGE_ID` and `RAW_USAGE_PAGE` were redefined in firmware
- `layouts` - list of supported keyboard layouts in two-letter format (app sends layout's index, not name)
- `reconnectDelay` - delay between reconnecting attempts in milliseconds
- `reconnectDelay` - delay between reconnecting attempts in milliseconds (optional, default is 5000)

#### Minimal config

```json
{
"devices": [
{
"productId": "0x0844"
}
],
"layouts": ["en"]
}
```

Configuration is read from file `qmk-hid-host.json` in the current working directory. If it is not found, then the default configuration is written to this file.
You can specify a different location for the configuration file by using `--config (-c)` command line option. For example:
Expand Down Expand Up @@ -65,20 +79,24 @@ When you verified that the application works with your keyboard, you can use `qm
3. Start `qmk-hid-host`, add it to autorun if needed

### MacOS

1. Download `qmk-hid-host`
2. Modify `qmk-hid-host.json`
3. Add your layouts, for example:
```
"layouts": [
"ABC", "Russian"
],
```
if you don't know what layout are installed in you system, run qmk-hid-host with the layouts listed above, change lang and look at terminal output:
```
INFO qmk_hid_host::providers::layout::macos: new layout: 'ABC', layout list: ["ABC", "Russian"]
INFO qmk_hid_host::providers::layout::macos: new layout: 'Russian', layout list: ["ABC", "Russian"]
```
"new layout:" is what you need

```json
"layouts": ["ABC", "Russian"],
```

if you don't know what layout are installed in you system, run qmk-hid-host with the layouts listed above, change lang and look at terminal output:

```
INFO qmk_hid_host::providers::layout::macos: new layout: 'ABC', layout list: ["ABC", "Russian"]
INFO qmk_hid_host::providers::layout::macos: new layout: 'Russian', layout list: ["ABC", "Russian"]
```

"new layout:" is what you need

4. start `qmk-hid-host` from directory where your `qmk-hid-host.json` is located
5. If you `qmk-hid-host` stuck at `Waiting for keyboard...` there are two common mistakes:
1. You're wrong with productId in your config
Expand All @@ -91,6 +109,9 @@ INFO qmk_hid_host::providers::layout::macos: new layout: 'Russian', layout list:
3. If needed, edit `qmk-hid-host.json` in root folder and run again

## Changelog

- 2024-10-03 - add support for multiple devices, restructure config
- 2024-09-15 - add MacOS support
- 2024-02-06 - add Linux support
- 2024-01-21 - remove run as windows service, add silent version instead
- 2024-01-02 - support RUST_LOG, run as windows service
Expand Down
14 changes: 7 additions & 7 deletions dist/qmk-hid-host.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"device": {
"productId": 2116,
"usage": 97,
"usagePage": 65376
},
"layouts": ["en"],
"reconnectDelay": 5000
"devices": [
{
"name": "stront",
"productId": "0x0844"
}
],
"layouts": ["en"]
}
75 changes: 53 additions & 22 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -1,46 +1,77 @@
use std::path::PathBuf;
use std::{path::PathBuf, sync::OnceLock};

#[derive(serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Config {
pub device: Device,
pub devices: Vec<Device>,
pub layouts: Vec<String>,
pub reconnect_delay: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub reconnect_delay: Option<u64>,
}

#[derive(serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Device {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(serialize_with = "hex_to_string", deserialize_with = "string_to_hex")]
pub product_id: u16,
pub usage: u16,
pub usage_page: u16,
#[serde(skip_serializing_if = "Option::is_none")]
pub usage: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
pub usage_page: Option<u16>,
}

pub fn get_config(maybe_path: Option<PathBuf>) -> Config {
static CONFIG: OnceLock<Config> = OnceLock::new();

pub fn get_config() -> &'static Config {
CONFIG.get().unwrap()
}

pub fn load_config(path: PathBuf) -> &'static Config {
if let Some(config) = CONFIG.get() {
return config;
}

let default_config = Config {
device: Device {
devices: vec![Device {
name: None,
product_id: 0x0844,
usage: 0x61,
usage_page: 0xff60,
},
layouts: vec!["pl".to_string()],
reconnect_delay: 5000,
usage: None,
usage_page: None,
}],
layouts: vec!["en".to_string()],
reconnect_delay: None,
};

let path = maybe_path.unwrap_or("./qmk-hid-host.json".into());

if let Ok(file) = std::fs::read_to_string(&path) {
if let Ok(file_config) = serde_json::from_str::<Config>(&file) {
tracing::info!("Read config from file {:?}", path);
return file_config;
}

tracing::error!("Error while reading config from file {:?}", path);
let config = serde_json::from_str::<Config>(&file)
.map_err(|e| tracing::error!("Incorrect config file: {}", e))
.unwrap();
return CONFIG.get_or_init(|| config);
}

let file_content = serde_json::to_string_pretty(&default_config).unwrap();
std::fs::write(&path, &file_content).unwrap();
std::fs::write(&path, &file_content)
.map_err(|e| tracing::error!("Error while saving config file to {:?}: {}", path, e))
.unwrap();
tracing::info!("New config file created at {:?}", path);

return default_config;
CONFIG.get_or_init(|| default_config)
}

fn string_to_hex<'de, D>(deserializer: D) -> Result<u16, D::Error>
where
D: serde::Deserializer<'de>,
{
let value: &str = serde::Deserialize::deserialize(deserializer)?;
let hex = value.trim_start_matches("0x");
return u16::from_str_radix(hex, 16).map_err(serde::de::Error::custom);
}

fn hex_to_string<S>(value: &u16, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&format!("0x{:04x}", value))
}
35 changes: 17 additions & 18 deletions src/keyboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,20 @@ use tokio::sync::{broadcast, mpsc};
use crate::config::Device;

pub struct Keyboard {
name: String,
product_id: u16,
usage: u16,
usage_page: u16,
reconnect_delay: u64,
}

impl Keyboard {
pub fn new(device: Device, reconnect_delay: u64) -> Self {
pub fn new(device: &Device, reconnect_delay: u64) -> Self {
return Self {
name: device.name.clone().unwrap_or("keyboard".to_string()),
product_id: device.product_id,
usage: device.usage,
usage_page: device.usage_page,
usage: device.usage.unwrap_or(0x61),
usage_page: device.usage_page.unwrap_or(0xff60),
reconnect_delay,
};
}
Expand All @@ -33,30 +35,29 @@ impl Keyboard {
return Err(HidError::HidApiErrorEmpty);
}

pub fn connect(&self) -> (broadcast::Sender<bool>, mpsc::Sender<Vec<u8>>) {
pub fn connect(&self, data_sender: broadcast::Sender<Vec<u8>>, is_connected_sender: mpsc::Sender<bool>) {
let name = self.name.clone();
let pid = self.product_id;
let usage = self.usage;
let usage_page = self.usage_page;
let reconnect_delay = self.reconnect_delay;
let (data_sender, mut data_receiver) = mpsc::channel::<Vec<u8>>(32);
let (connected_sender, _) = broadcast::channel::<bool>(32);
let internal_connected_sender = connected_sender.clone();
let is_connected_sender = is_connected_sender.clone();
let mut data_receiver = data_sender.subscribe();
std::thread::spawn(move || {
tracing::info!("Waiting for keyboard...");
tracing::info!("Waiting for {}...", name);
loop {
tracing::debug!("Trying to connect...");
tracing::debug!("Trying to connect to {}...", name);
if let Ok(device) = Self::get_device(&pid, &usage, &usage_page) {
let _ = &internal_connected_sender.send(true).unwrap();
tracing::info!("Connected to keyboard");
let _ = &is_connected_sender.try_send(true).unwrap_or_else(|e| tracing::error!("{}", e));
tracing::info!("Connected to {}", name);
loop {
let msg = data_receiver.blocking_recv();
if let Some(mut received) = msg {
tracing::info!("Sending to keyboard: {:?}", received);
if let Ok(mut received) = data_receiver.blocking_recv() {
tracing::info!("Sending to {}: {:?}", name, received);
received.truncate(32);
received.insert(0, 0);
if let Err(_) = device.write(received.as_mut()) {
let _ = internal_connected_sender.send(false).unwrap();
tracing::warn!("Disconnected from keyboard");
let _ = is_connected_sender.try_send(false).unwrap_or_else(|e| tracing::error!("{}", e));
tracing::warn!("Disconnected from {}", name);

break;
}
Expand All @@ -67,7 +68,5 @@ impl Keyboard {
std::thread::sleep(std::time::Duration::from_millis(reconnect_delay));
}
});

return (connected_sender, data_sender);
}
}
Loading

0 comments on commit 7803b63

Please sign in to comment.