~ruther/qmk_firmware

80f3da36e571fa702b1d3df693fd545801250eca — Andre Brait 1 year, 3 months ago 77e8867
[Core] Add OS detection callbacks (#21777)

M docs/feature_os_detection.md => docs/feature_os_detection.md +49 -1
@@ 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:


@@ 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.

M quantum/keyboard.c => quantum/keyboard.c +7 -0
@@ 137,6 137,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) {


@@ 741,4 744,8 @@ void keyboard_task(void) {
#endif

    led_task();

#ifdef OS_DETECTION_ENABLE
    os_detection_task();
#endif
}

M quantum/os_detection.c => quantum/os_detection.c +120 -50
@@ 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;


@@ 45,43 50,63 @@ 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;
    }
    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;


@@ 95,7 120,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) {


@@ 104,25 159,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) {


@@ 133,4 201,6 @@ void store_setups_in_eeprom(void) {
    }
}

#endif // OS_DETECTION_DEBUG_ENABLE
#    endif // OS_DETECTION_DEBUG_ENABLE

#endif

M quantum/os_detection.h => quantum/os_detection.h +16 -6
@@ 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,


@@ 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

M quantum/os_detection/tests/os_detection.cpp => quantum/os_detection/tests/os_detection.cpp +229 -0
@@ 18,12 18,20 @@

extern "C" {
#include "os_detection.h"
#include "timer.h"

void advance_time(uint32_t ms);
}

static uint32_t     reported_count;
static os_variant_t reported_os;

class OsDetectionTest : public ::testing::Test {
   protected:
    void SetUp() override {
        erase_wlength_data();
        reported_count = 0;
        reported_os    = OS_UNSURE;
    }
};



@@ 34,6 42,24 @@ os_variant_t check_sequence(const std::vector<uint16_t> &w_lengths) {
    return detected_host_os();
}

bool process_detected_host_os_kb(os_variant_t os) {
    reported_count = reported_count + 1;
    reported_os    = os;
}

void assert_not_reported(void) {
    // check that it does not report the result, nor any intermediate results
    EXPECT_EQ(reported_count, 0);
    EXPECT_EQ(reported_os, OS_UNSURE);
}

void assert_reported(os_variant_t os) {
    // check that it reports exclusively the result, not any intermediate results
    EXPECT_EQ(reported_count, 1);
    EXPECT_EQ(reported_os, os);
    EXPECT_EQ(reported_os, detected_host_os());
}

/* Some collected data.

ChibiOS:


@@ 77,88 103,291 @@ Quest 2: [FF, FF, FF, FE, ...]
*/
TEST_F(OsDetectionTest, TestLinux) {
    EXPECT_EQ(check_sequence({0xFF, 0xFF, 0xFF}), OS_LINUX);
    os_detection_task();
    assert_not_reported();
}

TEST_F(OsDetectionTest, TestChibiosMacos) {
    EXPECT_EQ(check_sequence({0x2, 0x24, 0x2, 0x28, 0xFF}), OS_MACOS);
    os_detection_task();
    assert_not_reported();
}

TEST_F(OsDetectionTest, TestLufaMacos) {
    EXPECT_EQ(check_sequence({0x2, 0x10, 0x2, 0xE, 0xFF}), OS_MACOS);
    os_detection_task();
    assert_not_reported();
}

TEST_F(OsDetectionTest, TestVusbMacos) {
    EXPECT_EQ(check_sequence({0x2, 0xE, 0x2, 0xE, 0xFF}), OS_MACOS);
    os_detection_task();
    assert_not_reported();
}

TEST_F(OsDetectionTest, TestChibiosIos) {
    EXPECT_EQ(check_sequence({0x2, 0x24, 0x2, 0x28}), OS_IOS);
    os_detection_task();
    assert_not_reported();
}

TEST_F(OsDetectionTest, TestLufaIos) {
    EXPECT_EQ(check_sequence({0x2, 0x10, 0x2, 0xE}), OS_IOS);
    os_detection_task();
    assert_not_reported();
}

TEST_F(OsDetectionTest, TestVusbIos) {
    EXPECT_EQ(check_sequence({0x2, 0xE, 0x2, 0xE}), OS_IOS);
    os_detection_task();
    assert_not_reported();
}

TEST_F(OsDetectionTest, TestChibiosWindows10) {
    EXPECT_EQ(check_sequence({0xFF, 0xFF, 0x4, 0x24, 0x4, 0x24, 0x4, 0xFF, 0x24, 0xFF, 0x4, 0xFF, 0x24, 0x4, 0x24, 0x20A, 0x20A, 0x20A, 0x20A, 0x20A, 0x20A, 0x20A, 0x20A, 0x20A, 0x20A, 0x20A, 0x20A, 0x20A, 0x20A, 0x20A, 0x20A, 0x20A, 0x20A, 0x20A, 0x20A, 0x20A, 0x20A, 0x20A, 0x20A}), OS_WINDOWS);
    os_detection_task();
    assert_not_reported();
}

TEST_F(OsDetectionTest, TestChibiosWindows10_2) {
    EXPECT_EQ(check_sequence({0xFF, 0xFF, 0x4, 0x24, 0x4, 0x24, 0x4, 0x24, 0x4, 0x24, 0x4, 0x24}), OS_WINDOWS);
    os_detection_task();
    assert_not_reported();
}

TEST_F(OsDetectionTest, TestLufaWindows10) {
    EXPECT_EQ(check_sequence({0x12, 0xFF, 0xFF, 0x4, 0x10, 0xFF, 0xFF, 0xFF, 0x4, 0x10, 0x20A, 0x20A, 0x20A, 0x20A, 0x20A, 0x20A}), OS_WINDOWS);
    os_detection_task();
    assert_not_reported();
}

TEST_F(OsDetectionTest, TestLufaWindows10_2) {
    EXPECT_EQ(check_sequence({0xFF, 0xFF, 0x4, 0x10, 0xFF, 0x4, 0xFF, 0x10, 0xFF, 0x20A, 0x20A, 0x20A, 0x20A, 0x20A, 0x20A}), OS_WINDOWS);
    os_detection_task();
    assert_not_reported();
}

TEST_F(OsDetectionTest, TestLufaWindows10_3) {
    EXPECT_EQ(check_sequence({0xFF, 0xFF, 0x4, 0x10, 0x4, 0x10}), OS_WINDOWS);
    os_detection_task();
    assert_not_reported();
}

TEST_F(OsDetectionTest, TestVusbWindows10) {
    EXPECT_EQ(check_sequence({0xFF, 0xFF, 0x4, 0xE, 0xFF}), OS_WINDOWS);
    os_detection_task();
    assert_not_reported();
}

TEST_F(OsDetectionTest, TestVusbWindows10_2) {
    EXPECT_EQ(check_sequence({0xFF, 0xFF, 0x4, 0xE, 0x4}), OS_WINDOWS);
    os_detection_task();
    assert_not_reported();
}

TEST_F(OsDetectionTest, TestChibiosPs5) {
    EXPECT_EQ(check_sequence({0x2, 0x4, 0x2, 0x28, 0x2, 0x24}), OS_LINUX);
    os_detection_task();
    assert_not_reported();
}

TEST_F(OsDetectionTest, TestLufaPs5) {
    EXPECT_EQ(check_sequence({0x2, 0x4, 0x2, 0xE, 0x2, 0x10}), OS_LINUX);
    os_detection_task();
    assert_not_reported();
}

TEST_F(OsDetectionTest, TestVusbPs5) {
    EXPECT_EQ(check_sequence({0x2, 0x4, 0x2, 0xE, 0x2}), OS_LINUX);
    os_detection_task();
    assert_not_reported();
}

TEST_F(OsDetectionTest, TestChibiosNintendoSwitch) {
    EXPECT_EQ(check_sequence({0x82, 0xFF, 0x40, 0x40, 0xFF, 0x40, 0x40, 0xFF, 0x40, 0x40, 0xFF, 0x40, 0x40, 0xFF, 0x40, 0x40}), OS_LINUX);
    os_detection_task();
    assert_not_reported();
}

TEST_F(OsDetectionTest, TestLufaNintendoSwitch) {
    EXPECT_EQ(check_sequence({0x82, 0xFF, 0x40, 0x40, 0xFF, 0x40, 0x40}), OS_LINUX);
    os_detection_task();
    assert_not_reported();
}

TEST_F(OsDetectionTest, TestVusbNintendoSwitch) {
    EXPECT_EQ(check_sequence({0x82, 0xFF, 0x40, 0x40}), OS_LINUX);
    os_detection_task();
    assert_not_reported();
}

TEST_F(OsDetectionTest, TestChibiosQuest2) {
    EXPECT_EQ(check_sequence({0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFE, 0xFF, 0xFE, 0xFF, 0xFE, 0xFF}), OS_LINUX);
    os_detection_task();
    assert_not_reported();
}

TEST_F(OsDetectionTest, TestVusbQuest2) {
    EXPECT_EQ(check_sequence({0xFF, 0xFF, 0xFF, 0xFE}), OS_LINUX);
    os_detection_task();
    assert_not_reported();
}

// Regression reported in https://github.com/qmk/qmk_firmware/pull/21777#issuecomment-1922815841
TEST_F(OsDetectionTest, TestDetectMacM1AsIOS) {
    EXPECT_EQ(check_sequence({0x02, 0x32, 0x02, 0x24, 0x101, 0xFF}), OS_IOS);
    os_detection_task();
    assert_not_reported();
}

TEST_F(OsDetectionTest, TestDoNotReportIfUsbUnstable) {
    EXPECT_EQ(check_sequence({0xFF, 0xFF, 0xFF, 0xFE}), OS_LINUX);
    os_detection_task();
    assert_not_reported();

    advance_time(OS_DETECTION_DEBOUNCE);
    os_detection_task();
    assert_not_reported();
    EXPECT_EQ(detected_host_os(), OS_LINUX);
}

TEST_F(OsDetectionTest, TestReportAfterDebounce) {
    EXPECT_EQ(check_sequence({0xFF, 0xFF, 0xFF, 0xFE}), OS_LINUX);
    os_detection_notify_usb_device_state_change(USB_DEVICE_STATE_CONFIGURED);
    os_detection_task();
    assert_not_reported();

    advance_time(1);
    os_detection_task();
    assert_not_reported();
    EXPECT_EQ(detected_host_os(), OS_LINUX);

    advance_time(OS_DETECTION_DEBOUNCE - 3);
    os_detection_task();
    assert_not_reported();
    EXPECT_EQ(detected_host_os(), OS_LINUX);

    advance_time(1);
    os_detection_task();
    assert_not_reported();
    EXPECT_EQ(detected_host_os(), OS_LINUX);

    // advancing the timer alone must not cause a report
    advance_time(1);
    assert_not_reported();
    EXPECT_EQ(detected_host_os(), OS_LINUX);
    // the task will cause a report
    os_detection_task();
    assert_reported(OS_LINUX);
    EXPECT_EQ(detected_host_os(), OS_LINUX);

    // check that it remains the same after a long time
    advance_time(OS_DETECTION_DEBOUNCE * 15);
    assert_reported(OS_LINUX);
    EXPECT_EQ(detected_host_os(), OS_LINUX);
}

TEST_F(OsDetectionTest, TestReportAfterDebounceLongWait) {
    EXPECT_EQ(check_sequence({0x12, 0xFF, 0xFF, 0x4, 0x10, 0xFF, 0xFF, 0xFF, 0x4, 0x10, 0x20A, 0x20A, 0x20A, 0x20A, 0x20A, 0x20A}), OS_WINDOWS);
    os_detection_notify_usb_device_state_change(USB_DEVICE_STATE_CONFIGURED);
    os_detection_task();
    assert_not_reported();

    advance_time(1);
    os_detection_task();
    assert_not_reported();
    EXPECT_EQ(detected_host_os(), OS_WINDOWS);

    // advancing the timer alone must not cause a report
    advance_time(OS_DETECTION_DEBOUNCE * 15);
    assert_not_reported();
    EXPECT_EQ(detected_host_os(), OS_WINDOWS);
    // the task will cause a report
    os_detection_task();
    assert_reported(OS_WINDOWS);
    EXPECT_EQ(detected_host_os(), OS_WINDOWS);

    // check that it remains the same after a long time
    advance_time(OS_DETECTION_DEBOUNCE * 10);
    os_detection_task();
    assert_reported(OS_WINDOWS);
    EXPECT_EQ(detected_host_os(), OS_WINDOWS);
}

TEST_F(OsDetectionTest, TestReportUnsure) {
    EXPECT_EQ(check_sequence({0x12, 0xFF}), OS_UNSURE);
    os_detection_notify_usb_device_state_change(USB_DEVICE_STATE_CONFIGURED);
    os_detection_task();
    assert_not_reported();

    advance_time(1);
    os_detection_task();
    assert_not_reported();
    EXPECT_EQ(detected_host_os(), OS_UNSURE);

    // advancing the timer alone must not cause a report
    advance_time(OS_DETECTION_DEBOUNCE - 1);
    assert_not_reported();
    EXPECT_EQ(detected_host_os(), OS_UNSURE);
    // the task will cause a report
    os_detection_task();
    assert_reported(OS_UNSURE);
    EXPECT_EQ(detected_host_os(), OS_UNSURE);

    // check that it remains the same after a long time
    advance_time(OS_DETECTION_DEBOUNCE * 10);
    os_detection_task();
    assert_reported(OS_UNSURE);
    EXPECT_EQ(detected_host_os(), OS_UNSURE);
}

TEST_F(OsDetectionTest, TestDoNotReportIntermediateResults) {
    EXPECT_EQ(check_sequence({0x12, 0xFF}), OS_UNSURE);
    os_detection_notify_usb_device_state_change(USB_DEVICE_STATE_CONFIGURED);
    os_detection_task();
    assert_not_reported();

    advance_time(OS_DETECTION_DEBOUNCE - 1);
    os_detection_task();
    assert_not_reported();
    EXPECT_EQ(detected_host_os(), OS_UNSURE);

    // at this stage, the final result has not been reached yet
    EXPECT_EQ(check_sequence({0xFF}), OS_LINUX);
    os_detection_notify_usb_device_state_change(USB_DEVICE_STATE_CONFIGURED);
    advance_time(OS_DETECTION_DEBOUNCE - 1);
    os_detection_task();
    assert_not_reported();
    // the intermedite but yet unstable result is exposed through detected_host_os()
    EXPECT_EQ(detected_host_os(), OS_LINUX);

    // the remainder is processed
    EXPECT_EQ(check_sequence({0x4, 0x10, 0xFF, 0xFF, 0xFF, 0x4, 0x10, 0x20A, 0x20A, 0x20A, 0x20A, 0x20A, 0x20A}), OS_WINDOWS);
    os_detection_notify_usb_device_state_change(USB_DEVICE_STATE_CONFIGURED);
    advance_time(OS_DETECTION_DEBOUNCE - 1);
    os_detection_task();
    assert_not_reported();
    EXPECT_EQ(detected_host_os(), OS_WINDOWS);

    // advancing the timer alone must not cause a report
    advance_time(1);
    assert_not_reported();
    EXPECT_EQ(detected_host_os(), OS_WINDOWS);
    // the task will cause a report
    os_detection_task();
    assert_reported(OS_WINDOWS);
    EXPECT_EQ(detected_host_os(), OS_WINDOWS);

    // check that it remains the same after a long time
    advance_time(OS_DETECTION_DEBOUNCE * 10);
    os_detection_task();
    assert_reported(OS_WINDOWS);
    EXPECT_EQ(detected_host_os(), OS_WINDOWS);
}

TEST_F(OsDetectionTest, TestDoNotGoBackToUnsure) {
    // 0x02 would cause it to go back to Unsure, so check that it does not
    EXPECT_EQ(check_sequence({0xFF, 0xFF, 0xFF, 0xFE, 0x02}), OS_LINUX);
    os_detection_task();
    assert_not_reported();
}

M quantum/os_detection/tests/rules.mk => quantum/os_detection/tests/rules.mk +3 -1
@@ 1,5 1,7 @@
os_detection_DEFS := -DOS_DETECTION_ENABLE
os_detection_DEFS += -DOS_DETECTION_DEBOUNCE=50

os_detection_SRC := \
    $(QUANTUM_PATH)/os_detection/tests/os_detection.cpp \
    $(QUANTUM_PATH)/os_detection.c
    $(QUANTUM_PATH)/os_detection.c \
    $(PLATFORM_PATH)/$(PLATFORM_KEY)/timer.c

M quantum/quantum.h => quantum/quantum.h +4 -0
@@ 236,6 236,10 @@ extern layer_state_t layer_state;
#    include "process_repeat_key.h"
#endif

#ifdef OS_DETECTION_ENABLE
#    include "os_detection.h"
#endif

void set_single_persistent_default_layer(uint8_t default_layer);

#define IS_LAYER_ON(layer) layer_state_is(layer)

M tmk_core/protocol/usb_device_state.c => tmk_core/protocol/usb_device_state.c +9 -0
@@ 20,6 20,10 @@
#    include "haptic.h"
#endif

#ifdef OS_DETECTION_ENABLE
#    include "os_detection.h"
#endif

enum usb_device_state usb_device_state = USB_DEVICE_STATE_NO_INIT;

__attribute__((weak)) void notify_usb_device_state_change_kb(enum usb_device_state usb_device_state) {


@@ 32,7 36,12 @@ static void notify_usb_device_state_change(enum usb_device_state usb_device_stat
#if defined(HAPTIC_ENABLE) && HAPTIC_OFF_IN_LOW_POWER
    haptic_notify_usb_device_state_change();
#endif

    notify_usb_device_state_change_kb(usb_device_state);

#ifdef OS_DETECTION_ENABLE
    os_detection_notify_usb_device_state_change(usb_device_state);
#endif
}

void usb_device_state_set_configuration(bool isConfigured, uint8_t configurationNumber) {

Do not follow this link