Skip to content

Commit

Permalink
[Core] Add OS detection callbacks (qmk#21777)
Browse files Browse the repository at this point in the history
  • Loading branch information
andrebrait authored and matt committed Feb 26, 2024
1 parent 2aea79d commit 9bfa799
Show file tree
Hide file tree
Showing 8 changed files with 438 additions and 63 deletions.
50 changes: 49 additions & 1 deletion docs/feature_os_detection.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ In your `rules.mk` add:
OS_DETECTION_ENABLE = yes
```

Include `"os_detection.h"` in your `keymap.c`.
It will automatically include the required headers file.
It declares `os_variant_t detected_host_os(void);` which you can call to get detected OS.

It returns one of the following values:
Expand All @@ -32,6 +32,54 @@ enum {
?> 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.

## Callbacks :id=callbacks

If you want to perform custom actions when the OS is detected, then you can use the `process_detected_host_os_kb` function on the keyboard level source file, or `process_detected_host_os_user` function in the user `keymap.c`.

```c
bool process_detected_host_os_kb(os_variant_t detected_os) {
if (!process_detected_host_os_user(detected_os)) {
return false;
}
switch (detected_os) {
case OS_MACOS:
case OS_IOS:
rgb_matrix_set_color_all(RGB_WHITE);
break;
case OS_WINDOWS:
rgb_matrix_set_color_all(RGB_BLUE);
break;
case OS_LINUX:
rgb_matrix_set_color_all(RGB_ORANGE);
break;
case OS_UNSURE:
rgb_matrix_set_color_all(RGB_RED);
break;
}

return true;
}
```
## OS detection stability
The OS detection is currently handled while the USB device descriptor is being assembled.
The process is done in steps, generating a number of intermediate results until it stabilizes.
We therefore resort to debouncing the result until it has been stable for a given amount of milliseconds.
This amount can be configured, in case your board is not stable within the default debouncing time of 200ms.
## KVM and USB switches
Some KVM and USB switches may not trigger the USB controller on the keyboard to fully reset upon switching machines.
If your keyboard does not redetect the OS in this situation, you can force the keyboard to reset when the USB initialization event is detected, forcing the USB controller to be reconfigured.
## Configuration Options
* `#define OS_DETECTION_DEBOUNCE 200`
* defined the debounce time for OS detection, in milliseconds
* `#define OS_DETECTION_KEYBOARD_RESET`
* enables the keyboard reset upon a USB device reinitilization, such as switching devices on some KVMs
## Debug
If OS is guessed incorrectly, you may want to collect data about USB setup packets to refine the detection logic.
Expand Down
7 changes: 7 additions & 0 deletions quantum/keyboard.c
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#ifdef WPM_ENABLE
# include "wpm.h"
#endif
#ifdef OS_DETECTION_ENABLE
# include "os_detection.h"
#endif

static uint32_t last_input_modification_time = 0;
uint32_t last_input_activity_time(void) {
Expand Down Expand Up @@ -718,4 +721,8 @@ void keyboard_task(void) {
#endif

led_task();

#ifdef OS_DETECTION_ENABLE
os_detection_task();
#endif
}
176 changes: 121 additions & 55 deletions quantum/os_detection.c
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,25 @@

#include "os_detection.h"

#include <string.h>
#ifdef OS_DETECTION_ENABLE

#ifdef OS_DETECTION_DEBUG_ENABLE
# include "eeconfig.h"
# include "eeprom.h"
# include "print.h"
# include <string.h>
# include "timer.h"
# ifdef OS_DETECTION_KEYBOARD_RESET
# include "quantum.h"
# endif

# define STORED_USB_SETUPS 50
# define EEPROM_USER_OFFSET (uint8_t*)EECONFIG_SIZE
# ifdef OS_DETECTION_DEBUG_ENABLE
# include "eeconfig.h"
# include "eeprom.h"
# include "print.h"

uint16_t usb_setups[STORED_USB_SETUPS];
#endif
# define STORED_USB_SETUPS 50
# define EEPROM_USER_OFFSET (uint8_t*)EECONFIG_SIZE

static uint16_t usb_setups[STORED_USB_SETUPS];
# endif

#ifdef OS_DETECTION_ENABLE
struct setups_data_t {
uint8_t count;
uint8_t cnt_02;
Expand All @@ -45,48 +50,64 @@ struct setups_data_t setups_data = {
.cnt_ff = 0,
};

os_variant_t detected_os = OS_UNSURE;
# ifndef OS_DETECTION_DEBOUNCE
# define OS_DETECTION_DEBOUNCE 200
# endif

// 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) {
detected_os = OS_WINDOWS;
return;
}
if (setups_data.count == setups_data.cnt_ff) {
// Linux has 3 packets with 0xFF.
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) {
detected_os = OS_MACOS;
return;
}
// My mac responds thusly:
if (setups_data.count == 6 && setups_data.last_wlength == 0xFF && setups_data.cnt_ff == 2 && setups_data.cnt_02 == 2) {
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.
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.
detected_os = OS_LINUX;
return;
// 2s should always be more than enough (otherwise, you may have other issues)
# if OS_DETECTION_DEBOUNCE > 2000
# undef OS_DETECTION_DEBOUNCE
# define OS_DETECTION_DEBOUNCE 2000
# endif

typedef uint16_t debouncing_t;

static volatile os_variant_t detected_os = OS_UNSURE;
static os_variant_t reported_os = OS_UNSURE;

// we need to be able to report OS_UNSURE if that is the stable result of the guesses
static bool first_report = true;

// to react on USB state changes
static volatile enum usb_device_state current_usb_device_state = USB_DEVICE_STATE_INIT;
static enum usb_device_state reported_usb_device_state = USB_DEVICE_STATE_INIT;

// the OS detection might be unstable for a while, "debounce" it
static volatile bool debouncing = false;
static volatile fast_timer_t last_time;

void os_detection_task(void) {
if (current_usb_device_state == USB_DEVICE_STATE_CONFIGURED) {
// debouncing goes for both the detected OS as well as the USB state
if (debouncing && timer_elapsed_fast(last_time) >= OS_DETECTION_DEBOUNCE) {
debouncing = false;
reported_usb_device_state = current_usb_device_state;
if (detected_os != reported_os || first_report) {
first_report = false;
reported_os = detected_os;
process_detected_host_os_kb(detected_os);
}
}
}
if (setups_data.cnt_ff >= 1 && setups_data.cnt_02 == 0 && setups_data.cnt_04 == 0) {
// This is actually Quest 2 or Nintendo Switch.
detected_os = OS_LINUX;
return;

# ifdef OS_DETECTION_KEYBOARD_RESET
// resetting the keyboard on the USB device state change callback results in instability, so delegate that to this task
// only take action if it's been stable at least once, to avoid issues with some KVMs
else if (current_usb_device_state == USB_DEVICE_STATE_INIT && reported_usb_device_state != USB_DEVICE_STATE_INIT) {
soft_reset_keyboard();
}
# endif
}

__attribute__((weak)) bool process_detected_host_os_kb(os_variant_t detected_os) {
return process_detected_host_os_user(detected_os);
}

__attribute__((weak)) bool process_detected_host_os_user(os_variant_t detected_os) {
return true;
}

// Some collected sequences of wLength can be found in tests.
void process_wlength(const uint16_t w_length) {
# ifdef OS_DETECTION_DEBUG_ENABLE
usb_setups[setups_data.count] = w_length;
Expand All @@ -100,7 +121,37 @@ void process_wlength(const uint16_t w_length) {
} else if (w_length == 0xFF) {
setups_data.cnt_ff++;
}
make_guess();

// now try to make a guess
os_variant_t guessed = OS_UNSURE;
if (setups_data.count >= 3) {
if (setups_data.cnt_ff >= 2 && setups_data.cnt_04 >= 1) {
guessed = OS_WINDOWS;
} else if (setups_data.count == setups_data.cnt_ff) {
// Linux has 3 packets with 0xFF.
guessed = OS_LINUX;
} else if (setups_data.count == 5 && setups_data.last_wlength == 0xFF && setups_data.cnt_ff == 1 && setups_data.cnt_02 == 2) {
guessed = OS_MACOS;
} else 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.
guessed = OS_IOS;
} else if (setups_data.cnt_ff == 0 && setups_data.cnt_02 == 3 && setups_data.cnt_04 == 1) {
// This is actually PS5.
guessed = OS_LINUX;
} else if (setups_data.cnt_ff >= 1 && setups_data.cnt_02 == 0 && setups_data.cnt_04 == 0) {
// This is actually Quest 2 or Nintendo Switch.
guessed = OS_LINUX;
}
}

// only replace the guessed value if not unsure
if (guessed != OS_UNSURE) {
detected_os = guessed;
}

// whatever the result, debounce
last_time = timer_read_fast();
debouncing = true;
}

os_variant_t detected_host_os(void) {
Expand All @@ -109,25 +160,38 @@ os_variant_t detected_host_os(void) {

void erase_wlength_data(void) {
memset(&setups_data, 0, sizeof(setups_data));
detected_os = OS_UNSURE;
detected_os = OS_UNSURE;
reported_os = OS_UNSURE;
current_usb_device_state = USB_DEVICE_STATE_INIT;
reported_usb_device_state = USB_DEVICE_STATE_INIT;
debouncing = false;
first_report = true;
}

void os_detection_notify_usb_device_state_change(enum usb_device_state usb_device_state) {
// treat this like any other source of instability
current_usb_device_state = usb_device_state;
last_time = timer_read_fast();
debouncing = true;
}

# if defined(SPLIT_KEYBOARD) && defined(SPLIT_DETECTED_OS_ENABLE)
void slave_update_detected_host_os(os_variant_t os) {
detected_os = os;
last_time = timer_read_fast();
debouncing = true;
}
# endif // defined(SPLIT_KEYBOARD) && defined(SPLIT_DETECTED_OS_ENABLE)
#endif // OS_DETECTION_ENABLE
# endif

#ifdef OS_DETECTION_DEBUG_ENABLE
# ifdef OS_DETECTION_DEBUG_ENABLE
void print_stored_setups(void) {
# ifdef CONSOLE_ENABLE
# 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
# endif
}

void store_setups_in_eeprom(void) {
Expand All @@ -138,4 +202,6 @@ void store_setups_in_eeprom(void) {
}
}

#endif // OS_DETECTION_DEBUG_ENABLE
# endif // OS_DETECTION_DEBUG_ENABLE

#endif
22 changes: 16 additions & 6 deletions quantum/os_detection.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@

#pragma once

#include <stdint.h>

#ifdef OS_DETECTION_ENABLE

# include <stdint.h>
# include <stdbool.h>
# include "usb_device_state.h"

typedef enum {
OS_UNSURE,
OS_LINUX,
Expand All @@ -30,13 +33,20 @@ typedef enum {
void process_wlength(const uint16_t w_length);
os_variant_t detected_host_os(void);
void erase_wlength_data(void);
void os_detection_notify_usb_device_state_change(enum usb_device_state usb_device_state);

void os_detection_task(void);

bool process_detected_host_os_kb(os_variant_t os);
bool process_detected_host_os_user(os_variant_t os);

# if defined(SPLIT_KEYBOARD) && defined(SPLIT_DETECTED_OS_ENABLE)
void slave_update_detected_host_os(os_variant_t os);
# endif // defined(SPLIT_KEYBOARD) && defined(SPLIT_DETECTED_OS_ENABLE)
#endif
# endif

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

#endif // OS_DETECTION_ENABLE
Loading

0 comments on commit 9bfa799

Please sign in to comment.