Skip to content

Commit

Permalink
Detect host OS based on USB fingerprint (qmk#18463)
Browse files Browse the repository at this point in the history
Co-authored-by: Drashna Jaelre <drashna@live.com>
Co-authored-by: Nick Brassel <nick@tzarc.org>
  • Loading branch information
3 people authored and abstracterror committed May 21, 2023
1 parent e23e173 commit 2a6d31b
Show file tree
Hide file tree
Showing 15 changed files with 450 additions and 7 deletions.
1 change: 1 addition & 0 deletions build_test.mk
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ include $(PLATFORM_PATH)/common.mk
include $(TMK_PATH)/protocol.mk
include $(QUANTUM_PATH)/debounce/tests/rules.mk
include $(QUANTUM_PATH)/encoder/tests/rules.mk
include $(QUANTUM_PATH)/os_detection/tests/rules.mk
include $(QUANTUM_PATH)/sequencer/tests/rules.mk
include $(PLATFORM_PATH)/test/rules.mk
ifneq ($(filter $(FULL_TESTS),$(TEST)),)
Expand Down
8 changes: 8 additions & 0 deletions common_features.mk
Original file line number Diff line number Diff line change
Expand Up @@ -722,3 +722,11 @@ ifeq ($(strip $(BLUETOOTH_ENABLE)), yes)
SRC += $(TMK_DIR)/protocol/serial_uart.c
endif
endif

ifeq ($(strip $(OS_DETECTION_ENABLE)), yes)
SRC += $(QUANTUM_DIR)/os_detection.c
OPT_DEFS += -DOS_DETECTION_ENABLE
ifeq ($(strip $(OS_DETECTION_DEBUG_ENABLE)), yes)
OPT_DEFS += -DOS_DETECTION_DEBUG_ENABLE
endif
endif
1 change: 1 addition & 0 deletions docs/_summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
* [Key Overrides](feature_key_overrides.md)
* [Layers](feature_layers.md)
* [One Shot Keys](one_shot_keys.md)
* [OS Detection](feature_os_detection.md)
* [Pointing Device](feature_pointing_device.md)
* [Raw HID](feature_rawhid.md)
* [Sequencer](feature_sequencer.md)
Expand Down
77 changes: 77 additions & 0 deletions docs/feature_os_detection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# OS Detection

This feature makes a best guess at the host OS based on OS specific behavior during USB setup. It may not always get the correct OS, and shouldn't be relied on as for critical functionality.

Using it you can have OS specific key mappings or combos which work differently on different devices.

It is available for keyboards which use ChibiOS, LUFA and V-USB.

## Usage

In your `rules.mk` add:

```make
OS_DETECTION_ENABLE = yes
```

Include `"os_detection.h"` in your `keymap.c`.
It declares `os_variant_t detected_host_os(void);` which you can call to get detected OS.

It returns one of the following values:

```c
enum {
OS_UNSURE,
OS_LINUX,
OS_WINDOWS,
OS_MACOS,
OS_IOS,
} os_variant_t;
```

?> Note that it takes some time after firmware is booted to detect the OS.
This time is quite short, probably hundreds of milliseconds, but this data may be not ready in keyboard and layout setup functions which run very early during firmware startup.

## Debug

If OS is guessed incorrectly, you may want to collect data about USB setup packets to refine the detection logic.

To do so in your `rules.mk` add:

```make
OS_DETECTION_DEBUG_ENABLE = yes
CONSOLE_ENABLE = yes
```

And also include `"os_detection.h"` in your `keymap.c`.

Then you can define custom keycodes to store data about USB setup packets in EEPROM (persistent memory) and to print it later on host where you can run `qmk console`:

```c
enum custom_keycodes {
STORE_SETUPS = SAFE_RANGE,
PRINT_SETUPS,
};

bool process_record_user(uint16_t keycode, keyrecord_t *record) {
switch (keycode) {
case STORE_SETUPS:
if (record->event.pressed) {
store_setups_in_eeprom();
}
return false;
case PRINT_SETUPS:
if (record->event.pressed) {
print_stored_setups();
}
return false;
}
}
```
Then please open an issue on Github with this information and tell what OS was not detected correctly and if you have any intermediate devices between keyboard and your computer.
## Credits
Original idea is coming from [FingerprintUSBHost](https://github.com/keyboardio/FingerprintUSBHost) project.
129 changes: 129 additions & 0 deletions quantum/os_detection.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/* Copyright 2022 Ruslan Sayfutdinov (@KapJI)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

#include "os_detection.h"

#include <string.h>

#ifdef OS_DETECTION_DEBUG_ENABLE
# include "eeconfig.h"
# include "eeprom.h"
# include "print.h"

# define STORED_USB_SETUPS 50
# define EEPROM_USER_OFFSET (uint8_t*)EECONFIG_SIZE

uint16_t usb_setups[STORED_USB_SETUPS];
#endif

#ifdef OS_DETECTION_ENABLE
struct setups_data_t {
uint8_t count;
uint8_t cnt_02;
uint8_t cnt_04;
uint8_t cnt_ff;
uint16_t last_wlength;
os_variant_t detected_os;
};

struct setups_data_t setups_data = {
.count = 0,
.cnt_02 = 0,
.cnt_04 = 0,
.cnt_ff = 0,
.detected_os = OS_UNSURE,
};

// Some collected sequences of wLength can be found in tests.
void make_guess(void) {
if (setups_data.count < 3) {
return;
}
if (setups_data.cnt_ff >= 2 && setups_data.cnt_04 >= 1) {
setups_data.detected_os = OS_WINDOWS;
return;
}
if (setups_data.count == setups_data.cnt_ff) {
// Linux has 3 packets with 0xFF.
setups_data.detected_os = OS_LINUX;
return;
}
if (setups_data.count == 5 && setups_data.last_wlength == 0xFF && setups_data.cnt_ff == 1 && setups_data.cnt_02 == 2) {
setups_data.detected_os = OS_MACOS;
return;
}
if (setups_data.count == 4 && setups_data.cnt_ff == 0 && setups_data.cnt_02 == 2) {
// iOS and iPadOS don't have the last 0xFF packet.
setups_data.detected_os = OS_IOS;
return;
}
if (setups_data.cnt_ff == 0 && setups_data.cnt_02 == 3 && setups_data.cnt_04 == 1) {
// This is actually PS5.
setups_data.detected_os = OS_LINUX;
return;
}
if (setups_data.cnt_ff >= 1 && setups_data.cnt_02 == 0 && setups_data.cnt_04 == 0) {
// This is actually Quest 2 or Nintendo Switch.
setups_data.detected_os = OS_LINUX;
return;
}
}

void process_wlength(const uint16_t w_length) {
# ifdef OS_DETECTION_DEBUG_ENABLE
usb_setups[setups_data.count] = w_length;
# endif
setups_data.count++;
setups_data.last_wlength = w_length;
if (w_length == 0x2) {
setups_data.cnt_02++;
} else if (w_length == 0x4) {
setups_data.cnt_04++;
} else if (w_length == 0xFF) {
setups_data.cnt_ff++;
}
make_guess();
}

os_variant_t detected_host_os(void) {
return setups_data.detected_os;
}

void erase_wlength_data(void) {
memset(&setups_data, 0, sizeof(setups_data));
}
#endif // OS_DETECTION_ENABLE

#ifdef OS_DETECTION_DEBUG_ENABLE
void print_stored_setups(void) {
# ifdef CONSOLE_ENABLE
uint8_t cnt = eeprom_read_byte(EEPROM_USER_OFFSET);
for (uint16_t i = 0; i < cnt; ++i) {
uint16_t* addr = (uint16_t*)EEPROM_USER_OFFSET + i * sizeof(uint16_t) + sizeof(uint8_t);
xprintf("i: %d, wLength: 0x%02X\n", i, eeprom_read_word(addr));
}
# endif
}

void store_setups_in_eeprom(void) {
eeprom_update_byte(EEPROM_USER_OFFSET, setups_data.count);
for (uint16_t i = 0; i < setups_data.count; ++i) {
uint16_t* addr = (uint16_t*)EEPROM_USER_OFFSET + i * sizeof(uint16_t) + sizeof(uint8_t);
eeprom_update_word(addr, usb_setups[i]);
}
}

#endif // OS_DETECTION_DEBUG_ENABLE
38 changes: 38 additions & 0 deletions quantum/os_detection.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/* Copyright 2022 Ruslan Sayfutdinov (@KapJI)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

#pragma once

#include <stdint.h>

#ifdef OS_DETECTION_ENABLE
typedef enum {
OS_UNSURE,
OS_LINUX,
OS_WINDOWS,
OS_MACOS,
OS_IOS,
} os_variant_t;

void process_wlength(const uint16_t w_length);
os_variant_t detected_host_os(void);
void erase_wlength_data(void);
#endif

#ifdef OS_DETECTION_DEBUG_ENABLE
void print_stored_setups(void);
void store_setups_in_eeprom(void);
#endif
Loading

0 comments on commit 2a6d31b

Please sign in to comment.