os_detection.c
7463 bytes
1/* Copyright 2022 Ruslan Sayfutdinov (@KapJI)
2 *
3 * This program is free software: you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation, either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 */
16
17#include "os_detection.h"
18
19#include <string.h>
20#include "timer.h"
21#ifdef OS_DETECTION_KEYBOARD_RESET
22# include "quantum.h"
23#endif
24
25#ifdef OS_DETECTION_DEBUG_ENABLE
26# include "eeconfig.h"
27# include "eeprom.h"
28# include "print.h"
29
30# define STORED_USB_SETUPS 50
31# define EEPROM_USER_OFFSET (uint8_t*)EECONFIG_SIZE
32
33static uint16_t usb_setups[STORED_USB_SETUPS];
34#endif
35
36#ifndef OS_DETECTION_DEBOUNCE
37# define OS_DETECTION_DEBOUNCE 250
38#endif
39
40// 2s should always be more than enough (otherwise, you may have other issues)
41#if OS_DETECTION_DEBOUNCE > 2000
42# undef OS_DETECTION_DEBOUNCE
43# define OS_DETECTION_DEBOUNCE 2000
44#endif
45
46struct setups_data_t {
47 uint8_t count;
48 uint8_t cnt_02;
49 uint8_t cnt_04;
50 uint8_t cnt_ff;
51 uint16_t last_wlength;
52};
53
54struct setups_data_t setups_data = {
55 .count = 0,
56 .cnt_02 = 0,
57 .cnt_04 = 0,
58 .cnt_ff = 0,
59};
60
61static volatile os_variant_t detected_os = OS_UNSURE;
62static volatile os_variant_t reported_os = OS_UNSURE;
63
64// we need to be able to report OS_UNSURE if that is the stable result of the guesses
65static volatile bool first_report = true;
66
67// to react on USB state changes
68static volatile struct usb_device_state current_usb_device_state = {.configure_state = USB_DEVICE_STATE_NO_INIT};
69static volatile struct usb_device_state maxprev_usb_device_state = {.configure_state = USB_DEVICE_STATE_NO_INIT};
70
71// the OS detection might be unstable for a while, "debounce" it
72static volatile bool debouncing = false;
73static volatile fast_timer_t last_time = 0;
74
75void os_detection_task(void) {
76#ifdef OS_DETECTION_KEYBOARD_RESET
77 // resetting the keyboard on the USB device state change callback results in instability, so delegate that to this task
78 // only take action if it's been stable at least once, to avoid issues with some KVMs
79 if (current_usb_device_state.configure_state <= USB_DEVICE_STATE_INIT && maxprev_usb_device_state.configure_state >= USB_DEVICE_STATE_CONFIGURED) {
80 if (debouncing && timer_elapsed_fast(last_time) >= OS_DETECTION_DEBOUNCE) {
81 soft_reset_keyboard();
82 }
83 return;
84 }
85#endif
86#ifdef OS_DETECTION_SINGLE_REPORT
87 if (!first_report) {
88 return;
89 }
90#endif
91 if (current_usb_device_state.configure_state == USB_DEVICE_STATE_CONFIGURED) {
92 // debouncing goes for both the detected OS as well as the USB state
93 if (debouncing && timer_elapsed_fast(last_time) >= OS_DETECTION_DEBOUNCE) {
94 debouncing = false;
95 last_time = 0;
96 if (detected_os != reported_os || first_report) {
97 first_report = false;
98 reported_os = detected_os;
99 process_detected_host_os_kb(detected_os);
100 }
101 }
102 }
103}
104
105__attribute__((weak)) bool process_detected_host_os_kb(os_variant_t detected_os) {
106 return process_detected_host_os_user(detected_os);
107}
108
109__attribute__((weak)) bool process_detected_host_os_user(os_variant_t detected_os) {
110 return true;
111}
112
113// Some collected sequences of wLength can be found in tests.
114void process_wlength(const uint16_t w_length) {
115#ifdef OS_DETECTION_DEBUG_ENABLE
116 usb_setups[setups_data.count] = w_length;
117#endif
118 setups_data.count++;
119 setups_data.last_wlength = w_length;
120 if (w_length == 0x2) {
121 setups_data.cnt_02++;
122 } else if (w_length == 0x4) {
123 setups_data.cnt_04++;
124 } else if (w_length == 0xFF) {
125 setups_data.cnt_ff++;
126 }
127
128 // now try to make a guess
129 os_variant_t guessed = OS_UNSURE;
130 if (setups_data.count >= 3) {
131 if (setups_data.cnt_ff >= 2 && setups_data.cnt_04 >= 1) {
132 guessed = OS_WINDOWS;
133 } else if (setups_data.count == setups_data.cnt_ff) {
134 // Linux has 3 packets with 0xFF.
135 guessed = OS_LINUX;
136 } else if (setups_data.count == 5 && setups_data.last_wlength == 0xFF && setups_data.cnt_ff == 1 && setups_data.cnt_02 == 2) {
137 guessed = OS_MACOS;
138 } else if (setups_data.count == 4 && setups_data.cnt_ff == 0 && setups_data.cnt_02 == 2) {
139 // iOS and iPadOS don't have the last 0xFF packet.
140 guessed = OS_IOS;
141 } else if (setups_data.cnt_ff == 0 && setups_data.cnt_02 == 3 && setups_data.cnt_04 == 1) {
142 // This is actually PS5.
143 guessed = OS_LINUX;
144 } else if (setups_data.cnt_ff >= 1 && setups_data.cnt_02 == 0 && setups_data.cnt_04 == 0) {
145 // This is actually Quest 2 or Nintendo Switch.
146 guessed = OS_LINUX;
147 }
148 }
149
150 // only replace the guessed value if not unsure
151 if (guessed != OS_UNSURE) {
152 detected_os = guessed;
153 }
154
155 // whatever the result, debounce
156 last_time = timer_read_fast();
157 debouncing = true;
158}
159
160os_variant_t detected_host_os(void) {
161 return detected_os;
162}
163
164void erase_wlength_data(void) {
165 memset(&setups_data, 0, sizeof(setups_data));
166 detected_os = OS_UNSURE;
167 reported_os = OS_UNSURE;
168 current_usb_device_state.configure_state = USB_DEVICE_STATE_NO_INIT;
169 maxprev_usb_device_state.configure_state = USB_DEVICE_STATE_NO_INIT;
170 debouncing = false;
171 last_time = 0;
172 first_report = true;
173}
174
175void os_detection_notify_usb_device_state_change(struct usb_device_state usb_device_state) {
176 // treat this like any other source of instability
177 if (maxprev_usb_device_state.configure_state < current_usb_device_state.configure_state) {
178 maxprev_usb_device_state.configure_state = current_usb_device_state.configure_state;
179 }
180 current_usb_device_state = usb_device_state;
181 last_time = timer_read_fast();
182 debouncing = true;
183}
184
185#if defined(SPLIT_KEYBOARD) && defined(SPLIT_DETECTED_OS_ENABLE)
186void slave_update_detected_host_os(os_variant_t os) {
187 detected_os = os;
188 last_time = timer_read_fast();
189 debouncing = true;
190}
191#endif
192
193#ifdef OS_DETECTION_DEBUG_ENABLE
194void print_stored_setups(void) {
195# ifdef CONSOLE_ENABLE
196 uint8_t cnt = eeprom_read_byte(EEPROM_USER_OFFSET);
197 for (uint16_t i = 0; i < cnt; ++i) {
198 uint16_t* addr = (uint16_t*)EEPROM_USER_OFFSET + i * sizeof(uint16_t) + sizeof(uint8_t);
199 xprintf("i: %d, wLength: 0x%02X\n", i, eeprom_read_word(addr));
200 }
201# endif
202}
203
204void store_setups_in_eeprom(void) {
205 eeprom_update_byte(EEPROM_USER_OFFSET, setups_data.count);
206 for (uint16_t i = 0; i < setups_data.count; ++i) {
207 uint16_t* addr = (uint16_t*)EEPROM_USER_OFFSET + i * sizeof(uint16_t) + sizeof(uint8_t);
208 eeprom_update_word(addr, usb_setups[i]);
209 }
210}
211
212#endif // OS_DETECTION_DEBUG_ENABLE