os_detection.c (8697B)
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 "nvm_eeprom_eeconfig_internal.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 33 static 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 46 struct 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 54 struct setups_data_t setups_data = { 55 .count = 0, 56 .cnt_02 = 0, 57 .cnt_04 = 0, 58 .cnt_ff = 0, 59 }; 60 61 static volatile os_variant_t detected_os = OS_UNSURE; 62 static 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 65 static volatile bool first_report = true; 66 67 // to react on USB state changes 68 static volatile struct usb_device_state current_usb_device_state = {.configure_state = USB_DEVICE_STATE_NO_INIT}; 69 static volatile struct usb_device_state maxprev_usb_device_state = {.configure_state = USB_DEVICE_STATE_NO_INIT}; 70 71 // to reset the keyboard on USB state change 72 #ifdef OS_DETECTION_KEYBOARD_RESET 73 # ifndef OS_DETECTION_RESET_DEBOUNCE 74 # define OS_DETECTION_RESET_DEBOUNCE OS_DETECTION_DEBOUNCE 75 # endif 76 static volatile fast_timer_t configured_since = 0; 77 static volatile bool reset_pending = false; 78 #endif 79 80 // the OS detection might be unstable for a while, "debounce" it 81 static volatile bool debouncing = false; 82 static volatile fast_timer_t last_time = 0; 83 84 bool process_detected_host_os_modules(os_variant_t os); 85 86 void os_detection_task(void) { 87 #ifdef OS_DETECTION_KEYBOARD_RESET 88 // resetting the keyboard on the USB device state change callback results in instability, so delegate that to this task 89 if (reset_pending) { 90 soft_reset_keyboard(); 91 } 92 // reset the keyboard if it is stuck in the init state for longer than debounce duration, which can happen with some KVMs 93 if (current_usb_device_state.configure_state <= USB_DEVICE_STATE_INIT && maxprev_usb_device_state.configure_state >= USB_DEVICE_STATE_CONFIGURED) { 94 if (debouncing && timer_elapsed_fast(last_time) >= OS_DETECTION_DEBOUNCE) { 95 soft_reset_keyboard(); 96 } 97 return; 98 } 99 #endif 100 #ifdef OS_DETECTION_SINGLE_REPORT 101 if (!first_report) { 102 return; 103 } 104 #endif 105 if (current_usb_device_state.configure_state == USB_DEVICE_STATE_CONFIGURED) { 106 // debouncing goes for both the detected OS as well as the USB state 107 if (debouncing && timer_elapsed_fast(last_time) >= OS_DETECTION_DEBOUNCE) { 108 debouncing = false; 109 last_time = 0; 110 if (detected_os != reported_os || first_report) { 111 first_report = false; 112 reported_os = detected_os; 113 process_detected_host_os_modules(detected_os); 114 process_detected_host_os_kb(detected_os); 115 } 116 } 117 } 118 } 119 120 __attribute__((weak)) bool process_detected_host_os_modules(os_variant_t os) { 121 return true; 122 } 123 124 __attribute__((weak)) bool process_detected_host_os_kb(os_variant_t detected_os) { 125 return process_detected_host_os_user(detected_os); 126 } 127 128 __attribute__((weak)) bool process_detected_host_os_user(os_variant_t detected_os) { 129 return true; 130 } 131 132 // Some collected sequences of wLength can be found in tests. 133 void process_wlength(const uint16_t w_length) { 134 #ifdef OS_DETECTION_DEBUG_ENABLE 135 usb_setups[setups_data.count] = w_length; 136 #endif 137 setups_data.count++; 138 setups_data.last_wlength = w_length; 139 if (w_length == 0x2) { 140 setups_data.cnt_02++; 141 } else if (w_length == 0x4) { 142 setups_data.cnt_04++; 143 } else if (w_length == 0xFF) { 144 setups_data.cnt_ff++; 145 } 146 147 // now try to make a guess 148 os_variant_t guessed = OS_UNSURE; 149 if (setups_data.count >= 3) { 150 if (setups_data.cnt_ff >= 2 && setups_data.cnt_04 >= 1) { 151 guessed = OS_WINDOWS; 152 } else if (setups_data.count == setups_data.cnt_ff) { 153 // Linux has 3 packets with 0xFF. 154 guessed = OS_LINUX; 155 } else if (setups_data.count >= 5 && setups_data.last_wlength == 0xFF && setups_data.cnt_ff >= 1 && setups_data.cnt_02 >= 2) { 156 guessed = OS_MACOS; 157 } else if (setups_data.count == 4 && setups_data.cnt_ff == 0 && setups_data.cnt_02 == 2) { 158 // iOS and iPadOS don't have the last 0xFF packet. 159 guessed = OS_IOS; 160 } else if (setups_data.cnt_ff == 0 && setups_data.cnt_02 == 3 && setups_data.cnt_04 == 1) { 161 // This is actually PS5. 162 guessed = OS_LINUX; 163 } else if (setups_data.cnt_ff >= 1 && setups_data.cnt_02 == 0 && setups_data.cnt_04 == 0) { 164 // This is actually Quest 2 or Nintendo Switch. 165 guessed = OS_LINUX; 166 } 167 } 168 169 // only replace the guessed value if not unsure 170 if (guessed != OS_UNSURE) { 171 detected_os = guessed; 172 } 173 174 // whatever the result, debounce 175 last_time = timer_read_fast(); 176 debouncing = true; 177 } 178 179 os_variant_t detected_host_os(void) { 180 return detected_os; 181 } 182 183 void erase_wlength_data(void) { 184 memset(&setups_data, 0, sizeof(setups_data)); 185 detected_os = OS_UNSURE; 186 reported_os = OS_UNSURE; 187 current_usb_device_state.configure_state = USB_DEVICE_STATE_NO_INIT; 188 maxprev_usb_device_state.configure_state = USB_DEVICE_STATE_NO_INIT; 189 debouncing = false; 190 last_time = 0; 191 first_report = true; 192 } 193 194 void os_detection_notify_usb_device_state_change(struct usb_device_state usb_device_state) { 195 // treat this like any other source of instability 196 if (maxprev_usb_device_state.configure_state < current_usb_device_state.configure_state) { 197 maxprev_usb_device_state.configure_state = current_usb_device_state.configure_state; 198 } 199 current_usb_device_state = usb_device_state; 200 last_time = timer_read_fast(); 201 debouncing = true; 202 203 #ifdef OS_DETECTION_KEYBOARD_RESET 204 if (configured_since == 0 && current_usb_device_state.configure_state == USB_DEVICE_STATE_CONFIGURED) { 205 configured_since = timer_read_fast(); 206 } else if (current_usb_device_state.configure_state == USB_DEVICE_STATE_INIT) { 207 // reset the keyboard only if it's been stable for at least debounce duration, to avoid issues with some KVMs 208 if (configured_since > 0 && timer_elapsed_fast(configured_since) >= OS_DETECTION_RESET_DEBOUNCE) { 209 reset_pending = true; 210 } 211 configured_since = 0; 212 } 213 #endif 214 } 215 216 #if defined(SPLIT_KEYBOARD) && defined(SPLIT_DETECTED_OS_ENABLE) 217 void slave_update_detected_host_os(os_variant_t os) { 218 detected_os = os; 219 last_time = timer_read_fast(); 220 debouncing = true; 221 } 222 #endif 223 224 #ifdef OS_DETECTION_DEBUG_ENABLE 225 void print_stored_setups(void) { 226 # ifdef CONSOLE_ENABLE 227 uint8_t cnt = eeprom_read_byte(EEPROM_USER_OFFSET); 228 for (uint16_t i = 0; i < cnt; ++i) { 229 uint16_t* addr = (uint16_t*)EEPROM_USER_OFFSET + i * sizeof(uint16_t) + sizeof(uint8_t); 230 xprintf("i: %d, wLength: 0x%02X\n", i, eeprom_read_word(addr)); 231 } 232 # endif 233 } 234 235 void store_setups_in_eeprom(void) { 236 eeprom_update_byte(EEPROM_USER_OFFSET, setups_data.count); 237 for (uint16_t i = 0; i < setups_data.count; ++i) { 238 uint16_t* addr = (uint16_t*)EEPROM_USER_OFFSET + i * sizeof(uint16_t) + sizeof(uint8_t); 239 eeprom_update_word(addr, usb_setups[i]); 240 } 241 } 242 243 #endif // OS_DETECTION_DEBUG_ENABLE