summaryrefslogtreecommitdiff
path: root/quantum/os_detection.c
blob: 96b026e2471c3759185bbae3b559b31b441064b5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
/* 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>
#include "timer.h"
#ifdef OS_DETECTION_KEYBOARD_RESET
#    include "quantum.h"
#endif

#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

static uint16_t usb_setups[STORED_USB_SETUPS];
#endif

#ifndef OS_DETECTION_DEBOUNCE
#    define OS_DETECTION_DEBOUNCE 200
#endif

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

struct setups_data_t {
    uint8_t  count;
    uint8_t  cnt_02;
    uint8_t  cnt_04;
    uint8_t  cnt_ff;
    uint16_t last_wlength;
};

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

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);
            }
        }
    }
#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;
#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++;
    }

    // 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) {
    return detected_os;
}

void erase_wlength_data(void) {
    memset(&setups_data, 0, sizeof(setups_data));
    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

#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