Files
mz25key/main/HID.cpp
2022-09-04 17:05:08 +01:00

1019 lines
41 KiB
C++

/////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// 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 <philip.smart@net2net.org>
//
// 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 <http://www.gnu.org/licenses/>.
/////////////////////////////////////////////////////////////////////////////////////////////////////////
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <iostream>
#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<BT::t_scanListItem> 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<HID_MOUSE_HOST_SCALING>(static_cast<int>(hidConfig.host.scaling) + 1);
} else
if(hidCtrl.configMode == HOST_CONFIG_RESOLUTION)
{
hidConfig.mouse.resolution = static_cast<HID::HID_MOUSE_RESOLUTION>(static_cast<int>(hidConfig.mouse.resolution) + 1);
}
}
if(hidCtrl.wheelCnt < -4)
{
if(hidCtrl.configMode == HOST_CONFIG_SCALING)
{
hidConfig.host.scaling = static_cast<HID_MOUSE_HOST_SCALING>(static_cast<int>(hidConfig.host.scaling) - 1);
} else
if(hidCtrl.configMode == HOST_CONFIG_RESOLUTION)
{
hidConfig.mouse.resolution = static_cast<HID::HID_MOUSE_RESOLUTION>(static_cast<int>(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<int>(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<int>(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;
}
}