Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Detect host OS based on USB fingerprint #18463

Merged
merged 7 commits into from
Dec 8, 2022
Merged
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
1 change: 1 addition & 0 deletions builddefs/build_test.mk
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,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 $(QUANTUM_PATH)/wear_leveling/tests/rules.mk
include $(QUANTUM_PATH)/logging/print.mk
Expand Down
8 changes: 8 additions & 0 deletions builddefs/common_features.mk
Original file line number Diff line number Diff line change
Expand Up @@ -907,3 +907,11 @@ ifeq ($(strip $(ENCODER_ENABLE)), yes)
OPT_DEFS += -DENCODER_MAP_ENABLE
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 builddefs/testlist.mk
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ FULL_TESTS := $(notdir $(TEST_LIST))

include $(QUANTUM_PATH)/debounce/tests/testlist.mk
include $(QUANTUM_PATH)/encoder/tests/testlist.mk
include $(QUANTUM_PATH)/os_detection/tests/testlist.mk
include $(QUANTUM_PATH)/sequencer/tests/testlist.mk
include $(QUANTUM_PATH)/wear_leveling/tests/testlist.mk
include $(PLATFORM_PATH)/test/testlist.mk
Expand Down
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)
* [Raw HID](feature_rawhid.md)
* [Secure](feature_secure.md)
* [Send String](feature_send_string.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"
drashna marked this conversation as resolved.
Show resolved Hide resolved

# 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