///////////////////////////////////////////////////////////////////////////////////////////////////////// // // Name: HID.cpp // Created: Mar 2022 // Version: v1.0 // Author(s): Philip Smart // Description: Final class for the encapsulation and presentation of differing input devices to // an instantiating object for the provision of HID input services. This class // provides a public API which a caller uses to receive keyboard and mouse data. // No other HID devices are planned at this time but given Bluetooth is being used, // the potential exists for other devices to be used. // Credits: // Copyright: (c) 2019-2022 Philip Smart // // History: Mar 2022 - Initial write. // v1.01 May 2022 - Initial release version. // v1.02 Jun 2022 - Updates to support Bluetooth keyboard and mouse. The mouse can be // a primary device or a secondary device for hosts which support // keyboard and mouse over one physical port. // // Notes: See Makefile to enable/disable conditional components // ///////////////////////////////////////////////////////////////////////////////////////////////////////// // This source file 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 3 of the License, or // (at your option) any later version. // // This source file 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 . ///////////////////////////////////////////////////////////////////////////////////////////////////////// #include #include #include #include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "esp_log.h" #include "esp_system.h" #include "driver/gpio.h" #include "soc/timer_group_struct.h" #include "soc/timer_group_reg.h" #include "driver/timer.h" #include "PS2KeyAdvanced.h" #include "PS2Mouse.h" #include "sdkconfig.h" #include "HID.h" // Tag for ESP HID logging. #define HIDTAG "HID" // Out of object pointer needed in the ESP API callbacks. HID *pHIDThis = NULL; // Method to start Bluetooth pairing. // The SharpKey doesnt have an output device so it is not possible to select a device for pairing or allow pairing key input. // This limits us to mainly BLE devices and some BT devices which dont require a pairing key. // The method used is to scan and select the first device found which is in pairing mode. It will be the users responsibility // to ensure no other Bluetooth devices are close by and pairing. // void HID::btStartPairing(void) { // Locals. // int scanCnt = 0; std::vector scanList; // Only pair if bluetooth is enabled. if(hidCtrl.hidDevice == HID_DEVICE_BT_KEYBOARD || hidCtrl.hidDevice == HID_DEVICE_BT_MOUSE || hidCtrl.hidDevice == HID_DEVICE_BLUETOOTH) { ESP_LOGW(HIDTAG, "Bluetooth Pairing Requested\n"); // Scan for a device in 5 second chunks, maximum 60 seconds before giving up. do { vTaskDelay(1); btHID->getDeviceList(scanList, 5); // Required to discover new keyboards and for pairing scanCnt++; // For debug purposes, print out any device found. for(std::size_t idx = 0; idx < scanList.size(); idx++) { ESP_LOGI(HIDTAG, "We have device:%s, %s, %d, %s", scanList[idx].deviceAddr.c_str(), scanList[idx].name.c_str(), scanList[idx].rssi, scanList[idx].deviceType.c_str()); } // If devices were found, try and open them until success or end of list. for(std::size_t idx = 0; idx < scanList.size(); idx++) { // Try and open the device, on failure move onto next device. if(btHID->openDevice(scanList[idx].bda, scanList[idx].transport, scanList[idx].ble.addr_type) == true) { ESP_LOGI(HIDTAG, "BT enabled on device:%s, %s, %d, %s", scanList[idx].deviceAddr.c_str(), scanList[idx].name.c_str(), scanList[idx].rssi, scanList[idx].deviceType.c_str()); } } } while(scanCnt < 11); } else { ESP_LOGW(HIDTAG, "Bluetooth Pairing disabled\n"); } return; } // Method to set the suspend flag. This is needed as input functionality may clash with WiFi, especially Bluetooth. // void HID::suspendInterface(bool suspendIf) { this->suspend = suspendIf; // WAIT FOR MUTEX? } // Method to test to see if the interface has been suspended. // Two modes, one just tests and returns the state, the second waits in a loop until the interface suspends. // bool HID::isSuspended(bool waitForSuspend) { // If flag set, loop waiting for the suspended flag to be set. while(waitForSuspend == true && this->suspended == false) { vTaskDelay(1); } // Return the suspended status. return(this->suspended); } // Method to read data from the underlying input device (keyboard). // uint16_t HID::read(void) { // Locals. // uint16_t result = 0; // Ensure we have exclusive access before reading the input device. if(hidCtrl.mutexInternal != NULL) { // Take controol of the semaphore to block all actions whilst reading key data. Other processes such as keyboard check and validation may // require access hence waiting for exclusive access. if(xSemaphoreTake(hidCtrl.mutexInternal, (TickType_t)100) == pdTRUE) { // Call the device method according to type. // switch(hidCtrl.hidDevice) { case HID_DEVICE_PS2_KEYBOARD: // Get a 16bit code from the keyboard: [15:8] = Control bits, [7:0] = Data bits. if((result = ps2Keyboard->read()) != 0) { hidCtrl.ps2CheckTimer = xTaskGetTickCount(); } break; case HID_DEVICE_BLUETOOTH: case HID_DEVICE_BT_KEYBOARD: // Get a 16bit code from the keyboard: [15:8] = Control bits, [7:0] = Data bits. if((result = btHID->getKey(0)) != 0) { hidCtrl.ps2CheckTimer = xTaskGetTickCount(); } break; // Mouse processing is different, based on callbacks. case HID_DEVICE_TYPE_MOUSE: break; default: break; } // Release mutex, internal or external methods can now access the HID devices. xSemaphoreGive(hidCtrl.mutexInternal); } } // Return 0 if no data available otherwise the key or mouse code. return(result); } // Method to allow update of the mouse resolution. The config is updated and the device configured but the change is not persisted. // void HID::setMouseResolution(enum HID_MOUSE_RESOLUTION resolution) { // Update the resolution in the config. hidConfig.mouse.resolution = resolution; // Set the updated flag to trigger an update. hidCtrl.updated = false; } // Method to allow update of the host side scaling. The config is updated and is actioned realtime. The change is not persisted. // void HID::setMouseHostScaling(enum HID_MOUSE_HOST_SCALING scaling) { // Update the mouse scaling in the config. hidConfig.host.scaling = scaling; } // Method to allow update of the mouse scaling. The config is updated and the device configured but the change is not persisted. // void HID::setMouseScaling(enum HID_MOUSE_SCALING scaling) { // Update the mouse scaling in the config. hidConfig.mouse.scaling = scaling; // Set the updated flag to trigger an update. hidCtrl.updated = false; } // Method to allow update of the mouse sample rate. The config is updated and the device configured but the change is not persisted. // void HID::setMouseSampleRate(enum HID_MOUSE_SAMPLING sampleRate) { // Update the mouse sample rate in the config. hidConfig.mouse.sampleRate = sampleRate; // Set the updated flag to trigger an update. hidCtrl.updated = false; } // Method to detect if the PS2 Mouse is connected and/or re-initialise it. // This method is called on startup to detect if a PS2 device is connected, if it isnt then Bluetooth is started. // If a PS2 mouse is detected on startup then this method is called periodically to check for it being unplugged and reconnected // initialising it as required. // Returns: true - mouse connected, false - not detected. // bool HID::checkPS2Mouse(void) { // Locals. // bool result = false; // Ask the mouse for it's ID. No mouse connected or error = 0xFF. result = ps2Mouse->getDeviceId() == 0xFF ? false : true; // Return current status. return(result); } // Method to check the mouse, if it goes offline then perform reset and configuration once online. Also allow third wheel configuration // of mouse parameters. void HID::processPS2Mouse( void ) { // Locals. // PS/2 mouse check involves periodically requesting the device Id. If no device Id is returned, then reset the mouse until it responds and re-initialise. // // Ensure we have exclusive access before checking mouse. if(xSemaphoreTake(hidCtrl.mutexInternal, (TickType_t)10) == pdTRUE) { // If this mouse has gone offline (ie. unplugged), keep sending the RESET command until it becomes available. if(hidCtrl.active == false) { // Issue a reset, if we dont get an acknowledgement back then the mouse is offline. if(ps2Mouse->reset() == false) { vTaskDelay(100); } else { hidCtrl.active = true; // Set active. hidCtrl.updated = true; // Configure the mouse with latest settings. // As the mouse has been reset, update the Intelli Mouse configuration as a different mouse could have been plugged in. ps2Mouse->checkIntelliMouseExtensions(); // Mouse is online now so reset the check counter. hidCtrl.noValidMouseMessage = 0; } } else { // If the mouse configuration has changed, send the updated values. if(hidCtrl.updated) { hidCtrl.updated = false; ps2Mouse->setResolution((enum PS2Mouse::PS2_RESOLUTION)hidConfig.mouse.resolution); ps2Mouse->setScaling((enum PS2Mouse::PS2_SCALING)hidConfig.mouse.scaling); ps2Mouse->setSampleRate((enum PS2Mouse::PS2_SAMPLING)hidConfig.mouse.sampleRate); } // Read mouse data. This triggers a callback if data is available. ps2Mouse->readData(); // Keep a count of the number of times no valid message is received, reset when a valid message is received. This is used to determine if the mouse has gone // offline. If the counter goes above a threshold then request the mouse ID, if it is not sent, mouse is offline. if(hidCtrl.noValidMouseMessage++ > MAX_MOUSE_INACTIVITY_TIME) { // Check to see if the mouse is online. if(checkPS2Mouse()) { // Mouse is online just not being used. hidCtrl.noValidMouseMessage = 0; } else { hidCtrl.active = false; } } } // Release mutex, external access now possible to the input devices. xSemaphoreGive(hidCtrl.mutexInternal); } // Done! return; } // Callback to process mouse data originating from a PS/2 or Bluetooth mouse. The data is encapsulated in a PS/2 // message and processed into a host message. // void HID::mouseReceiveData(uint8_t src, PS2Mouse::MouseData mouseData) { // Locals. // uint32_t loopTime = (milliSeconds() - hidCtrl.loopTimer)/1000; t_mouseMessageElement mouseMsg; ESP_LOGD(HIDTAG, "Valid:%d, Overrun:%d, Status:%d, X:%d, Y:%d, Wheel:%d", mouseData.valid, mouseData.overrun, mouseData.status, mouseData.position.x, mouseData.position.y, mouseData.wheel); // Check the loop timer and set the blink rate according to the mode which is determined by the range of the loop timer. if((hidCtrl.mouseData.status & 0x04) == 0 && hidCtrl.middleKeyPressed == true && hidCtrl.configMode == HOST_CONFIG_OFF) { // Do nothing, time exceeded to configuration cancelled. if(loopTime >= 4 * hidConfig.params.optionAdvanceDelay) { led->setLEDMode(LED::LED_MODE_ON, LED::LED_DUTY_CYCLE_OFF, 0, 0L, 0L); } else // Approx 2 times the delay setting stored in the config. if(loopTime >= 2 * hidConfig.params.optionAdvanceDelay) { led->setLEDMode(LED::LED_MODE_BLINK, LED::LED_DUTY_CYCLE_30, (uint32_t)(hidConfig.mouse.resolution)+1, 250000L, 1000L); } else // First configuration to be selected in the first optionAdvanceDelay/HID_MOUSE_DATA_POLL_DELAY seconds. if(loopTime >= 1) { led->setLEDMode(LED::LED_MODE_BLINK, LED::LED_DUTY_CYCLE_20, (uint32_t)(hidConfig.host.scaling)+1, 150000L, 1000L); } } // Copy data into control structure as it is needed by the update process above. memcpy((void *)&hidCtrl.mouseData, (void *)&mouseData, sizeof(PS2Mouse::MouseData)); // Process data if valid - normally the case on a callback but could be an overrun occurred invalidating the data. if(hidCtrl.mouseData.valid) { // If configuration mode is enabled then the wheel value is used to increment/decrement an option value. // int16_t wheel = -(((hidCtrl.mouseData.wheel & 0x80) ? 0xFF80 : 0x0000) | (hidCtrl.mouseData.wheel & 0x7F)); if(hidCtrl.configMode != HOST_CONFIG_OFF) { hidCtrl.wheelCnt += wheel; if(hidCtrl.wheelCnt > 4) { if(hidCtrl.configMode == HOST_CONFIG_SCALING) { hidConfig.host.scaling = static_cast(static_cast(hidConfig.host.scaling) + 1); } else if(hidCtrl.configMode == HOST_CONFIG_RESOLUTION) { hidConfig.mouse.resolution = static_cast(static_cast(hidConfig.mouse.resolution) + 1); } } if(hidCtrl.wheelCnt < -4) { if(hidCtrl.configMode == HOST_CONFIG_SCALING) { hidConfig.host.scaling = static_cast(static_cast(hidConfig.host.scaling) - 1); } else if(hidCtrl.configMode == HOST_CONFIG_RESOLUTION) { hidConfig.mouse.resolution = static_cast(static_cast(hidConfig.mouse.resolution) - 1); } } if(hidCtrl.wheelCnt < -4 || hidCtrl.wheelCnt > 4) { if(hidCtrl.configMode == HOST_CONFIG_SCALING) { if(hidConfig.host.scaling > HID::HID_MOUSE_HOST_SCALING_1_5) hidConfig.host.scaling = HID::HID_MOUSE_HOST_SCALING_1_5; if(hidConfig.host.scaling < HID::HID_MOUSE_HOST_SCALING_1_1) hidConfig.host.scaling = HID::HID_MOUSE_HOST_SCALING_1_1; led->setLEDMode(LED::LED_MODE_BLINK, LED::LED_DUTY_CYCLE_20, static_cast(hidConfig.host.scaling)+1, 150000L, 1000L); } else if(hidCtrl.configMode == HOST_CONFIG_RESOLUTION) { if(hidConfig.mouse.resolution > HID::HID_MOUSE_RESOLUTION_1_8) hidConfig.mouse.resolution = HID::HID_MOUSE_RESOLUTION_1_8; if(hidConfig.mouse.resolution < HID::HID_MOUSE_RESOLUTION_1_1) hidConfig.mouse.resolution = HID::HID_MOUSE_RESOLUTION_1_1; led->setLEDMode(LED::LED_MODE_BLINK, LED::LED_DUTY_CYCLE_30, static_cast(hidConfig.mouse.resolution)+1, 250000L, 1000L); hidCtrl.updated = true; } hidCtrl.wheelCnt = 0; } } // If the middle key has been pressed, reset the timer and set the flag. if((hidCtrl.mouseData.status & 0x04) && hidCtrl.middleKeyPressed == false) { hidCtrl.loopTimer = milliSeconds(); hidCtrl.middleKeyPressed = true; led->setLEDMode(LED::LED_MODE_OFF, LED::LED_DUTY_CYCLE_OFF, 0, 0L, 0L); } // When the key has been released the timer can be used to decide on function required. if((hidCtrl.mouseData.status & 0x04) == 0 && hidCtrl.middleKeyPressed == true && loopTime >= 1) { // If the middle button is set we start configuration mode. This entails the wheel value being used to select the scaling required and the LED blink rate indicates // the mode to the user. When the middle button is clicked a second time the configuration is disabled. if(hidCtrl.configMode == HOST_CONFIG_OFF) { if(loopTime >= 1 && loopTime < 2 * hidConfig.params.optionAdvanceDelay) { hidCtrl.configMode = HOST_CONFIG_SCALING; led->setLEDMode(LED::LED_MODE_BLINK, LED::LED_DUTY_CYCLE_20, (uint32_t)(hidConfig.host.scaling)+1, 150000L, 1000L); } else if(loopTime >= 2 * hidConfig.params.optionAdvanceDelay && loopTime < 4 * hidConfig.params.optionAdvanceDelay) { hidCtrl.configMode = HOST_CONFIG_RESOLUTION; led->setLEDMode(LED::LED_MODE_BLINK, LED::LED_DUTY_CYCLE_30, (uint32_t)(hidConfig.mouse.resolution)+1, 250000L, 1000L); } else // If the button is held too long, do nothing, configuration mode cancelled. if(loopTime >= 4 * hidConfig.params.optionAdvanceDelay) { } } else if(hidCtrl.configMode != HOST_CONFIG_OFF) { hidCtrl.configMode = HOST_CONFIG_OFF; // Persist the changes. persistConfig(); // Turn off LED as we have exitted configuration mode. led->setLEDMode(LED::LED_MODE_ON, LED::LED_DUTY_CYCLE_OFF, 0, 0L, 0L); } hidCtrl.loopTimer = milliSeconds(); hidCtrl.middleKeyPressed = false; } // Build the next message with all data, scaled and filtered as necessary. // Firstly, for PS/2 extend the X,Y 9bit movement values into 16bit for easier manipulation. if(src == 0) { mouseMsg.xPos = (((hidCtrl.mouseData.status & 0x10) ? 0xFF00 : 0x0000) | hidCtrl.mouseData.position.x); mouseMsg.yPos = (((hidCtrl.mouseData.status & 0x20) ? 0xFF00 : 0x0000) | hidCtrl.mouseData.position.y); } else { mouseMsg.xPos = hidCtrl.mouseData.position.x / 16; mouseMsg.yPos = hidCtrl.mouseData.position.y / 16; } switch(hidConfig.mouse.resolution) { case HID_MOUSE_RESOLUTION_1_1: mouseMsg.xPos = mouseMsg.xPos / 8; mouseMsg.yPos = mouseMsg.yPos / 8; break; case HID_MOUSE_RESOLUTION_1_2: mouseMsg.xPos = mouseMsg.xPos / 4; mouseMsg.yPos = mouseMsg.yPos / 4; break; case HID_MOUSE_RESOLUTION_1_4: mouseMsg.xPos = mouseMsg.xPos / 2; mouseMsg.yPos = mouseMsg.yPos / 2; break; case HID_MOUSE_RESOLUTION_1_8: default: mouseMsg.xPos = mouseMsg.xPos / 1; mouseMsg.yPos = mouseMsg.yPos / 1; break; } // Perform any in-situ scaling and adjustments. switch(hidConfig.host.scaling) { case HID::HID_MOUSE_HOST_SCALING_1_2: mouseMsg.xPos = mouseMsg.xPos / 2; mouseMsg.yPos = mouseMsg.yPos / 2; break; case HID::HID_MOUSE_HOST_SCALING_1_3: mouseMsg.xPos = mouseMsg.xPos / 3; mouseMsg.yPos = mouseMsg.yPos / 3; break; case HID::HID_MOUSE_HOST_SCALING_1_4: mouseMsg.xPos = mouseMsg.xPos / 4; mouseMsg.yPos = mouseMsg.yPos / 4; break; case HID::HID_MOUSE_HOST_SCALING_1_5: mouseMsg.xPos = mouseMsg.xPos / 5; mouseMsg.yPos = mouseMsg.yPos / 5; break; // No scaling needed for 1:1, the data is clipped at 8bit 2's compliment threshold and overflow/underflow set accordingly. case HID::HID_MOUSE_HOST_SCALING_1_1: default: break; } // Add in status and wheel data to complete message. // mouseMsg.status = hidCtrl.mouseData.status; mouseMsg.wheel = hidCtrl.mouseData.wheel; // If a data callback has been setup, invoke otherwise data is wasted. // if(hidCtrl.dataCallback != NULL) hidCtrl.dataCallback(mouseMsg); // Reset the mouse activity check counter. hidCtrl.noValidMouseMessage = 0; } } // Method to check and process the Bluetooth mouse which operates slightly differently to the PS/2 Mouse. // Data arriving over a BT connection is queued and we read and process it, invoking the application callback for sending the mouse data // to the host. // The Bluetooth HAL is responsible for maintaining a connection and if it goes offline, it will be closed. We detect this and invoke an open // until it comes back online. // void HID::checkBTMouse( void ) { // Locals. // One common function for BT. The protocol manages checks and reconnections but should a device go out of range we initiate // a new scan and connect. btHID->checkBTDevices(); // If the mouse configuration has changed, send the updated values to the BTHID. if(hidCtrl.updated) { hidCtrl.updated = false; btHID->setResolution((enum PS2Mouse::PS2_RESOLUTION)hidConfig.mouse.resolution); btHID->setScaling((enum PS2Mouse::PS2_SCALING)hidConfig.mouse.scaling); btHID->setSampleRate((enum PS2Mouse::PS2_SAMPLING)hidConfig.mouse.sampleRate); } // Done! return; } // Method to detect if the PS2 Keyboard is connected and/or re-initialise it. // This method is called on startup to detect if a PS2 device is connected if it isnt then Bluetooth is started. // If a PS2 keyboard is detected on startup then this method is called periodically to check for it being unplugged and reconnected // initialising it as required. // Returns: true - keyboard connected, false - not detected. // bool HID::checkPS2Keyboard(void) { // Locals. // uint16_t scanCode = 0x0000; // Check to see if the keyboard is still available, no keyboard = no point!! // Firstly, ping keyboard to see if it is there. ps2Keyboard->echo(); vTaskDelay(6); scanCode = ps2Keyboard->read(); // If the keyboard doesnt answer back, then it has been disconnected. if( (scanCode & 0xFF) != PS2_KEY_ECHO && (scanCode & 0xFF) != PS2_KEY_BAT) { hidCtrl.noEchoCount++; // Re-initialise the subsystem, if the keyboard is plugged in then it will be detected on next loop. if(hidCtrl.noEchoCount > 5) ps2Keyboard->begin(CONFIG_PS2_HW_DATAPIN, CONFIG_PS2_HW_CLKPIN); // First entry print out message that the keyboard has disconnected. if(hidCtrl.noEchoCount == 10 && (hidCtrl.ps2Active == 1 || hidCtrl.ps2CheckTimer == 0)) { // Turn on LED when keyboard is detached. led->setLEDMode(LED::LED_MODE_ON, LED::LED_DUTY_CYCLE_OFF, 0, 0L, 0L); ESP_LOGE(HIDTAG, "No PS2 keyboard detected, please connect."); } hidCtrl.ps2Active = 0; hidCtrl.ps2CheckTimer = xTaskGetTickCount(); // Check every second when offline. } else { // First entry after keyboard starts responding, print out message. if(hidCtrl.ps2Active == 0) { ESP_LOGW(HIDTAG, "PS2 keyboard detected and online."); hidCtrl.ps2Active = 1; // If indication was given that the keyboard has gone offline, issue a new message to show it is back online. // This coding is necessary due to KVM devices which can idle the PS/2 connection randomly or when another device such as the mouse is in use. if(hidCtrl.noEchoCount > 10) { ESP_LOGW(HIDTAG, "PS2 keyboard detected and online."); // Flash LED to indicate Keyboard recognised. led->setLEDMode(LED::LED_MODE_BLINK_ONESHOT, LED::LED_DUTY_CYCLE_50, 5, 100000L, 0L); } } hidCtrl.noEchoCount = 0L; hidCtrl.ps2CheckTimer = xTaskGetTickCount(); } // Return current status. return(hidCtrl.ps2Active); } // Method to verify keyboard connectivity. If the keyboard goes offline, once detected, it is re-initialised. // void HID::checkKeyboard( void ) { // Locals. switch(hidCtrl.hidDevice) { case HID_DEVICE_PS2_KEYBOARD: // PS/2 keyboard checks involve sending an echo and reading back the response. If no response is received then the keyboard is unplugged, once an echo returns then // re-initialise the keyboard so it continues to function. // // Check the keyboard is online, this is done at startup and periodically to cater for user disconnect. if((xTaskGetTickCount() - hidCtrl.ps2CheckTimer) > 1000 && (ps2Keyboard->keyAvailable() == 0 || hidCtrl.ps2Active == 0)) { // Ensure we have exclusive access before checking keyboard. if(xSemaphoreTake(hidCtrl.mutexInternal, (TickType_t)10) == pdTRUE) { // Check if the PS/2 keyboard is available. checkPS2Keyboard(); // Release mutex, external access now possible to the input devices. xSemaphoreGive(hidCtrl.mutexInternal); } } break; case HID_DEVICE_BLUETOOTH: case HID_DEVICE_BT_KEYBOARD: // Bluetooth checks involve reconnection on closed handles, ie. when keyboard goes out of range or is switched off, keep retrying to connect. if(hidCtrl.hidDevice == HID_DEVICE_BT_KEYBOARD) { btHID->checkBTDevices(); } break; default: break; } // Done. return; } // Method to verify mouse connectivity and process any pending updates/changes. // void HID::checkMouse(void) { // Locals. switch(hidCtrl.hidDevice) { case HID_DEVICE_PS2_MOUSE: // Process any data and check PS/2 mouse status. processPS2Mouse(); break; case HID_DEVICE_BLUETOOTH: case HID_DEVICE_BT_MOUSE: // Process any pending setting updates and rescan for new device if current device goes out of range. checkBTMouse(); break; default: break; } // Done. return; } // Method to manage and maintain input device connectivity. // IRAM_ATTR void HID::hidControl( void * pvParameters ) { // Locals. // int checkCnt = 0; #define HIDCTRLTAG "hidControl" // Map the instantiating object so we can access its methods and data. HID* pThis = (HID*)pvParameters; // Infinite loop, performing maintenance and control checks. while(true) { // Run through the checks, first keyboard. Assumes PS/2 keyboard or singular Bluetooth keyboard. if(pThis->hidCtrl.deviceType == HID_DEVICE_TYPE_KEYBOARD) { // Check keyboard. pThis->checkKeyboard(); // Relinquish CPU for other tasks. vTaskDelay(100); } // Scan mouse if enabled. Assumes PS/2 mouse or singular Bluetooth mouse. else if(pThis->hidCtrl.deviceType == HID_DEVICE_TYPE_MOUSE) { // Check mouse. pThis->checkMouse(); // Yield if the suspend flag is set. vTaskDelay(HID_MOUSE_DATA_POLL_DELAY); } // Scan bluetooth mouse and keyboard as one or both can be active. else if(pThis->hidCtrl.deviceType == HID_DEVICE_TYPE_BLUETOOTH) { // Check keyboard. Mouse needs more frequent checking so we base delay on mouse period. if(checkCnt-- == 0) { pThis->checkKeyboard(); checkCnt = 100/HID_MOUSE_DATA_POLL_DELAY; } // Check mouse. pThis->checkMouse(); // Yield if the suspend flag is set. vTaskDelay(HID_MOUSE_DATA_POLL_DELAY); } // Check stack space, report if it is getting low. if(uxTaskGetStackHighWaterMark(NULL) < 1024) { ESP_LOGW(HIDCTRLTAG, "THREAD STACK SPACE(%d)\n",uxTaskGetStackHighWaterMark(NULL)); } } return; } // Testing pairing method. The security has been disabled so this method shouldnt be called. // void HID::btPairingHandler(uint32_t pid, uint8_t trigger) { // Trigger indicates which part of the BT/BLE stack triggered the password request. // BT: 1 // BT AUTH: 2 // BLE: 3 // BLE shouldnt require a pin as the stack has been setup to authorise without pin, BT may require a PIN and so a BT AUTH callback will be made. // BT AUTH pid = status, if 0 then successful connection, no pin, if 9 then FAIL, so raise an alert via the LED that a PIN is required for this device. switch(trigger) { case 1: std::cout << "Please enter the following pairing code, " << std::endl << "followed with ENTER on your keyboard: " << pid << std::endl; ESP_LOGE(HIDTAG, "Password request for BT pairing device, normally this should be AUTH, please log details."); break; case 2: if(pid == 0) { pHIDThis->led->setLEDMode(LED::LED_MODE_OFF, LED::LED_DUTY_CYCLE_OFF, 0, 0L, 0L); } else if(pid == 9) { pHIDThis->led->setLEDMode(LED::LED_MODE_BLINK, LED::LED_DUTY_CYCLE_80, 3, 250000L, 1000L); } break; case 3: default: ESP_LOGE(HIDTAG, "Password request for pairing device. Auth disabled so this shouldnt occur, please log details."); break; } } // Method to see if the enabled underlying HID device is Bluetooth. // bool HID::isBluetooth(void) { return(hidCtrl.hidDevice == HID_DEVICE_BT_KEYBOARD || hidCtrl.hidDevice == HID_DEVICE_BT_MOUSE || hidCtrl.hidDevice == HID_DEVICE_BLUETOOTH); } // Method to re-initialise the bluetooth subsystem after being disabled. // At the moment this is a stub because WiFi is used for configuration and once complete a reboot takes place. void HID::enableBluetooth(void) { if(isBluetooth()) { } return; } // Method to disable the bluetooth subsystem. This is necessary if WiFi is required as the two wireless devices share the same // antenna and dont coexist very well. void HID::disableBluetooth(void) { if(isBluetooth()) { // Disable and de-initialse BT and BLE to free up the antenna. // esp_bluedroid_disable(); esp_bluedroid_deinit(); esp_bt_controller_disable(); esp_bt_controller_deinit(); } return; } // Method to persist the current configuration into NVS storage. // bool HID::persistConfig(void) { // Locals bool result = true;; // Update persistence with changed data. if(nvs->persistData(getClassName(__PRETTY_FUNCTION__), &hidConfig, sizeof(t_hidConfig)) == false) { ESP_LOGW(HIDTAG, "Persisting Mouse configuration data failed, updates will not persist in future power cycles."); led->setLEDMode(LED::LED_MODE_BLINK_ONESHOT, LED::LED_DUTY_CYCLE_10, 200, 1000L, 0L); result = false; } // Few other updates so make a commit here to ensure data is flushed and written. else if(nvs->commitData() == false) { ESP_LOGW(HIDTAG, "NVS Commit writes operation failed, some previous writes may not persist in future power cycles."); led->setLEDMode(LED::LED_MODE_BLINK_ONESHOT, LED::LED_DUTY_CYCLE_10, 200, 500L, 0L); result = false; } // Return result, true = success. return(result); } // Base initialisation for generic HID hardware used. void HID::init(const char *className, enum HID_DEVICE_TYPES deviceType) { // Locals #define INITTAG "init" // Initialise variables. hidCtrl.mutexInternal = NULL; hidCtrl.dataCallback = NULL; hidCtrl.configMode = HOST_CONFIG_OFF; hidCtrl.loopTimer = milliSeconds(); // Retrieve configuration, if it doesnt exist, set defaults. // if(nvs->retrieveData(className, &this->hidConfig, sizeof(t_hidConfig)) == false) { ESP_LOGW(INITTAG, "HID configuration set to default, no valid config in NVS found."); hidConfig.mouse.resolution = HID_MOUSE_RESOLUTION_1_8; hidConfig.mouse.scaling = HID_MOUSE_SCALING_1_1; hidConfig.mouse.sampleRate = HID_MOUSE_SAMPLE_RATE_60; hidConfig.host.scaling = HID_MOUSE_HOST_SCALING_1_1; hidConfig.params.optionAdvanceDelay = 1; // Persist the data for next time. if(nvs->persistData(className, &this->hidConfig, sizeof(t_hidConfig)) == false) { ESP_LOGW(INITTAG, "Persisting Default HID configuration data failed, check NVS setup.\n"); } // Commit data, ensuring values are written to NVS and the mutex is released. else if(nvs->commitData() == false) { ESP_LOGW(INITTAG, "NVS Commit writes operation failed, some previous writes may not persist in future power cycles."); } } // Store the class name for later use, ie. NVS key access. this->className = className; // Initially I wanted PS/2 and Bluetooth to work in tandem. They do, sort of, but Bluetooth heavy resource usage and high priority // interferes with the PS/2 interrupt latency and consequently the PS/2 data can become corrupt. Also the Bluetooth stack is not that // stable especially with BLE (Logitech K780 keyboard when it connects will sometimes hang, if it disconnects it may hang reconnecting). Too many issues // so decided to seperate the logic, if a PS/2 device cannot be seen on startup it is disabled and bluetooth enabled. // This setup is for the primary device expected by the host. ie. If the host is detected as an X68000 then logically it needs a keyboard which is true for PS/2 // as you can only have one PS/2 device connected at a time. For Bluetooth though, it is possible to have a keyboard and mouse connected and the X68000 interface // allows for both over one port, so if the host is detected as an X68000 it will check for a PS/2 keyboard, if not found it will enable Bluetooth and then the // logic can connect both a mouse and keyboard and channel them to the X68000. switch(deviceType) { case HID_DEVICE_TYPE_KEYBOARD: { // Instantiate the PS/2 Keyboard object and initialise. ESP_LOGW(INITTAG, "Initialise PS2 keyboard."); ps2Keyboard = new PS2KeyAdvanced(); ps2Keyboard->begin(CONFIG_PS2_HW_DATAPIN, CONFIG_PS2_HW_CLKPIN); // If no PS/2 keyboard detected then default to Bluetooth. if(checkPS2Keyboard() == false) { // Remove the PS/2 keyboard object, free up memory and disable the interrupts. ESP_LOGW(INITTAG, "PS2 keyboard not available."); delete ps2Keyboard; hidCtrl.hidDevice = HID_DEVICE_BT_KEYBOARD; // Instantiate Bluetooth HID object. ESP_LOGW(INITTAG, "Initialise Bluetooth keyboard."); btHID = new BTHID(); btHID->setup(btPairingHandler); sw->setBTPairingEventCallback(&HID::btStartPairing, this); // Setup a mouse callback as it is possible to receive mouse data when the primary input method is a keyboard. This data can be used by a registered // mouse interface to provide dual services to a host. btHID->setMouseDataCallback(&HID::mouseReceiveData, this); hidCtrl.deviceType = HID_DEVICE_TYPE_BLUETOOTH; hidCtrl.hidDevice = HID_DEVICE_BLUETOOTH; } else { hidCtrl.deviceType = HID_DEVICE_TYPE_KEYBOARD; hidCtrl.hidDevice = HID_DEVICE_PS2_KEYBOARD; } break; } case HID_DEVICE_TYPE_MOUSE: { // Instantiate the PS/2 Keyboard object and initialise. ESP_LOGW(INITTAG, "Initialise PS2 Mouse."); ps2Mouse = new PS2Mouse(CONFIG_PS2_HW_CLKPIN, CONFIG_PS2_HW_DATAPIN); ps2Mouse->initialize(); // Test to see if a PS/2 Mouse is connected. IF it isnt, delete the PS/2 Mouse object and initiate a Bluetooth object. if(checkPS2Mouse() == false) { ESP_LOGW(INITTAG, "PS2 Mouse not available."); delete ps2Mouse; hidCtrl.deviceType = HID_DEVICE_TYPE_BLUETOOTH; hidCtrl.hidDevice = HID_DEVICE_BT_MOUSE; // Instantiate Bluetooth HID object. ESP_LOGW(INITTAG, "Initialise Bluetooth mouse."); btHID = new BTHID(); btHID->setup(btPairingHandler); btHID->setMouseDataCallback(&HID::mouseReceiveData, this); sw->setBTPairingEventCallback(&HID::btStartPairing, this); } else { hidCtrl.deviceType = HID_DEVICE_TYPE_MOUSE; hidCtrl.hidDevice = HID_DEVICE_PS2_MOUSE; // Set the mouse to streaming mode so all movements generate data. ps2Mouse->setMouseDataCallback(&HID::mouseReceiveData, this); ps2Mouse->setStreamMode(); ps2Mouse->enableStreaming(); hidCtrl.hidDevice = HID_DEVICE_PS2_MOUSE; } break; } default: break; } // Setup mutex's. hidCtrl.mutexInternal = xSemaphoreCreateMutex(); xSemaphoreGive(hidCtrl.mutexInternal); // Core 0 - Application // HID control thread. ESP_LOGW(HIDTAG, "Starting HID thread..."); ::xTaskCreatePinnedToCore(&this->hidControl, "HID", 4096, this, 0, &this->TaskHID, 0); // All done, no return code! return; } // Constructor, Initialise interface. HID::HID(enum HID_DEVICE_TYPES deviceType, NVS *hdlNVS, LED *hdlLED, SWITCH *hdlSWITCH) { // Check for multiple instantiations, only one instance allowed. if(pHIDThis != nullptr) { // If the constructor to create an object with the underlying hardware is called more than once, flag it and set to a basic object as only one object can access // hardware at a time. ESP_LOGE(HIDTAG, "Constructor called more than once. Only one instance of HID with hardware allowed."); this->nvs = hdlNVS; return; } // Store current object, used in ESP API callbacks (C based). pHIDThis = this; // Save the NVS object so we can persist and retrieve config data. this->nvs = hdlNVS; // Save the LED object so it can be used to warn the user. this->led = hdlLED; // Save the SWITCH object so it can be used to enable Bluetooth pairing. this->sw = hdlSWITCH; init(getClassName(__PRETTY_FUNCTION__), deviceType); } // Basic constructor, no input device defined, just NVS for config retrieval and persistence. HID::HID(NVS *hdlNVS) { // Save the NVS object so we can persist and retrieve config data. this->nvs = hdlNVS; } // Basic constructor, do nothing! Used for probing versions etc. HID::HID(void) { // } // Basic destructor, do nothing! Only ever called for instantiation of uninitialsed class to prove version data.Used for probing versions etc. HID::~HID(void) { // Release object pointer if set. if(pHIDThis == this) { pHIDThis = NULL; } }