From 96a69cc86df9f38a204b09e3fbbf82c69484048d Mon Sep 17 00:00:00 2001 From: Philip Smart Date: Sun, 4 Sep 2022 14:51:38 +0100 Subject: [PATCH] Added source code, MZ6500 module still not complete --- main/BT.cpp | 789 ++++- main/BTHID.cpp | 1012 +++++- main/CMakeLists.txt | 5 +- main/HID.cpp | 1019 +++++- main/Kconfig.projbuild | 310 +- main/KeyInterface.cpp | 212 +- main/LED.cpp | 342 +- main/MZ2528.cpp | 1245 ++++++- main/MZ5665.cpp | 1053 +++++- main/Mouse.cpp | 730 ++++- main/NVS.cpp | 294 +- main/PC9801.cpp | 1055 +++++- main/PS2KeyAdvanced.cpp | 1114 ++++++- main/PS2Mouse.cpp | 683 +++- main/SWITCH.cpp | 217 +- main/SharpKey.cpp | 1075 +++++- main/WiFi.cpp | 2873 ++++++++++++++++- main/X1.cpp | 1125 ++++++- main/X68K.cpp | 1068 +++++- main/component.mk | 9 +- main/include/BT.h | 223 +- main/include/BTHID.h | 702 +++- main/include/HID.h | 386 ++- main/include/KeyInterface.h | 204 +- main/include/LED.h | 182 +- main/include/MZ2528.h | 535 ++- main/include/MZ5665.h | 575 +++- main/include/Mouse.h | 152 +- main/include/NVS.h | 163 +- main/include/PC9801.h | 530 ++- main/include/PS2KeyAdvanced.h | 452 ++- main/include/PS2KeyCode.h | 277 +- main/include/PS2KeyTable.h | 394 ++- main/include/PS2Mouse.h | 188 +- main/include/SWITCH.h | 233 +- main/include/WiFi.h | 401 ++- main/include/X1.h | 688 +++- main/include/X68K.h | 534 ++- sdkconfig | 1685 +++++++++- version.txt | 2 +- webserver/css/bootstrap.min.css.gz | Bin 52 -> 18136 bytes webserver/css/css | 1 - webserver/css/jquery.edittable.min.css | 19 +- webserver/css/sb-admin.css | 160 +- webserver/css/sharpkey.css | 267 +- webserver/css/style.css | 254 +- webserver/css/styles.css | 2 +- webserver/favicon.ico | Bin 36 -> 4286 bytes webserver/font-awesome/css/css | 1 - webserver/font-awesome/css/font-awesome.css | 2338 +++++++++++++- .../font-awesome/css/font-awesome.min.css | 5 +- .../css/font-awesome.min.css.orig | 5 +- webserver/font-awesome/font-awesome | 1 - .../fonts/fontawesome-webfont.woff | Bin 74 -> 98024 bytes webserver/font-awesome/fonts/fonts | 1 - webserver/images/images | 1 - webserver/index.html | 167 +- webserver/js/140medley.min.js | 3 +- webserver/js/bootstrap.min.js.gz | Bin 50 -> 10921 bytes webserver/js/index.js | 67 +- webserver/js/jquery.edittable.js | 341 +- webserver/js/jquery.edittable.min.js | 3 +- webserver/js/jquery.min.js.gz | Bin 47 -> 33465 bytes webserver/js/js | 1 - webserver/js/keymap.js | 654 +++- webserver/js/mouse.js | 142 +- webserver/js/ota.js | 301 +- webserver/js/wifimanager.js | 179 +- webserver/keymap.html | 240 +- webserver/mouse.html | 168 +- webserver/ota.html | 239 +- webserver/ptable.bin | 2 +- webserver/version.txt | 2 +- webserver/wifimanager.html | 313 +- 74 files changed, 30544 insertions(+), 69 deletions(-) mode change 120000 => 100644 main/BT.cpp mode change 120000 => 100644 main/BTHID.cpp mode change 120000 => 100644 main/CMakeLists.txt mode change 120000 => 100644 main/HID.cpp mode change 120000 => 100644 main/Kconfig.projbuild mode change 120000 => 100644 main/KeyInterface.cpp mode change 120000 => 100644 main/LED.cpp mode change 120000 => 100644 main/MZ2528.cpp mode change 120000 => 100644 main/MZ5665.cpp mode change 120000 => 100644 main/Mouse.cpp mode change 120000 => 100644 main/NVS.cpp mode change 120000 => 100644 main/PC9801.cpp mode change 120000 => 100644 main/PS2KeyAdvanced.cpp mode change 120000 => 100644 main/PS2Mouse.cpp mode change 120000 => 100644 main/SWITCH.cpp mode change 120000 => 100644 main/SharpKey.cpp mode change 120000 => 100644 main/WiFi.cpp mode change 120000 => 100644 main/X1.cpp mode change 120000 => 100644 main/X68K.cpp mode change 120000 => 100644 main/component.mk mode change 120000 => 100644 main/include/BT.h mode change 120000 => 100644 main/include/BTHID.h mode change 120000 => 100644 main/include/HID.h mode change 120000 => 100644 main/include/KeyInterface.h mode change 120000 => 100644 main/include/LED.h mode change 120000 => 100644 main/include/MZ2528.h mode change 120000 => 100644 main/include/MZ5665.h mode change 120000 => 100644 main/include/Mouse.h mode change 120000 => 100644 main/include/NVS.h mode change 120000 => 100644 main/include/PC9801.h mode change 120000 => 100644 main/include/PS2KeyAdvanced.h mode change 120000 => 100644 main/include/PS2KeyCode.h mode change 120000 => 100644 main/include/PS2KeyTable.h mode change 120000 => 100644 main/include/PS2Mouse.h mode change 120000 => 100644 main/include/SWITCH.h mode change 120000 => 100644 main/include/WiFi.h mode change 120000 => 100644 main/include/X1.h mode change 120000 => 100644 main/include/X68K.h mode change 120000 => 100644 sdkconfig mode change 120000 => 100644 webserver/css/bootstrap.min.css.gz delete mode 120000 webserver/css/css mode change 120000 => 100644 webserver/css/jquery.edittable.min.css mode change 120000 => 100644 webserver/css/sb-admin.css mode change 120000 => 100644 webserver/css/sharpkey.css mode change 120000 => 100644 webserver/css/style.css mode change 120000 => 100644 webserver/css/styles.css mode change 120000 => 100644 webserver/favicon.ico delete mode 120000 webserver/font-awesome/css/css mode change 120000 => 100644 webserver/font-awesome/css/font-awesome.css mode change 120000 => 100644 webserver/font-awesome/css/font-awesome.min.css mode change 120000 => 100644 webserver/font-awesome/css/font-awesome.min.css.orig delete mode 120000 webserver/font-awesome/font-awesome mode change 120000 => 100644 webserver/font-awesome/fonts/fontawesome-webfont.woff delete mode 120000 webserver/font-awesome/fonts/fonts delete mode 120000 webserver/images/images mode change 120000 => 100644 webserver/index.html mode change 120000 => 100644 webserver/js/140medley.min.js mode change 120000 => 100644 webserver/js/bootstrap.min.js.gz mode change 120000 => 100644 webserver/js/index.js mode change 120000 => 100644 webserver/js/jquery.edittable.js mode change 120000 => 100644 webserver/js/jquery.edittable.min.js mode change 120000 => 100644 webserver/js/jquery.min.js.gz delete mode 120000 webserver/js/js mode change 120000 => 100644 webserver/js/keymap.js mode change 120000 => 100644 webserver/js/mouse.js mode change 120000 => 100644 webserver/js/ota.js mode change 120000 => 100644 webserver/js/wifimanager.js mode change 120000 => 100644 webserver/keymap.html mode change 120000 => 100644 webserver/mouse.html mode change 120000 => 100644 webserver/ota.html mode change 120000 => 100644 webserver/ptable.bin mode change 120000 => 100644 webserver/version.txt mode change 120000 => 100644 webserver/wifimanager.html diff --git a/main/BT.cpp b/main/BT.cpp deleted file mode 120000 index 9e5c96c..0000000 --- a/main/BT.cpp +++ /dev/null @@ -1 +0,0 @@ -../../sharpkey/main/BT.cpp \ No newline at end of file diff --git a/main/BT.cpp b/main/BT.cpp new file mode 100644 index 0000000..a611254 --- /dev/null +++ b/main/BT.cpp @@ -0,0 +1,788 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// Name: BT.cpp +// Created: Mar 2022 +// Version: v1.0 +// Author(s): Philip Smart +// Description: Bluetooth base class. +// This source file contains the class to encapsulate the Bluetooth ESP API. Both +// BLE and BT Classic are supported. Allows for scanning, pairing and connection +// to a peripheral device such as a Keyboard or Mouse. +// +// The application uses the Espressif Development environment with Arduino components. +// This is necessary as the class uses the Arduino methods for GPIO manipulation. I +// was considering using pure Espressif IDF methods but considered the potential +// of also using this class on an Arduino project. +// +// Credits: +// Copyright: (c) 2022 Philip Smart +// +// History: Mar 2022 - Initial write. +// +// 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 "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_log.h" +#include "esp_bt.h" +#include "esp_bt_main.h" +#include "esp_gap_bt_api.h" +#include "esp_bt_device.h" +#include "esp_spp_api.h" +#include "Arduino.h" +#include "driver/gpio.h" +#include "soc/timer_group_struct.h" +#include "soc/timer_group_reg.h" +#include "BT.h" + +#define SIZEOF_ARRAY(a) (sizeof(a) / sizeof(*a)) + +// Out of object pointer to a singleton class for use in the ESP IDF API callback routines which werent written for C++. Other methods can be used but this one is the simplest +// to understand and the class can only ever be singleton. +BT *pBTThis = NULL; + +// Method to locate a valid scan entry in the results list. +// +BT::t_scanListItem* BT::findValidScannedDevice(esp_bd_addr_t bda, std::vector &scanList) +{ + // Locals. + // + + // Loop through the scan results list looking for a valid entry, return entry if found. + for(std::size_t idx = 0; idx < scanList.size(); idx++) + { + if (memcmp(bda, scanList[idx].bda, sizeof(esp_bd_addr_t)) == 0) + { + return &scanList[idx]; + } + } + return(nullptr); +} + +#ifdef CONFIG_CLASSIC_BT_ENABLED +// Method to add a valid BT Classic device onto the scan list. +// +void BT::addBTScanDevice(esp_bd_addr_t bda, esp_bt_cod_t *cod, esp_bt_uuid_t *uuid, uint8_t *name, uint8_t name_len, int rssi) +{ + // Locals. + t_scanListItem item; + + // Find a valid device in the BT Classic scan results. If a device is found then this callback is with new data. + t_scanListItem* result = findValidScannedDevice(bda, btCtrl.btScanList); + if(result) + { + // Information can be updated through several calls. + if(result->name.length() == 0 && name && name_len) + { + result->name.assign((char *)name, name_len); + } + if(result->bt.uuid.len == 0 && uuid->len) + { + memcpy(&result->bt.uuid, uuid, sizeof(esp_bt_uuid_t)); + } + if(rssi != 0) + { + result->rssi = rssi; + } + return; + } + + // Populate new list item with device results. + item.transport = ESP_HID_TRANSPORT_BT; + memcpy(item.bda, bda, sizeof(esp_bd_addr_t)); + memcpy(&item.bt.cod, cod, sizeof(esp_bt_cod_t)); + memcpy(&item.bt.uuid, uuid, sizeof(esp_bt_uuid_t)); + item.usage = esp_hid_usage_from_cod((uint32_t)cod); + item.rssi = rssi; + item.name = ""; + + // Store device name if present. This is possibly provided in a seperate callback. + if(name_len && name) + { + item.name.assign((char *)name, name_len); + } + + // Add new item onto list. + btCtrl.btScanList.push_back(item); + return; +} +#endif + +// Method to add a valid BLE device to our scan list. +// +void BT::addBLEScanDevice(esp_bd_addr_t bda, esp_ble_addr_type_t addr_type, uint16_t appearance, uint8_t *name, uint8_t name_len, int rssi) +{ + // Locals. + // + t_scanListItem item; + + // See if the device is already in the list, exit if found as data updates with seperate callbacks not normal under BLE. + if(findValidScannedDevice(bda, btCtrl.bleScanList)) + { + ESP_LOGW(TAG, "Result already exists!"); + return; + } + + // Populate the item with data. + item.transport = ESP_HID_TRANSPORT_BLE; + memcpy(item.bda, bda, sizeof(esp_bd_addr_t)); + item.ble.appearance = appearance; + item.ble.addr_type = addr_type; + item.usage = esp_hid_usage_from_appearance(appearance); + item.rssi = rssi; + item.name = ""; + + // Store device name if present. + if(name_len && name) + { + item.name.assign((char *)name, name_len); + } + + // Add new item onto list. + btCtrl.bleScanList.push_back(item); + return; +} + +#ifdef CONFIG_CLASSIC_BT_ENABLED +// Method to process a device data resulting from a BT scan. +// +void BT::processBTDeviceScanResult(esp_bt_gap_cb_param_t * param) +{ + // Locals + // + uint32_t codv = 0; + esp_bt_cod_t *cod = (esp_bt_cod_t *)&codv; + int8_t rssi = 0; + uint8_t *name = nullptr; + uint8_t name_len = 0; + esp_bt_uuid_t uuid; + uint8_t len = 0; + uint8_t *data = 0; + + uuid.len = ESP_UUID_LEN_16; + uuid.uuid.uuid16 = 0; + + for (int i = 0; i < param->disc_res.num_prop; i++) + { + esp_bt_gap_dev_prop_t * prop = ¶m->disc_res.prop[i]; + if(prop->type != ESP_BT_GAP_DEV_PROP_EIR) + { + } + if(prop->type == ESP_BT_GAP_DEV_PROP_BDNAME) + { + name = (uint8_t *) prop->val; + name_len = strlen((const char *)name); + } + else if(prop->type == ESP_BT_GAP_DEV_PROP_RSSI) + { + rssi = *((int8_t *) prop->val); + } + else if(prop->type == ESP_BT_GAP_DEV_PROP_COD) + { + memcpy(&codv, prop->val, sizeof(uint32_t)); + } + else if(prop->type == ESP_BT_GAP_DEV_PROP_EIR) + { + data = esp_bt_gap_resolve_eir_data((uint8_t *) prop->val, ESP_BT_EIR_TYPE_CMPL_16BITS_UUID, &len); + + if(data == nullptr) + { + data = esp_bt_gap_resolve_eir_data((uint8_t *) prop->val, ESP_BT_EIR_TYPE_INCMPL_16BITS_UUID, &len); + } + + if(data && len == ESP_UUID_LEN_16) + { + uuid.len = ESP_UUID_LEN_16; + uuid.uuid.uuid16 = data[0] + (data[1] << 8); + continue; + } + + data = esp_bt_gap_resolve_eir_data((uint8_t *) prop->val, ESP_BT_EIR_TYPE_CMPL_32BITS_UUID, &len); + + if(data == nullptr) + { + data = esp_bt_gap_resolve_eir_data((uint8_t *) prop->val, ESP_BT_EIR_TYPE_INCMPL_32BITS_UUID, &len); + } + + if(data && len == ESP_UUID_LEN_32) + { + uuid.len = len; + memcpy(&uuid.uuid.uuid32, data, sizeof(uint32_t)); + continue; + } + + data = esp_bt_gap_resolve_eir_data((uint8_t *) prop->val, ESP_BT_EIR_TYPE_CMPL_128BITS_UUID, &len); + + if(data == nullptr) + { + data = esp_bt_gap_resolve_eir_data((uint8_t *) prop->val, ESP_BT_EIR_TYPE_INCMPL_128BITS_UUID, &len); + } + + if(data && len == ESP_UUID_LEN_128) + { + uuid.len = len; + memcpy(uuid.uuid.uuid128, (uint8_t *)data, len); + continue; + } + + //try to find a name + if (name == nullptr) + { + data = esp_bt_gap_resolve_eir_data((uint8_t *) prop->val, ESP_BT_EIR_TYPE_CMPL_LOCAL_NAME, &len); + + if (data == nullptr) + { + data = esp_bt_gap_resolve_eir_data((uint8_t *) prop->val, ESP_BT_EIR_TYPE_SHORT_LOCAL_NAME, &len); + } + + if (data && len) + { + name = data; + name_len = len; + } + } + } + } + + // If the found device is a peripheral or a second call on an existing device, add/update the device. + if ((cod->major == ESP_BT_COD_MAJOR_DEV_PERIPHERAL) || (findValidScannedDevice(param->disc_res.bda, btCtrl.btScanList) != nullptr)) + { + addBTScanDevice(param->disc_res.bda, cod, &uuid, name, name_len, rssi); + } +} +#endif + +#ifdef CONFIG_CLASSIC_BT_ENABLED +// BT GAP Event Handler. +// +void BT::processBTGapEvent(esp_bt_gap_cb_event_t event, esp_bt_gap_cb_param_t *param) +{ + // Locals. + // + + switch(event) + { + case ESP_BT_GAP_DISC_STATE_CHANGED_EVT: + { + ESP_LOGI(TAG, "BT GAP DISC_STATE %s", (param->disc_st_chg.state == ESP_BT_GAP_DISCOVERY_STARTED) ? "START" : "STOP"); + if (param->disc_st_chg.state == ESP_BT_GAP_DISCOVERY_STOPPED) + { + // Release semaphore on which the initiator is waiting, this signals processing complete and results ready. + xSemaphoreGive(pBTThis->btCtrl.bt_hidh_cb_semaphore); + } + break; + } + case ESP_BT_GAP_DISC_RES_EVT: + { + pBTThis->processBTDeviceScanResult(param); + break; + } + case ESP_BT_GAP_KEY_NOTIF_EVT: + ESP_LOGI(TAG, "BT GAP KEY_NOTIF passkey:%d", param->key_notif.passkey); + if(pBTThis->btCtrl.pairingHandler != nullptr) (*pBTThis->btCtrl.pairingHandler)(param->key_notif.passkey, 1); + break; + case ESP_BT_GAP_MODE_CHG_EVT: + ESP_LOGI(TAG, "BT GAP MODE_CHG_EVT mode:%d", param->mode_chg.mode); + break; + case ESP_BT_GAP_AUTH_CMPL_EVT: + ESP_LOGI(TAG, "BT GAP MODE AUTH_CMPL:%s (%d)", param->auth_cmpl.device_name, param->auth_cmpl.stat); + if(pBTThis->btCtrl.pairingHandler != nullptr) (*pBTThis->btCtrl.pairingHandler)((uint32_t)param->auth_cmpl.stat, 2); + break; + default: + ESP_LOGI(TAG, "BT GAP EVENT %s", pBTThis->bt_gap_evt_str(event)); + break; + } +} +#endif + +// Method to process a device data resulting from a BLE scan. +// +void BT::processBLEDeviceScanResult(esp_ble_gap_cb_param_t *param) +{ + // Locals. + // + uint16_t uuid = 0; + uint16_t appearance = 0; + char name[64] = ""; + uint8_t uuid_len = 0; + uint8_t *uuid_d = esp_ble_resolve_adv_data(param->scan_rst.ble_adv, ESP_BLE_AD_TYPE_16SRV_CMPL, &uuid_len); + uint8_t appearance_len = 0; + uint8_t *appearance_d = esp_ble_resolve_adv_data(param->scan_rst.ble_adv, ESP_BLE_AD_TYPE_APPEARANCE, &appearance_len); + uint8_t adv_name_len = 0; + uint8_t *adv_name = esp_ble_resolve_adv_data(param->scan_rst.ble_adv, ESP_BLE_AD_TYPE_NAME_CMPL, &adv_name_len); + + if (uuid_d != nullptr && uuid_len) + { + uuid = uuid_d[0] + (uuid_d[1] << 8); + } + + if (appearance_d != nullptr && appearance_len) + { + appearance = appearance_d[0] + (appearance_d[1] << 8); + } + + if (adv_name == nullptr) + { + adv_name = esp_ble_resolve_adv_data(param->scan_rst.ble_adv, ESP_BLE_AD_TYPE_NAME_SHORT, &adv_name_len); + } + + if (adv_name != nullptr && adv_name_len) + { + memcpy(name, adv_name, adv_name_len); + name[adv_name_len] = 0; + } + + if (uuid == ESP_GATT_UUID_HID_SVC) + { + addBLEScanDevice(param->scan_rst.bda, + param->scan_rst.ble_addr_type, + appearance, adv_name, adv_name_len, + param->scan_rst.rssi); + } +} + +// BLE GAP Event Handler. +// +void BT::processBLEGapEvent(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t * param) +{ + switch(event) + { + // SCAN + case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: + { + ESP_LOGI(TAG, "BLE GAP EVENT SCAN_PARAM_SET_COMPLETE"); + + // Release semaphore, this releases the caller who initiated the scan as we are now complete. + xSemaphoreGive(pBTThis->btCtrl.ble_hidh_cb_semaphore); + break; + } + case ESP_GAP_BLE_SCAN_RESULT_EVT: + { + switch (param->scan_rst.search_evt) + { + case ESP_GAP_SEARCH_INQ_RES_EVT: + { + pBTThis->processBLEDeviceScanResult(param); + break; + } + case ESP_GAP_SEARCH_INQ_CMPL_EVT: + ESP_LOGI(TAG, "BLE GAP EVENT SCAN DONE: %d", param->scan_rst.num_resps); + + // Release semaphore, this releases the caller who initiated the scan as we are now complete. + xSemaphoreGive(pBTThis->btCtrl.ble_hidh_cb_semaphore); + break; + default: + break; + } + break; + } + case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: + { + ESP_LOGI(TAG, "BLE GAP EVENT SCAN CANCELED"); + break; + } + + // ADVERTISEMENT + case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT: + ESP_LOGI(TAG, "BLE GAP ADV_DATA_SET_COMPLETE"); + break; + + case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: + ESP_LOGI(TAG, "BLE GAP ADV_START_COMPLETE"); + break; + + // AUTHENTICATION + case ESP_GAP_BLE_AUTH_CMPL_EVT: + if (!param->ble_security.auth_cmpl.success) + { + ESP_LOGE(TAG, "BLE GAP AUTH ERROR: 0x%x", param->ble_security.auth_cmpl.fail_reason); + } + else + { + ESP_LOGI(TAG, "BLE GAP AUTH SUCCESS"); + } + break; + + case ESP_GAP_BLE_KEY_EVT: //shows the ble key info share with peer device to the user. + ESP_LOGI(TAG, "BLE GAP KEY type = %s", pBTThis->ble_key_type_str(param->ble_security.ble_key.key_type)); + break; + + case ESP_GAP_BLE_PASSKEY_NOTIF_EVT: // ESP_IO_CAP_OUT + // The app will receive this evt when the IO has Output capability and the peer device IO has Input capability. + // Show the passkey number to the user to input it in the peer device. + ESP_LOGI(TAG, "BLE GAP PASSKEY_NOTIF passkey:%d", param->ble_security.key_notif.passkey); + if(pBTThis->btCtrl.pairingHandler != nullptr) (*pBTThis->btCtrl.pairingHandler)(param->ble_security.key_notif.passkey, 3); + break; + + case ESP_GAP_BLE_NC_REQ_EVT: // ESP_IO_CAP_IO + // The app will receive this event when the IO has DisplayYesNO capability and the peer device IO also has DisplayYesNo capability. + // show the passkey number to the user to confirm it with the number displayed by peer device. + ESP_LOGI(TAG, "BLE GAP NC_REQ passkey:%d", param->ble_security.key_notif.passkey); + esp_ble_confirm_reply(param->ble_security.key_notif.bd_addr, true); + break; + + case ESP_GAP_BLE_PASSKEY_REQ_EVT: // ESP_IO_CAP_IN + // The app will receive this evt when the IO has Input capability and the peer device IO has Output capability. + // See the passkey number on the peer device and send it back. + ESP_LOGI(TAG, "BLE GAP PASSKEY_REQ"); + //esp_ble_passkey_reply(param->ble_security.ble_req.bd_addr, true, 1234); + break; + + case ESP_GAP_BLE_SEC_REQ_EVT: + ESP_LOGI(TAG, "BLE GAP SEC_REQ"); + // Send the positive(true) security response to the peer device to accept the security request. + // If not accept the security request, should send the security response with negative(false) accept value. + esp_ble_gap_security_rsp(param->ble_security.ble_req.bd_addr, true); + break; + + case ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT: + ESP_LOGI(TAG, "update connection params status = %d, min_int = %d, max_int = %d,conn_int = %d,latency = %d, timeout = %d", + param->update_conn_params.status, + param->update_conn_params.min_int, + param->update_conn_params.max_int, + param->update_conn_params.conn_int, + param->update_conn_params.latency, + param->update_conn_params.timeout); + break; + + default: + ESP_LOGI(TAG, "BLE GAP EVENT %s", pBTThis->ble_gap_evt_str(event)); + break; + } +} + +#ifdef CONFIG_CLASSIC_BT_ENABLED +// Method to scan for BT Classic devices. +// +esp_err_t BT::scanForBTDevices(uint32_t timeout) +{ + // Locals. + // + esp_err_t result = ESP_OK; + + // Start BT GAP Discovery, wait for 'timeout' seconds for a valid result. + if((result = esp_bt_gap_start_discovery(ESP_BT_INQ_MODE_GENERAL_INQUIRY, (int)(timeout / 1.28), 0)) != ESP_OK) + { + ESP_LOGE(TAG, "esp_bt_gap_start_discovery failed: %d", result); + } + return(result); +} +#endif + +// Method to scan for BLE Devices. +// +esp_err_t BT::scanForBLEDevices(uint32_t timeout) +{ + // Locals. + // + esp_err_t result = ESP_OK; + // Setup BLE scan parameters structure, defined in ESP IDF documentation. + static esp_ble_scan_params_t hid_scan_params = { + .scan_type = BLE_SCAN_TYPE_ACTIVE, + .own_addr_type = BLE_ADDR_TYPE_PUBLIC, + .scan_filter_policy = BLE_SCAN_FILTER_ALLOW_ALL, + .scan_interval = 0x50, + .scan_window = 0x30, + .scan_duplicate = BLE_SCAN_DUPLICATE_ENABLE, + }; + + // Set scan parameters using populated structure. + if((result = esp_ble_gap_set_scan_params(&hid_scan_params)) != ESP_OK) + { + ESP_LOGE(TAG, "esp_ble_gap_set_scan_params failed: %d", result); + return(result); + } + + // Wait for result, this is done by taking possession of a semaphore which is released in the callback when scan complete. + xSemaphoreTake(btCtrl.ble_hidh_cb_semaphore, portMAX_DELAY); + + if((result = esp_ble_gap_start_scanning(timeout)) != ESP_OK) + { + ESP_LOGE(TAG, "esp_ble_gap_start_scanning failed: %d", result); + return(result); + } + return(result); +} + +// Method to scan for Bluetooth devices. +// +esp_err_t BT::scanForAllDevices(uint32_t timeout, size_t *noDevices, std::vector &scanList) +{ + // Locals. + // + + // Clear previous lists. + #ifdef CONFIG_CLASSIC_BT_ENABLED + btCtrl.btScanList.clear(); + #endif + btCtrl.bleScanList.clear(); + + // Scan for BLE devices. + if(scanForBLEDevices(timeout) == ESP_OK) + { + // Wait for result, this is done by taking possession of a semaphore which is released in the callback when scan complete. + xSemaphoreTake(btCtrl.ble_hidh_cb_semaphore, portMAX_DELAY); + } + else + { + return(ESP_FAIL); + } + + #ifdef CONFIG_CLASSIC_BT_ENABLED + // Scan for BT devices + if(scanForBTDevices(timeout) == ESP_OK) + { + // Wait for result, this is done by taking possession of a semaphore which is released in the callback when scan complete. + xSemaphoreTake(btCtrl.bt_hidh_cb_semaphore, portMAX_DELAY); + } + else + { + return(ESP_FAIL); + } + #endif + + //esp_bt_gap_cancel_discovery(); + //esp_ble_gap_stop_scanning(); + + // Process results into a merged list. + #ifdef CONFIG_CLASSIC_BT_ENABLED + for(std::size_t idx = 0; idx < btCtrl.btScanList.size(); idx++) + { + scanList.push_back(btCtrl.btScanList[idx]); + } + #endif + for(std::size_t idx = 0; idx < btCtrl.bleScanList.size(); idx++) + { + scanList.push_back(btCtrl.bleScanList[idx]); + } + + // Update the final list with display values. + for(std::size_t idx = 0; idx < scanList.size(); idx++) + { + char buf[50]; + sprintf(buf, ESP_BD_ADDR_STR, ESP_BD_ADDR_HEX(scanList[idx].bda)); + scanList[idx].deviceAddr = buf; + if(scanList[idx].transport == ESP_HID_TRANSPORT_BLE) + { + scanList[idx].deviceType = "BLE"; + } + #ifdef CONFIG_CLASSIC_BT_ENABLED + if(scanList[idx].transport == ESP_HID_TRANSPORT_BT) + { + scanList[idx].deviceType = "BT"; + } + #endif + } + + // Save number of entries. + *noDevices = scanList.size(); + + // Clear BT/BLE lists as data no longer needed. + #ifdef CONFIG_CLASSIC_BT_ENABLED + btCtrl.btScanList.clear(); + #endif + btCtrl.bleScanList.clear(); + + return(ESP_OK); +} + +// Method to scan and build a list for all available devices. +void BT::getDeviceList(std::vector &scanList, int waitTime) +{ + // Locals. + // + size_t devicesFound = 0; + + ESP_LOGD(TAG, "SCAN..."); + + // Clear previous entries. + scanList.clear(); + + // Start scan for HID devices + scanForAllDevices(waitTime, &devicesFound, scanList); + + ESP_LOGD(TAG, "SCAN: %u results", devicesFound); +} + +// Method to configure Bluetooth and register required callbacks. +bool BT::setup(t_pairingHandler *handler) +{ + // Locals. + // + esp_err_t result; + const esp_bt_mode_t mode = HIDH_BTDM_MODE; + uint8_t key_size = 16; + uint8_t init_key = ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK; + uint8_t rsp_key = ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK; + uint32_t passkey = 123456; + uint8_t auth_option = ESP_BLE_ONLY_ACCEPT_SPECIFIED_AUTH_DISABLE; + uint8_t oob_support = ESP_BLE_OOB_DISABLE; + + // Check for multiple instantiations, only one instance allowed. + if(pBTThis != nullptr) + { + ESP_LOGE(TAG, "Setup called more than once. Only one instance of BT is allowed."); + return false; + } + + // Store current object and handlers. + pBTThis = this; + btCtrl.pairingHandler = handler; + + // Bluetooth not enabled, exit. + if(mode == HIDH_IDLE_MODE) + { + ESP_LOGE(TAG, "Please turn on BT HID host or BLE!"); + return false; + } + + #ifdef CONFIG_CLASSIC_BT_ENABLED + // Create BT Classic semaphore, used to halt caller whilst underlying receives and proceses data. + btCtrl.bt_hidh_cb_semaphore = xSemaphoreCreateBinary(); + if (btCtrl.bt_hidh_cb_semaphore == nullptr) + { + ESP_LOGE(TAG, "xSemaphoreCreateMutex BT failed!"); + return false; + } + #endif + + // Create BLE semaphore, used to halt caller whilst underlying receives and proceses data. + btCtrl.ble_hidh_cb_semaphore = xSemaphoreCreateBinary(); + if(btCtrl.ble_hidh_cb_semaphore == nullptr) + { + ESP_LOGE(TAG, "xSemaphoreCreateMutex BLE failed!"); + + #ifdef CONFIG_CLASSIC_BT_ENABLED + // Delete BT semaphore as both BT and BLE need to be active, return fail to caller. + vSemaphoreDelete(btCtrl.bt_hidh_cb_semaphore); + btCtrl.bt_hidh_cb_semaphore = nullptr; + #endif + return false; + } + + #ifdef CONFIG_CLASSIC_BT_ENABLED + // Setup default config for BT Classic. + esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT(); + bt_cfg.mode = mode; + bt_cfg.bt_max_acl_conn = 3; + bt_cfg.bt_max_sync_conn = 3; + + // Configure Bluetooth controller for BT Classic operation. + if((result = esp_bt_controller_init(&bt_cfg))) + { + ESP_LOGE(TAG, "esp_bt_controller_init failed: %d", result); + return false; + } + + // Enable Bluetooth Classic mode. + if((result = esp_bt_controller_enable(mode))) + { + ESP_LOGE(TAG, "esp_bt_controller_enable failed: %d", result); + return false; + } + esp_bredr_tx_power_set(ESP_PWR_LVL_P9, ESP_PWR_LVL_P9); + #endif + + // Setup and initialise Bluetooth BLE mode. + if((result = esp_bluedroid_init())) + { + ESP_LOGE(TAG, "esp_bluedroid_init failed: %d", result); + return false; + } + if((result = esp_bluedroid_enable())) + { + ESP_LOGE(TAG, "esp_bluedroid_enable failed: %d", result); + return false; + } + esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_DEFAULT, ESP_PWR_LVL_P9); + + #ifdef CONFIG_CLASSIC_BT_ENABLED + // Classic Bluetooth GAP + esp_bt_sp_param_t param_type = ESP_BT_SP_IOCAP_MODE; + esp_bt_io_cap_t iocap = ESP_BT_IO_CAP_IO; + esp_bt_gap_set_security_param(param_type, &iocap, sizeof(uint8_t)); + + // Set default parameters for Legacy Pairing + // Use fixed pin code + // + esp_bt_pin_type_t pin_type = ESP_BT_PIN_TYPE_FIXED; + esp_bt_pin_code_t pin_code; + pin_code[0] = '1'; + pin_code[1] = '2'; + pin_code[2] = '3'; + pin_code[3] = '4'; + esp_bt_gap_set_pin(pin_type, 4, pin_code); + + if((result = esp_bt_gap_register_callback(processBTGapEvent))) + { + ESP_LOGE(TAG, "esp_bt_gap_register_callback failed: %d", result); + return false; + } + + // Allow BT devices to connect back to us + if((result = esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_NON_DISCOVERABLE))) + { + ESP_LOGE(TAG, "esp_bt_gap_set_scan_mode failed: %d", result); + return false; + } + #endif + + // BLE GAP + if((result = esp_ble_gap_register_callback(processBLEGapEvent))) + { + ESP_LOGE(TAG, "esp_ble_gap_register_callback failed: %d", result); + return false; + } + + // Setup security, no password. + esp_ble_auth_req_t auth_req = ESP_LE_AUTH_REQ_SC_MITM_BOND; // Bonding with peer device after authentication + esp_ble_io_cap_t iocapble = ESP_IO_CAP_NONE; // Set the IO capability to No output No input + esp_ble_gap_set_security_param(ESP_BLE_SM_SET_STATIC_PASSKEY, &passkey, sizeof(uint32_t)); + esp_ble_gap_set_security_param(ESP_BLE_SM_AUTHEN_REQ_MODE, &auth_req, sizeof(uint8_t)); + esp_ble_gap_set_security_param(ESP_BLE_SM_IOCAP_MODE, &iocapble, sizeof(uint8_t)); + esp_ble_gap_set_security_param(ESP_BLE_SM_MAX_KEY_SIZE, &key_size, sizeof(uint8_t)); + esp_ble_gap_set_security_param(ESP_BLE_SM_ONLY_ACCEPT_SPECIFIED_SEC_AUTH, &auth_option, sizeof(uint8_t)); + esp_ble_gap_set_security_param(ESP_BLE_SM_OOB_SUPPORT, &oob_support, sizeof(uint8_t)); + esp_ble_gap_set_security_param(ESP_BLE_SM_SET_INIT_KEY, &init_key, sizeof(uint8_t)); + esp_ble_gap_set_security_param(ESP_BLE_SM_SET_RSP_KEY, &rsp_key, sizeof(uint8_t)); + + // Initialise parameters. + btCtrl.batteryLevel = -1; + return true; +} + +// Basic constructor, do nothing! +BT::BT(void) +{ + btCtrl.hidhDevHdl = NULL; + #ifdef CONFIG_CLASSIC_BT_ENABLED + btCtrl.pairingHandler = nullptr; + btCtrl.bt_hidh_cb_semaphore = nullptr; + #endif + btCtrl.ble_hidh_cb_semaphore = nullptr; + pBTThis = NULL; + // +} + +// Basic destructor, do nothing! Only ever called for instantiation of uninitialsed class to prove version data.Used for probing versions etc. +BT::~BT(void) +{ + // +} diff --git a/main/BTHID.cpp b/main/BTHID.cpp deleted file mode 120000 index e25db07..0000000 --- a/main/BTHID.cpp +++ /dev/null @@ -1 +0,0 @@ -../../sharpkey/main/BTHID.cpp \ No newline at end of file diff --git a/main/BTHID.cpp b/main/BTHID.cpp new file mode 100644 index 0000000..68e34cd --- /dev/null +++ b/main/BTHID.cpp @@ -0,0 +1,1011 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// Name: BTHID.cpp +// Created: Mar 2022 +// Version: v1.0 +// Author(s): Philip Smart +// Description: Bluetooth Keyboard Class. +// This source file contains the class to encapsulate a Bluetooth keyboard as a sub +// class of the BT base class. +// It provides connection, key retrieval and first stage mapping to be compatible +// with a PS/2 keyboard prior to host mapping. +// +// The application uses the Espressif Development environment with Arduino components. +// This is necessary as the class uses the Arduino methods for GPIO manipulation. I +// was considering using pure Espressif IDF methods but considered the potential +// of also using this class on an Arduino project. +// +// Credits: +// Copyright: (c) 2022 Philip Smart +// +// History: Mar 2022 - Initial write. +// Jun 2022 - Updated with latest findings. Now checks the bonded list and opens +// connections or scans for new devices if no connections exist. +// +// 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 "BTHID.h" + +// Out of object pointer to a singleton class for use in the ESP IDF API callback routines which werent written for C++. Other methods can be used but this one is the simplest +// to understand and the class can only ever be singleton. +BTHID *pBTHID = NULL; + +// Method to open a connection with a paired device. +// +bool BTHID::openDevice(esp_bd_addr_t bda, esp_hid_transport_t transport, esp_ble_addr_type_t addrType) +{ + // Locals. + // + bool found = false; + t_activeDev device; + + // Call underlying IDF API to open the device. Store handle for future use. + device.hidhDevHdl = esp_hidh_dev_open(bda, transport, addrType); + + // Add device to list of known devices. + for(std::size_t idx = 0; idx < pBTHID->btHIDCtrl.devices.size(); idx++) + { + // Already on list? + if (memcmp(bda, btHIDCtrl.devices[idx].bda, sizeof(esp_bd_addr_t)) == 0) + { + found = true; + } + } + if(!found && device.hidhDevHdl != 0) + { + memcpy(device.bda, bda, sizeof(esp_bd_addr_t)); + device.transport = transport; + device.addrType = addrType; + device.open = true; + device.nextCheckTime = milliSeconds() + 5000L; + btHIDCtrl.devices.push_back(device); + } + + // Return connection status. + return(device.hidhDevHdl == NULL ? false : true); +} + +// Method to close a connection with a paired device. +// +bool BTHID::closeDevice(esp_bd_addr_t bda) +{ + // Locals. + // + esp_err_t result = ESP_OK; + + // Locate device and close it out. + for(std::size_t idx = 0; idx < pBTHID->btHIDCtrl.devices.size(); idx++) + { + // Already on list? + if (memcmp(bda, btHIDCtrl.devices[idx].bda, sizeof(esp_bd_addr_t)) == 0) + { + if(btHIDCtrl.devices[idx].hidhDevHdl != NULL) + { + result = esp_hidh_dev_close(btHIDCtrl.devices[idx].hidhDevHdl); + btHIDCtrl.devices[idx].open = false; + } + } + } + + return(result); +} + +// Callback to handle Bluetooth HID data. This method is called whenever an event occurs, such as a new device being opened or existing one closed. Also +// called with data reports from connected devices. The method determines source of data and routes it to the correct channel (keyboard or mouse). +// +// Example BT Classic Device +// BDA:17:27:6d:85:25:e9, Status: OK, Connected: YES, Handle: 0, Usage: KEYBOARD +// Name: , Manufacturer: , Serial Number: +// PID: 0x7021, VID: 0x04e8, VERSION: 0x011b +// Report Map Length: 295 +// CCONTROL INPUT REPORT, ID: 255, Length: 1 +// CCONTROL INPUT REPORT, ID: 5, Length: 1 +// GENERIC INPUT REPORT, ID: 4, Length: 1 +// CCONTROL INPUT REPORT, ID: 3, Length: 7 +// CCONTROL INPUT REPORT, ID: 2, Length: 3 +// KEYBOARD OUTPUT BOOT, ID: 1, Length: 1 +// KEYBOARD OUTPUT REPORT, ID: 1, Length: 1 +// KEYBOARD INPUT BOOT, ID: 1, Length: 8 +// KEYBOARD INPUT REPORT, ID: 1, Length: 8 +// +// Example BLE Device +// BDA:cf:4a:a8:5c:5e:8c, Appearance: 0x03c0, Connection ID: 0 +// Name: M585/M590, Manufacturer: Logitech, Serial Number: A2C4E0DBF89C6DFA +// PID: 0xb01b, VID: 0x046d, VERSION: 0x0011 +// Battery: Handle: 29, CCC Handle: 0 +// Report Maps: 1 +// Report Map Length: 140 +// VENDOR OUTPUT REPORT, ID: 17, Length: 19, Permissions: 0x0e, Handle: 56, CCC Handle: 0 +// VENDOR INPUT REPORT, ID: 17, Length: 19, Permissions: 0x12, Handle: 52, CCC Handle: 53 +// MOUSE INPUT REPORT, ID: 2, Length: 7, Permissions: 0x12, Handle: 48, CCC Handle: 49 +// KEYBOARD INPUT REPORT, ID: 1, Length: 7, Permissions: 0x12, Handle: 44, CCC Handle: 45 +// MOUSE INPUT BOOT, ID: 2, Length: 3, Permissions: 0x12, Handle: 39, CCC Handle: 40 +// KEYBOARD OUTPUT BOOT, ID: 0, Length: 8, Permissions: 0x0e, Handle: 37, CCC Handle: 0 +// KEYBOARD INPUT BOOT, ID: 1, Length: 8, Permissions: 0x12, Handle: 34, CCC Handle: 35 +// +void BTHID::hidh_callback(void *handler_args, esp_event_base_t base, int32_t id, void * event_data) +{ + // Locals. + // + t_activeDev device; + esp_hidh_event_t event = (esp_hidh_event_t) id; + esp_hidh_event_data_t *param = (esp_hidh_event_data_t *) event_data; + + switch (event) + { + case ESP_HIDH_OPEN_EVENT: + { + const uint8_t *bda = esp_hidh_dev_bda_get(param->open.dev); + esp_hid_usage_t usage = esp_hidh_dev_usage_get(param->open.dev); + if (param->open.status == ESP_OK) + { + // Update status of device in list. + bool found = false; + for(std::size_t idx = 0; idx < pBTHID->btHIDCtrl.devices.size(); idx++) + { + // Try and re-open closed devices. + if(memcmp(bda, pBTHID->btHIDCtrl.devices[idx].bda, sizeof(esp_bd_addr_t)) == 0) + { + pBTHID->btHIDCtrl.devices[idx].open = true; + pBTHID->btHIDCtrl.devices[idx].usage = usage; + found = true; + break; + } + } + // If device not found on device list it will be a previous pairing which has woken up so add to list. + if(found == false) + { + memcpy(device.bda, bda, sizeof(esp_bd_addr_t)); + device.transport = esp_hidh_dev_transport_get(param->open.dev); + device.addrType = BLE_ADDR_TYPE_RANDOM; + device.open = true; + device.usage = usage; + pBTHID->btHIDCtrl.devices.push_back(device); + } + + // Ask for the current LED status on keyboards, this is used to pre-set the function locks. + if(usage == ESP_HID_USAGE_KEYBOARD) + { + esp_hidh_dev_get_report(param->open.dev, 0, 0x1, ESP_HID_REPORT_TYPE_OUTPUT, 10); + } + ESP_LOGD(TAG, ESP_BD_ADDR_STR " OPEN: %s", ESP_BD_ADDR_HEX(bda), esp_hidh_dev_name_get(param->open.dev)); + esp_hidh_dev_dump(param->open.dev, stdout); + vTaskDelay(100); + } else + { + // Update status of device in list. + for(std::size_t idx = 0; idx < pBTHID->btHIDCtrl.devices.size(); idx++) + { + // Try and re-open closed devices. + if(bda != NULL && pBTHID->btHIDCtrl.devices[idx].bda != NULL && memcmp(bda, pBTHID->btHIDCtrl.devices[idx].bda, sizeof(esp_bd_addr_t)) == 0) + { + pBTHID->btHIDCtrl.devices[idx].open = false; + } + } + ESP_LOGE(TAG, " OPEN failed!"); + // pBTHID->closeDevice(); + } + break; + } + case ESP_HIDH_BATTERY_EVENT: + { + const uint8_t *bda = esp_hidh_dev_bda_get(param->battery.dev); + ESP_LOGD(TAG, ESP_BD_ADDR_STR " BATTERY: %d%%", ESP_BD_ADDR_HEX(bda), param->battery.level); + pBTHID->setBatteryLevel(param->battery.level); + break; + } + case ESP_HIDH_INPUT_EVENT: + { + const uint8_t *bda = esp_hidh_dev_bda_get(param->input.dev); + ESP_LOGD(TAG, ESP_BD_ADDR_STR " INPUT: %8s, MAP: %2u, ID: %3u, Len: %d, Data:", + ESP_BD_ADDR_HEX(bda), + esp_hid_usage_str(param->input.usage), + param->input.map_index, + param->input.report_id, + param->input.length); + ESP_LOG_BUFFER_HEX_LEVEL(TAG, param->input.data, param->input.length, ESP_LOG_DEBUG); + + // Add data to queue for later filtering and processing. + pBTHID->pushKeyToFIFO(param->input.usage, param->input.dev, param->input.data, param->input.length); + break; + } + case ESP_HIDH_FEATURE_EVENT: + { + const uint8_t *bda = esp_hidh_dev_bda_get(param->feature.dev); + + for(std::size_t idx = 0; idx < pBTHID->btHIDCtrl.devices.size(); idx++) + { + // Matched device? + if(memcmp(bda, pBTHID->btHIDCtrl.devices[idx].bda, sizeof(esp_bd_addr_t)) == 0) + { + // Is this an LED update report? + if(pBTHID->btHIDCtrl.devices[idx].usage == ESP_HID_USAGE_KEYBOARD && param->feature.map_index == 0 && param->feature.report_id == 0x01 && param->feature.length == 0x01) + { + if(param->feature.data[0] & BT_LED_NUMLOCK) + { + pBTHID->btHIDCtrl.kbd.btFlags |= BT_NUM_LOCK; + } else + { + pBTHID->btHIDCtrl.kbd.btFlags &= ~BT_NUM_LOCK; + } + if(param->feature.data[0] & BT_LED_CAPSLOCK) + { + pBTHID->btHIDCtrl.kbd.btFlags |= BT_CAPS_LOCK; + } else + { + pBTHID->btHIDCtrl.kbd.btFlags &= ~BT_CAPS_LOCK; + } + if(param->feature.data[0] & BT_LED_SCROLLLOCK) + { + pBTHID->btHIDCtrl.kbd.btFlags |= BT_SCROLL_LOCK; + } else + { + pBTHID->btHIDCtrl.kbd.btFlags &= ~BT_SCROLL_LOCK; + } + pBTHID->btHIDCtrl.kbd.statusLED = param->feature.data[0]; + } + break; + } + } + ESP_LOGD(TAG, ESP_BD_ADDR_STR " FEATURE: %8s, MAP: %2u, ID: %3u, Len: %d", + ESP_BD_ADDR_HEX(bda), + esp_hid_usage_str(param->feature.usage), + param->feature.map_index, + param->feature.report_id, + param->feature.length); + ESP_LOG_BUFFER_HEX_LEVEL(TAG, param->feature.data, param->feature.length, ESP_LOG_DEBUG); + break; + } + case ESP_HIDH_CLOSE_EVENT: + { + const uint8_t *bda = esp_hidh_dev_bda_get(param->close.dev); + if(bda != NULL) + { + for(std::size_t idx = 0; idx < pBTHID->btHIDCtrl.devices.size(); idx++) + { + // Device which has closed? + if(memcmp(bda, pBTHID->btHIDCtrl.devices[idx].bda, sizeof(esp_bd_addr_t)) == 0) + { + ESP_LOGD(TAG, "Closing device:%d,%s", idx, esp_hidh_dev_name_get(param->close.dev)); + pBTHID->btHIDCtrl.devices[idx].open = false; + } + } + ESP_LOGD(TAG, ESP_BD_ADDR_STR " CLOSE: %s", ESP_BD_ADDR_HEX(bda), esp_hidh_dev_name_get(param->close.dev)); + } + break; + } + default: + ESP_LOGD(TAG, "EVENT: %d", event); + break; + } +} + +// Method to process a received key or mouse movement, keys go onto an internal FIFO queue buffering until the application requests them, +// mouse movements are dispatched immediately via callback as latency is important with a mouse. +// NB: Overflow data is lost so application needs to process data in a timely fashion. +// +void BTHID::pushKeyToFIFO(esp_hid_usage_t src, esp_hidh_dev_t *hdlDev, uint8_t *keys, uint8_t size) +{ + // Locals. + KeyInfo keyInfo; + PS2Mouse::MouseData mouseData; + + // Use FreeRTOS queue manager to push the key record onto the FIFO. + if(src == ESP_HID_USAGE_KEYBOARD || src == ESP_HID_USAGE_CCONTROL) + { + for(int idx=0; idx < MAX_KEYBOARD_DATA_BYTES; idx++) + { + if(idx < size) + { + keyInfo.keys[idx] = keys[idx]; + } else + { + keyInfo.keys[idx] = 0x00; + } + } + keyInfo.length = size; + keyInfo.cControl = (src == ESP_HID_USAGE_CCONTROL ? true : false); + keyInfo.hdlDev = hdlDev; + xQueueSendFromISR(btHIDCtrl.kbd.rawKeyQueue, &keyInfo, 0); + } + else if(src == ESP_HID_USAGE_MOUSE) + { + // Mouse data is processed realtime. It is massaged into PS/2 data, encapsulated, then passed to the provided + // callback which handles it. + mouseData.overrun = false; + mouseData.valid = true; + + // Ensure a movement report. + if(size > 3) + { + // Bit 3 is always set on PS/2 messages. + mouseData.status = keys[0] | 0x08; + + // The resolution of a BT mouse is typically 12bit signed, ie. -2048 .. +2047 on both axis. PS/2 was typically 9bit, ie. -255 .. +254 and the Sharp + // hosts are typically -128 .. +127 so the values need to be scaled down after applying any configurable setting. + mouseData.position.x = keys[3] & 0x08 ? (-2048 + ((((keys[3]&0x07) << 8)) | keys[2])) : (((keys[3]&0x07) << 8) | keys[2]); + mouseData.position.y = -(keys[4] & 0x80 ? (-2048 + (((keys[4]&0x7f) << 4) | ((keys[3]&0xf0) >> 4))) : (((keys[4]&0x7f) << 4) | ((keys[3]&0xf0) >> 4))); + + // Apply any PS/2 configurable settings which have meaning. + // + mouseData.position.x = mouseData.position.x * btHIDCtrl.ms.scaling; + mouseData.position.y = mouseData.position.y * btHIDCtrl.ms.scaling; + mouseData.position.x = mouseData.position.x * btHIDCtrl.ms.resolution; + mouseData.position.y = mouseData.position.y * btHIDCtrl.ms.resolution; + + // Set the wheel value. + mouseData.wheel = keys[5]; + } + + // If a data callback has been setup, invoke otherwise data is wasted. + // + if(btHIDCtrl.ms.mouseDataCallback != NULL) + btHIDCtrl.ms.mouseDataCallback(mouseData); + } + return; +} + +// Method to check devices for connectivity. This generally entails re-opening closed devices as BT links are self maintaining until closure. +// +void BTHID::checkBTDevices(void) +{ + // Locals. + // + bool nonFound = true; + std::vector scanList; + + // Loop through list of known devices and open a connection with them. If no devices exist or no connection can be opened, start + // a scan for new devices. Normally, bonded devices when activated will connect but sometimes a physical open is needed hence this + // logic. + for(std::size_t idx = 0; idx < btHIDCtrl.devices.size(); idx++) + { + if(btHIDCtrl.devices[idx].open == true) + { + nonFound = false; + } else + { + // If the timer has expired on this entry, make an open attempt. + if(btHIDCtrl.devices[idx].nextCheckTime <= milliSeconds()) + { + ESP_LOGI(TAG, ESP_BD_ADDR_STR " PAIREDOPEN", ESP_BD_ADDR_HEX(btHIDCtrl.devices[idx].bda)); + if(openDevice(btHIDCtrl.devices[idx].bda, btHIDCtrl.devices[idx].transport, btHIDCtrl.devices[idx].addrType) == true) + { + btHIDCtrl.devices[idx].open = true; + } else + { + btHIDCtrl.devices[idx].nextCheckTime = milliSeconds() + 5000L; + } + } + nonFound = false; + } + } + if(nonFound) + { + // Get list of devices which can be seen by bluetooth receiver and try to connect to known/pairing devices. + getDeviceList(scanList, 5); + + for(int idx = 0; idx < scanList.size(); idx++) + { + ESP_LOGI(TAG, ESP_BD_ADDR_STR " SCANOPEN", ESP_BD_ADDR_HEX(scanList[idx].bda)); + openDevice(scanList[idx].bda, scanList[idx].transport, scanList[idx].ble.addr_type); + } + } + return; +} + +//********************************************************************************************************************************************* +//** Mouse handler Methods. +//********************************************************************************************************************************************* +// +// Protocol: Finding accurate information for the Mouse low level protocol is not so easy and what you do find doesnt match the data reports +// sent by the ESP HIDH. The protocol as worked out (so far) is: +// +// +// = Status or Button Report. Bit 2 = Wheel/Middle Button, Bit 1 = Right Button, Bit 0 = Left Button. '1' = Button pressed. +// = 0x00 +// = LSB [7:0] of X co-ordinate, Signed range -2048:+2047 +// = MSNIBBLE [11:8] contained in bits [3:0] of x co-ordinate. LSNIBBLE [3:0] contained in bits [7:4] or y co-ordinate. +// = MSB [11:4] of Y co-ordinate, Signed range -2048:+2047 +// = 0x00 +// = 0x00 +// Reading the data, sample rate is 125 frames per second. +// + +// Public method to set the mouse resolution in pixels per millimeter, valid values are 0..3. +// This method is for compatibility with a PS/2 Mouse, any use of the value has to be programmtical in this module prior to delivery +// of the fixed data streamed from the BT HID. +// +bool BTHID::setResolution(enum PS2Mouse::PS2_RESOLUTION resolution) +{ + // Locals. + // + bool result = false; + + // Sanity check. + if(resolution >= PS2Mouse::PS2_MOUSE_RESOLUTION_1_1 && resolution < PS2Mouse::PS2_MOUSE_RESOLUTION_1_8) + { + switch(to_underlying(resolution)) + { + case 0: // 1pixel per mm. + btHIDCtrl.ms.resolution = 1; + break; + case 1: // 2pixles per mm. + btHIDCtrl.ms.resolution = 2; + break; + case 2: // 4pixels per mm. + btHIDCtrl.ms.resolution = 4; + break; + case 3: // 8pixels per mm. + default: + btHIDCtrl.ms.resolution = 8; + break; + } + result = true; + } + + // Return result. + return(result); +} + +// Public method to set the mouse scaling, either Normal 1:1 (scaling = 0) or non-linear 2:1 (scaling = 1). +// This method is for compatibility with a PS/2 Mouse, any use of the value has to be programmtical in this module prior to delivery +// of the fixed data streamed from the BT HID. +// +bool BTHID::setScaling(enum PS2Mouse::PS2_SCALING scaling) +{ + // Locals. + // + bool result = false; + + // Sanity check. + if(scaling >= PS2Mouse::PS2_MOUSE_SCALING_1_1 && scaling < PS2Mouse::PS2_MOUSE_SCALING_2_1) + { + btHIDCtrl.ms.scaling = to_underlying(scaling)+1; + result = true; + } + + // Return result. + return(result); +} + +// Public method to set the automatic sample rate. +// This method is for compatibility with a PS/2 Mouse, any use of the value has to be programmtical in this module prior to delivery +// of the fixed data streamed from the BT HID. +// +bool BTHID::setSampleRate(enum PS2Mouse::PS2_SAMPLING rate) +{ + // Locals. + // + bool result = false; + + // Sanity check. + if(rate == PS2Mouse::PS2_MOUSE_SAMPLE_RATE_10 || rate == PS2Mouse::PS2_MOUSE_SAMPLE_RATE_20 || rate == PS2Mouse::PS2_MOUSE_SAMPLE_RATE_40 || rate == PS2Mouse::PS2_MOUSE_SAMPLE_RATE_60 || rate == PS2Mouse::PS2_MOUSE_SAMPLE_RATE_80 || rate == PS2Mouse::PS2_MOUSE_SAMPLE_RATE_100 || rate == PS2Mouse::PS2_MOUSE_SAMPLE_RATE_200) + { + btHIDCtrl.ms.sampleRate = to_underlying(rate); + result = true; + } + + // Return result. + return(result); +} + +//********************************************************************************************************************************************* +//** Keyboard handler Methods. +//********************************************************************************************************************************************* + +// Method to map a Bluetooth Media Key (ESP HIDH specific) Scan Code to its PS/2 equivalent or 0x0000 if not mappable. +uint16_t BTHID::mapBTMediaToPS2(uint32_t key) +{ + // Locals. + // + uint16_t retKey = 0x0000; + + // Loop through mapping table to find a match. + for(int idx=0; idx < btHIDCtrl.kbd.kmeMediaRows; idx++) + { + if(btHIDCtrl.kbd.kmeMedia[idx].mediaKey == key) + { + retKey = (btHIDCtrl.kbd.kmeMedia[idx].ps2Ctrl << 8) | btHIDCtrl.kbd.kmeMedia[idx].ps2Key; + break; + } + } + + // Return map result or 0x00 if not mappable. + return(retKey); +} + +// Method to map a Bluetooth Scan Code to its PS/2 equivalent or 0x00 if not mappable. +uint16_t BTHID::mapBTtoPS2(uint8_t key) +{ + // Locals. + // + uint16_t retKey = 0x0000; + + // Loop through mapping table to find a match. + for(int idx=0; idx < btHIDCtrl.kbd.kmeRows && retKey == 0x0000; idx++) + { + // Find a match. + if(btHIDCtrl.kbd.kme[idx].btKeyCode == key && (btHIDCtrl.kbd.kme[idx].btCtrl == btHIDCtrl.kbd.btFlags || btHIDCtrl.kbd.kme[idx].btCtrl == BT_NONE)) + { + retKey = (uint16_t)btHIDCtrl.kbd.kme[idx].ps2KeyCode; + if((retKey <= PS2_KEY_SPACE || retKey >= PS2_KEY_F1) && retKey != PS2_KEY_BTICK && retKey != PS2_KEY_HASH && retKey != PS2_KEY_EUROPE2) retKey |= PS2_FUNCTION; + if(btHIDCtrl.kbd.btFlags & BT_CTRL_LEFT || btHIDCtrl.kbd.btFlags & BT_CTRL_RIGHT) retKey |= PS2_CTRL; + if(btHIDCtrl.kbd.btFlags & BT_SHIFT_LEFT || btHIDCtrl.kbd.btFlags & BT_SHIFT_RIGHT) retKey |= PS2_SHIFT; + if(btHIDCtrl.kbd.btFlags & BT_ALT_LEFT) retKey |= PS2_ALT; + if(btHIDCtrl.kbd.btFlags & BT_ALT_RIGHT) retKey |= PS2_ALT_GR; + if(btHIDCtrl.kbd.btFlags & BT_GUI_LEFT || btHIDCtrl.kbd.btFlags & BT_GUI_RIGHT) retKey |= PS2_GUI; + } + } + + // Return map result or 0x00 if not mappable. + return(retKey); +} + +// Method to set a status LED on the keyboard. +// +void BTHID::setStatusLED(esp_hidh_dev_t *dev, uint8_t led) +{ + // Locals + + // Set flag in LED status byte then forward to the keyboard for actual display. + btHIDCtrl.kbd.statusLED |= led; + esp_hidh_dev_output_set(dev, 0, 0x1, &btHIDCtrl.kbd.statusLED, 1); + return; +} + +// Method to clear a status LED on the keyboard. +// +void BTHID::clearStatusLED(esp_hidh_dev_t *dev, uint8_t led) +{ + // Locals + + // Clear flag in LED status byte then forward to the keyboard for actual display. + btHIDCtrl.kbd.statusLED &= ~led; + esp_hidh_dev_output_set(dev, 0, 0x1, &btHIDCtrl.kbd.statusLED, 1); + return; +} + +// Method to process the incoming Bluetooth keyboard data stream and convert it into PS/2 compatible values. +// +// Protocol (received after pre-processing by the BT module) +// -------- +// KEYBOARD: +// <0x00> +// All scan codes are set to overflow (0x01) if more than 6 keys are pressed. +// +// Modifier Byte: +// Bit 7 Bit 6 Bit 5 Bit 4 Bit 3 Bit 2 Bit 1 Bit 0 +// Right GUI Right Alt Right Shift Right Ctrl Left GUI Left Alt Left Shift Left Ctrl +// 1 = Key Active, 0 = Inactive. +// +// CCONTROL: (the esp idf splits the bluetooth report of keys into keys and media control) +// ESP havent documented the HIDH module so the values below are worked out, needs updating when they provide documentation. +// +// +// Bit 7 Bit 6 Bit 5 Bit 4 Bit 3 Bit 2 Bit 1 Bit 0 +// SEARCH HOME +// Bit 7 Bit 6 Bit 5 Bit 4 Bit 3 Bit 2 Bit 1 Bit 0 +// BRITEDN BRITEUP +// Bit 7 Bit 6 Bit 5 Bit 4 Bit 3 Bit 2 Bit 1 Bit 0 +// MUTE VOL DOWN VOL UP TRK PREV +// +// +// A down event sees the scan code appear in the list, an up event it disappears. For the modifier bits, the bit is set for down event and cleared for up event. +// +// Control mapping - BT modifier needs to be mapped to these bits: +// Define name bit description +// PS2_BREAK 15 1 = Break key code +// (MSB) 0 = Make Key code +// PS2_SHIFT 14 1 = Shift key pressed as well (either side) +// 0 = NO shift key +// PS2_CTRL 13 1 = Ctrl key pressed as well (either side) +// 0 = NO Ctrl key +// PS2_CAPS 12 1 = Caps Lock ON +// 0 = Caps lock OFF +// PS2_ALT 11 1 = Left Alt key pressed as well +// 0 = NO Left Alt key +// PS2_ALT_GR 10 1 = Right Alt (Alt GR) key pressed as well +// 0 = NO Right Alt key +// PS2_GUI 9 1 = GUI key pressed as well (either) +// 0 = NO GUI key +// PS2_FUNCTION 8 1 = FUNCTION key non-printable character (plus space, tab, enter) +// 0 = standard character key +// +// Mapped data/events is pushed onto a queue which is read by the calling API. +// +void BTHID::processBTKeys(void) +{ + // Locals. + uint16_t genKey; + uint32_t mediaKey; + KeyInfo keyInfo; + + // Process all the queued event data. + while(xQueueReceive(btHIDCtrl.kbd.rawKeyQueue, &keyInfo, 0) == pdTRUE) + { + // Process normal scancodes. + if(keyInfo.cControl == false) + { + // Only process if the size is correct. + if(keyInfo.length <= MAX_KEYBOARD_DATA_BYTES) + { + // Process control keys and set flags. + if(keyInfo.keys[0] & BT_CTRL_LEFT) btHIDCtrl.kbd.btFlags |= BT_CTRL_LEFT; else btHIDCtrl.kbd.btFlags &= ~(BT_CTRL_LEFT); + if(keyInfo.keys[0] & BT_SHIFT_LEFT) btHIDCtrl.kbd.btFlags |= BT_SHIFT_LEFT; else btHIDCtrl.kbd.btFlags &= ~(BT_SHIFT_LEFT); + if(keyInfo.keys[0] & BT_ALT_LEFT) btHIDCtrl.kbd.btFlags |= BT_ALT_LEFT; else btHIDCtrl.kbd.btFlags &= ~BT_ALT_LEFT; + if(keyInfo.keys[0] & BT_GUI_LEFT) btHIDCtrl.kbd.btFlags |= BT_GUI_LEFT; else btHIDCtrl.kbd.btFlags &= ~BT_GUI_LEFT; + if(keyInfo.keys[0] & BT_CTRL_RIGHT) btHIDCtrl.kbd.btFlags |= BT_CTRL_RIGHT; else btHIDCtrl.kbd.btFlags &= ~(BT_CTRL_RIGHT); + if(keyInfo.keys[0] & BT_SHIFT_RIGHT) btHIDCtrl.kbd.btFlags |= BT_SHIFT_RIGHT; else btHIDCtrl.kbd.btFlags &= ~(BT_SHIFT_RIGHT); + if(keyInfo.keys[0] & BT_ALT_RIGHT) btHIDCtrl.kbd.btFlags |= BT_ALT_RIGHT; else btHIDCtrl.kbd.btFlags &= ~(BT_ALT_RIGHT); + if(keyInfo.keys[0] & BT_GUI_RIGHT) btHIDCtrl.kbd.btFlags |= BT_GUI_RIGHT; else btHIDCtrl.kbd.btFlags &= ~(BT_GUI_RIGHT); + + // Process the control(modifier) keys and generate events. + // CTRL keys + if((keyInfo.keys[0] & BT_CTRL_LEFT) != 0 && (btHIDCtrl.kbd.lastKeys[0] & BT_CTRL_LEFT) == 0) + { + // First time key was pressed send a Make event. + genKey = (btHIDCtrl.kbd.ps2Flags & 0xFF00) | PS2_CTRL | PS2_FUNCTION | PS2_KEY_L_CTRL; + xQueueSend(btHIDCtrl.kbd.keyQueue, &genKey, 0); + } + if((keyInfo.keys[0] & BT_CTRL_LEFT) == 0 && (btHIDCtrl.kbd.lastKeys[0] & BT_CTRL_LEFT) != 0) + { + // Key being released generates a BREAK event. + genKey = (btHIDCtrl.kbd.ps2Flags & 0xFF00) | PS2_BREAK | PS2_FUNCTION | PS2_KEY_L_CTRL; + xQueueSend(btHIDCtrl.kbd.keyQueue, &genKey, 0); + } + if((keyInfo.keys[0] & BT_CTRL_RIGHT) != 0 && (btHIDCtrl.kbd.lastKeys[0] & BT_CTRL_RIGHT) == 0) + { + // First time key was pressed send a Make event. + genKey = (btHIDCtrl.kbd.ps2Flags & 0xFF00) | PS2_CTRL | PS2_FUNCTION | PS2_KEY_R_CTRL; + xQueueSend(btHIDCtrl.kbd.keyQueue, &genKey, 0); + } + if((keyInfo.keys[0] & BT_CTRL_RIGHT) == 0 && (btHIDCtrl.kbd.lastKeys[0] & BT_CTRL_RIGHT) != 0) + { + // Key being released generates a BREAK event. + genKey = (btHIDCtrl.kbd.ps2Flags & 0xFF00) | PS2_BREAK | PS2_FUNCTION | PS2_KEY_R_CTRL; + xQueueSend(btHIDCtrl.kbd.keyQueue, &genKey, 0); + } + // SHIFT Keys + if((keyInfo.keys[0] & BT_SHIFT_LEFT) != 0 && (btHIDCtrl.kbd.lastKeys[0] & BT_SHIFT_LEFT) == 0) + { + // First time key was pressed send a Make event. + genKey = (btHIDCtrl.kbd.ps2Flags & 0xFF00) | PS2_SHIFT | PS2_FUNCTION | PS2_KEY_L_SHIFT; + xQueueSend(btHIDCtrl.kbd.keyQueue, &genKey, 0); + } + if((keyInfo.keys[0] & BT_SHIFT_LEFT) == 0 && (btHIDCtrl.kbd.lastKeys[0] & BT_SHIFT_LEFT) != 0) + { + // Key being released generates a BREAK event. + genKey = (btHIDCtrl.kbd.ps2Flags & 0xFF00) | PS2_BREAK | PS2_FUNCTION | PS2_KEY_L_SHIFT; + xQueueSend(btHIDCtrl.kbd.keyQueue, &genKey, 0); + } + if((keyInfo.keys[0] & BT_SHIFT_RIGHT) != 0 && (btHIDCtrl.kbd.lastKeys[0] & BT_SHIFT_RIGHT) == 0) + { + // First time key was pressed send a Make event. + genKey = (btHIDCtrl.kbd.ps2Flags & 0xFF00) | PS2_SHIFT | PS2_FUNCTION | PS2_KEY_R_SHIFT; + xQueueSend(btHIDCtrl.kbd.keyQueue, &genKey, 0); + } + if((keyInfo.keys[0] & BT_SHIFT_RIGHT) == 0 && (btHIDCtrl.kbd.lastKeys[0] & BT_SHIFT_RIGHT) != 0) + { + // Key being released generates a BREAK event. + genKey = (btHIDCtrl.kbd.ps2Flags & 0xFF00) | PS2_BREAK | PS2_FUNCTION | PS2_KEY_R_SHIFT; + xQueueSend(btHIDCtrl.kbd.keyQueue, &genKey, 0); + } + // ALT Keys + if((keyInfo.keys[0] & BT_ALT_LEFT) != 0 && (btHIDCtrl.kbd.lastKeys[0] & BT_ALT_LEFT) == 0) + { + // First time key was pressed send a Make event. + genKey = (btHIDCtrl.kbd.ps2Flags & 0xFF00) | PS2_ALT | PS2_FUNCTION | PS2_KEY_L_ALT; + xQueueSend(btHIDCtrl.kbd.keyQueue, &genKey, 0); + } + if((keyInfo.keys[0] & BT_ALT_LEFT) == 0 && (btHIDCtrl.kbd.lastKeys[0] & BT_ALT_LEFT) != 0) + { + // Key being released generates a BREAK event. + genKey = (btHIDCtrl.kbd.ps2Flags & 0xFF00) | PS2_BREAK | PS2_FUNCTION | PS2_KEY_L_ALT; + xQueueSend(btHIDCtrl.kbd.keyQueue, &genKey, 0); + } + if((keyInfo.keys[0] & BT_ALT_RIGHT) != 0 && (btHIDCtrl.kbd.lastKeys[0] & BT_ALT_RIGHT) == 0) + { + // First time key was pressed send a Make event. + genKey = (btHIDCtrl.kbd.ps2Flags & 0xFF00) | PS2_ALT_GR | PS2_FUNCTION | PS2_KEY_R_ALT; + xQueueSend(btHIDCtrl.kbd.keyQueue, &genKey, 0); + } + if((keyInfo.keys[0] & BT_ALT_RIGHT) == 0 && (btHIDCtrl.kbd.lastKeys[0] & BT_ALT_RIGHT) != 0) + { + // Key being released generates a BREAK event. + genKey = (btHIDCtrl.kbd.ps2Flags & 0xFF00) | PS2_BREAK | PS2_FUNCTION | PS2_KEY_R_ALT; + xQueueSend(btHIDCtrl.kbd.keyQueue, &genKey, 0); + } + // GUI Keys + if((keyInfo.keys[0] & BT_GUI_LEFT) != 0 && (btHIDCtrl.kbd.lastKeys[0] & BT_GUI_LEFT) == 0) + { + // First time key was pressed send a Make event. + genKey = (btHIDCtrl.kbd.ps2Flags & 0xFF00) | PS2_GUI | PS2_FUNCTION | PS2_KEY_L_GUI; + xQueueSend(btHIDCtrl.kbd.keyQueue, &genKey, 0); + } + if((keyInfo.keys[0] & BT_GUI_LEFT) == 0 && (btHIDCtrl.kbd.lastKeys[0] & BT_GUI_LEFT) != 0) + { + // Key being released generates a BREAK event. + genKey = (btHIDCtrl.kbd.ps2Flags & 0xFF00) | PS2_BREAK | PS2_FUNCTION | PS2_KEY_L_GUI; + xQueueSend(btHIDCtrl.kbd.keyQueue, &genKey, 0); + } + if((keyInfo.keys[0] & BT_GUI_RIGHT) != 0 && (btHIDCtrl.kbd.lastKeys[0] & BT_GUI_RIGHT) == 0) + { + // First time key was pressed send a Make event. + genKey = (btHIDCtrl.kbd.ps2Flags & 0xFF00) | PS2_GUI | PS2_FUNCTION | PS2_KEY_R_GUI; + xQueueSend(btHIDCtrl.kbd.keyQueue, &genKey, 0); + } + if((keyInfo.keys[0] & BT_GUI_RIGHT) == 0 && (btHIDCtrl.kbd.lastKeys[0] & BT_GUI_RIGHT) != 0) + { + // Key being released generates a BREAK event. + genKey = (btHIDCtrl.kbd.ps2Flags & 0xFF00) | PS2_BREAK | PS2_FUNCTION | PS2_KEY_R_GUI; + xQueueSend(btHIDCtrl.kbd.keyQueue, &genKey, 0); + } + + // Loop through the 6 scan codes and if a code appears in this set but not in the last generate a Make event. + // + for(int idx=1; idx < MAX_KEYBOARD_DATA_BYTES; idx++) + { + if(keyInfo.keys[idx] != 0) + { + bool found = false; + for(int idx2=1; idx2 < MAX_KEYBOARD_DATA_BYTES; idx2++) + { + if(keyInfo.keys[idx] == btHIDCtrl.kbd.lastKeys[idx2]) found = true; + } + if(!found) + { + // Process CAPS Lock. + if(keyInfo.keys[idx] == BT_KEY_CAPSLOCK && (btHIDCtrl.kbd.btFlags & BT_CAPS_LOCK) == 0) + { + btHIDCtrl.kbd.btFlags |= BT_CAPS_LOCK; + setStatusLED(keyInfo.hdlDev, BT_LED_CAPSLOCK); + } + else if(keyInfo.keys[idx] == BT_KEY_CAPSLOCK && (btHIDCtrl.kbd.btFlags & BT_CAPS_LOCK) != 0) + { + btHIDCtrl.kbd.btFlags &= ~(BT_CAPS_LOCK); + clearStatusLED(keyInfo.hdlDev, BT_LED_CAPSLOCK); + } + + // Process NUM Lock. + if(keyInfo.keys[idx] == BT_KEY_NUMLOCK && (btHIDCtrl.kbd.btFlags & BT_NUM_LOCK) == 0) + { + btHIDCtrl.kbd.btFlags |= BT_NUM_LOCK; + setStatusLED(keyInfo.hdlDev, BT_LED_NUMLOCK); + } + else if(keyInfo.keys[idx] == BT_KEY_NUMLOCK && (btHIDCtrl.kbd.btFlags & BT_NUM_LOCK) != 0) + { + btHIDCtrl.kbd.btFlags &= ~(BT_NUM_LOCK); + clearStatusLED(keyInfo.hdlDev, BT_LED_NUMLOCK); + } + + // Process SCROLL Lock. + if(keyInfo.keys[idx] == BT_KEY_SCROLLLOCK && (btHIDCtrl.kbd.btFlags & BT_SCROLL_LOCK) == 0) + { + btHIDCtrl.kbd.btFlags |= BT_SCROLL_LOCK; + setStatusLED(keyInfo.hdlDev, BT_LED_SCROLLLOCK); + } + else if(keyInfo.keys[idx] == BT_KEY_SCROLLLOCK && (btHIDCtrl.kbd.btFlags & BT_SCROLL_LOCK) != 0) + { + btHIDCtrl.kbd.btFlags &= ~(BT_SCROLL_LOCK); + clearStatusLED(keyInfo.hdlDev, BT_LED_SCROLLLOCK); + } + + // Mimicking the PS/2 class, set Function for certain mapped keys. + uint16_t mapKey = mapBTtoPS2(keyInfo.keys[idx]); + ESP_LOGI(TAG, "BTKEYMAP:%02x:%04x -> %04x", keyInfo.keys[idx], btHIDCtrl.kbd.btFlags, mapKey); + + // Do not forward certain keys. + if(mapKey != 0x0000 && keyInfo.keys[idx] != BT_KEY_NUMLOCK) + { + // Create a Make event. + xQueueSend(btHIDCtrl.kbd.keyQueue, &mapKey, 0); + } + } + } + // Now repeat in reverse, has a break event occurred? + if(btHIDCtrl.kbd.lastKeys[idx] != 0) + { + bool found = false; + for(int idx2=1; idx2 < MAX_KEYBOARD_DATA_BYTES; idx2++) + { + if(btHIDCtrl.kbd.lastKeys[idx] == keyInfo.keys[idx2]) found = true; + } + if(!found) + { + uint16_t mapKey = mapBTtoPS2(btHIDCtrl.kbd.lastKeys[idx]); + mapKey |= PS2_BREAK; // Send break event by adding PS2_BREAK control flag. + + // Do not forward certain keys. + if(mapKey != 0x0000 && btHIDCtrl.kbd.lastKeys[idx] != BT_KEY_NUMLOCK) + { + // Create a Break event. + xQueueSend(btHIDCtrl.kbd.keyQueue, &mapKey, 0); + } + } + } + } + } + } + // Media control keys, for some reason these come as a seperate BT report and are 24bits wide. + else + { + // Only process if size is correct. + if(keyInfo.length == MAX_CCONTROL_DATA_BYTES) + { + // Assemble 24bit map, easier to work with. + mediaKey = (keyInfo.keys[0] << 16) | (keyInfo.keys[1] << 8) | (keyInfo.keys[2]); + + // Check for key Make events. + for(int idx=0; idx < 23; idx++) + { + uint32_t mask = (1 << idx); + + // Make event. + if((mediaKey & mask) != 0 && (btHIDCtrl.kbd.lastMediaKey & mask) == 0) + { + uint16_t mapKey = mapBTMediaToPS2(mediaKey & mask); + xQueueSend(btHIDCtrl.kbd.keyQueue, &mapKey, 0); + + } + // Break event. + if((mediaKey & mask) == 0 && (btHIDCtrl.kbd.lastMediaKey & mask) != 0) + { + uint16_t mapKey = mapBTMediaToPS2(btHIDCtrl.kbd.lastMediaKey & mask); + xQueueSend(btHIDCtrl.kbd.keyQueue, &mapKey, 0); + } + } + + // Store last processed keymap for next loop. + btHIDCtrl.kbd.lastMediaKey = mediaKey; + } + } + + // Copy current to last. + for(int idx=0; idx < MAX_KEYBOARD_DATA_BYTES; idx++) + { + btHIDCtrl.kbd.lastKeys[idx] = keyInfo.keys[idx]; + } + } + + return; +} + +// Method to retrieve a key from the BT stack. The key is mapped from BT scancodes to PS/2 scancodes. +// +uint16_t BTHID::getKey(uint32_t timeout) +{ + // Locals. + // + uint16_t key; + bool result = false; + uint32_t timeCurrent = milliSeconds(); + + // Loop processing BT keys until a key received or timeout occurs. + do { + // Process latest BT keys. + processBTKeys(); + + // Get the next key from the processed queue and return to caller. + result = (xQueueReceive(btHIDCtrl.kbd.keyQueue, &key, 0) == pdTRUE ? true : false); + } while(timeout > 0 && timeCurrent+timeout > milliSeconds() && result == false); + + // Return key if one has been read else 0x00. + return(result == true ? key : 0x00); +} + +// Method to configure Bluetooth and register required callbacks. +bool BTHID::setup(t_pairingHandler *handler) +{ + // Locals. + // + bool result = false; + + // Check for multiple instantiations, only one instance allowed. + if(pBTHID != nullptr) + { + ESP_LOGE(TAG, "Setup called more than once. Only one instance of BTHID is allowed."); + } else + { + // Invoke the base class method which sets up the bluetooth layer. + BT::setup(handler); + + // Store current object for use in callback handlers. + pBTHID = this; + + // Create a FIFO queue to store incoming keyboard keys and mouse movements. + btHIDCtrl.kbd.rawKeyQueue = xQueueCreate(10, sizeof(KeyInfo)); + btHIDCtrl.kbd.keyQueue = xQueueCreate(10, sizeof(uint16_t)); + + ESP_ERROR_CHECK(esp_ble_gattc_register_callback(esp_hidh_gattc_event_handler)); + esp_hidh_config_t config = { + .callback = hidh_callback, + .event_stack_size = 4*1024, + .callback_arg = nullptr + }; + ESP_ERROR_CHECK(esp_hidh_init(&config)); + result = true; + + // Go through bonded lists and add to our control vector or known devices. + // First BLE devices. + int bleDevNum = esp_ble_get_bond_device_num(); + esp_ble_bond_dev_t *bleDevList = (esp_ble_bond_dev_t *)malloc(sizeof(esp_ble_bond_dev_t) * bleDevNum); + esp_ble_get_bond_device_list(&bleDevNum, bleDevList); + for (int idx = 0; idx < bleDevNum; idx++) + { + t_activeDev device; + memcpy(device.bda, bleDevList[idx].bd_addr, sizeof(esp_bd_addr_t)); + device.transport = ESP_HID_TRANSPORT_BLE; + device.addrType = BLE_ADDR_TYPE_RANDOM; + device.open = false; + device.nextCheckTime = milliSeconds() + 3000L; + btHIDCtrl.devices.push_back(device); + ESP_LOGW(TAG, "BLE BONDED DEVICE: " ESP_BD_ADDR_STR, ESP_BD_ADDR_HEX(bleDevList[idx].bd_addr)); + } + free(bleDevList); + + // Next BT devices. + int btDevNum = esp_bt_gap_get_bond_device_num(); + esp_bd_addr_t *btDevList = (esp_bd_addr_t *)malloc(sizeof(esp_bd_addr_t) * btDevNum); + esp_bt_gap_get_bond_device_list(&btDevNum, btDevList); + for (int idx = 0; idx < btDevNum; idx++) + { + t_activeDev device; + memcpy(device.bda, btDevList[idx], sizeof(esp_bd_addr_t)); + device.transport = ESP_HID_TRANSPORT_BT; + device.addrType = BLE_ADDR_TYPE_RANDOM; + device.open = false; + device.nextCheckTime = milliSeconds() + 3000L; + btHIDCtrl.devices.push_back(device); + ESP_LOGW(TAG, "BT BONDED DEVICE: " ESP_BD_ADDR_STR, ESP_BD_ADDR_HEX(btDevList[idx])); + } + free(btDevList); + } + + // False = failed to setup, true = success. + return(result); +} + +// Basic constructor, do nothing! +BTHID::BTHID(void) +{ + btHIDCtrl.kbd.rawKeyQueue = NULL; + btHIDCtrl.kbd.keyQueue = NULL; + memset((void *)&btHIDCtrl.kbd.lastKeys, 0x00, 6); + btHIDCtrl.kbd.lastMediaKey = 0x00000000; + btHIDCtrl.kbd.ps2Flags = 0x0000; + btHIDCtrl.kbd.btFlags = 0x0000; + btHIDCtrl.kbd.statusLED = 0x00; + btHIDCtrl.kbd.kme = BTKeyToPS2.kme; + btHIDCtrl.kbd.kmeRows = MAX_BT2PS2_MAP_ENTRIES; + btHIDCtrl.kbd.kmeMedia = MediaKeyToPS2.kme; + btHIDCtrl.kbd.kmeMediaRows = MAX_BTMEDIA2PS2_MAP_ENTRIES; + btHIDCtrl.ms.mouseDataCallback = NULL; + btHIDCtrl.ms.resolution = 8; + btHIDCtrl.ms.scaling = 1; + btHIDCtrl.ms.sampleRate = 100; + btHIDCtrl.ms.xDivisor = 8; + btHIDCtrl.ms.yDivisor = 8; + + // btHIDCtrl.repeatPeriod = pdMS_TO_TICKS(120); +} + +// Basic destructor, do nothing! Only ever called for instantiation of uninitialsed class to prove version data.Used for probing versions etc. +BTHID::~BTHID(void) +{ + // +} diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt deleted file mode 120000 index a086ba4..0000000 --- a/main/CMakeLists.txt +++ /dev/null @@ -1 +0,0 @@ -../../sharpkey/main/CMakeLists.txt \ No newline at end of file diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt new file mode 100644 index 0000000..94e811d --- /dev/null +++ b/main/CMakeLists.txt @@ -0,0 +1,4 @@ +set(COMPONENT_SRCS SharpKey.cpp NVS.cpp LED.cpp SWITCH.cpp KeyInterface.cpp MZ2528.cpp X1.cpp X68K.cpp Mouse.cpp MZ5665.cpp PC9801.cpp HID.cpp WiFi.cpp PS2KeyAdvanced.cpp PS2Mouse.cpp BT.cpp BTHID.cpp esp_efuse_custom_table.c) +set(COMPONENT_ADD_INCLUDEDIRS "." "include") + +register_component() diff --git a/main/HID.cpp b/main/HID.cpp deleted file mode 120000 index ba6daf2..0000000 --- a/main/HID.cpp +++ /dev/null @@ -1 +0,0 @@ -../../sharpkey/main/HID.cpp \ No newline at end of file diff --git a/main/HID.cpp b/main/HID.cpp new file mode 100644 index 0000000..ecb72a5 --- /dev/null +++ b/main/HID.cpp @@ -0,0 +1,1018 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// 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; + } +} diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild deleted file mode 120000 index 54ade14..0000000 --- a/main/Kconfig.projbuild +++ /dev/null @@ -1 +0,0 @@ -../../sharpkey/main/Kconfig.projbuild \ No newline at end of file diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild new file mode 100644 index 0000000..14cbad4 --- /dev/null +++ b/main/Kconfig.projbuild @@ -0,0 +1,309 @@ +menu "SharpKey Configuration" + + choice BUILD_TARGET + prompt "Build target" + default SHARPKEY + help + Choose the target of the next build, SHARPKEY for the multi-host SharpKey Interface, MZ25KEY_MZ2500 for the mz25key Interface which will be used + with an MZ-2500 or MZ25KEY_MZ2800 for the mz25key Interface which will be used with an MZ-2800. + + config SHARPKEY + bool "SharpKey" + help + Target build for the SharpKey multi-host Interface. + + config MZ25KEY_MZ2500 + bool "mz25key - MZ2500" + help + Target build for the mz25key Interface for use on an MZ-2500 + + config MZ25KEY_MZ2800 + bool "mz25key - MZ2800" + help + Target build for the mz25key Interface for use on an MZ-2800 + endchoice + + choice FEATURE_SECURITY + prompt "Runtime Feature Security" + default DISABLE_FEATURE_SECURITY + help + Choose wether to enable features in the firmware based on fuse settings or to disable the feature. + + config DISABLE_FEATURE_SECURITY + bool "Disable feature security" + help + Disable the feature security, enabling all inbuilt modules of the SharpKey firmware. + + config ENABLE_FEATURE_SECURITY + bool "Enable feature security" + help + Enable feature security, modules will only be available if the corresponding eFuse is set. + endchoice + + menu "PS2 Keyboard" + + config PS2_HW_DATAPIN + int "GPIO pin number used for the PS/2 Keyboard DATA line" + range 0 46 + default 14 + help + GPIO number (IOxx) used to connect with the PS/2 Keyboard DATA line. + Some GPIOs are used for other purposes (flash connections, etc.) and cannot be used to I2C. + GPIOs 35-39 are input-only so cannot be used as outputs. + + config PS2_HW_CLKPIN + int "GPIO pin number used for the PS/2 Keyboard CLK line" + range 0 46 + default 13 + help + GPIO number (IOxx) used to connect with the PS/2 Keyboard CLK line. + This pin must be interrupt capable. + Some GPIOs are used for other purposes (flash connections, etc.) and cannot be used to I2C. + GPIOs 35-39 are input-only so cannot be used as outputs. + + endmenu + + menu "Host Interface" + menu "4Bit Strobe Input" + + config HOST_KDB0 + int "KDB0 GPIO pin number" + range 0 46 + default 23 + help + GPIO number (IOxx) used to connect the 4bit bidirectional data bus Bit 0 with the ESP32. See schematic for actual used value. May change with revisions. + + config HOST_KDB1 + int "KDB1 GPIO pin number" + range 0 46 + default 25 + help + GPIO number (IOxx) used to connect the 4bit bidirectional data bus Bit 1 with the ESP32. See schematic for actual used value. May change with revisions. + + config HOST_KDB2 + int "KDB2 GPIO pin number" + range 0 46 + default 26 + help + GPIO number (IOxx) used to connect the 4bit bidirectional data bus Bit 2 with the ESP32. See schematic for actual used value. May change with revisions. + + config HOST_KDB3 + int "KDB3 GPIO pin number" + range 0 46 + default 27 + help + GPIO number (IOxx) used to connect the 4bit bidirectional data bus Bit 3 with the ESP32. See schematic for actual used value. May change with revisions. + endmenu + + menu "8Bit Scan Data Output" + config HOST_KDO0 + int "KDO0 GPIO pin number" + range 0 46 + default 14 + help + GPIO number (IOxx) used to connect the 8bit scan data output Bit 0 to the 74HCT257 IC. See schematic for actual used value. May change with revisions. + + config HOST_KDO1 + int "KDO1 GPIO pin number" + range 0 46 + default 15 + help + GPIO number (IOxx) used to connect the 8bit scan data output Bit 1 to the 74HCT257 IC. See schematic for actual used value. May change with revisions. + + config HOST_KDO2 + int "KDO2 GPIO pin number" + range 0 46 + default 16 + help + GPIO number (IOxx) used to connect the 8bit scan data output Bit 2 to the 74HCT257 IC. See schematic for actual used value. May change with revisions. + + config HOST_KDO3 + int "KDO3 GPIO pin number" + range 0 46 + default 17 + help + GPIO number (IOxx) used to connect the 8bit scan data output Bit 3 to the 74HCT257 IC. See schematic for actual used value. May change with revisions. + + config HOST_KDO4 + int "KDO4 GPIO pin number" + range 0 46 + default 18 + help + GPIO number (IOxx) used to connect the 8bit scan data output Bit 4 to the 74HCT257 IC. See schematic for actual used value. May change with revisions. + + config HOST_KDO5 + int "KDO5 GPIO pin number" + range 0 46 + default 19 + help + GPIO number (IOxx) used to connect the 8bit scan data output Bit 5 to the 74HCT257 IC. See schematic for actual used value. May change with revisions. + + config HOST_KDO6 + int "KDO6 GPIO pin number" + range 0 46 + default 21 + help + GPIO number (IOxx) used to connect the 8bit scan data output Bit 6 to the 74HCT257 IC. See schematic for actual used value. May change with revisions. + + config HOST_KDO7 + int "KDO7 GPIO pin number" + range 0 46 + default 21 + help + GPIO number (IOxx) used to connect the 8bit scan data output Bit 7 to the 74HCT257 IC. See schematic for actual used value. May change with revisions. + endmenu + + choice MOUSE_UART_CHOICE + prompt "Mouse host side UART" + default HOST_BITBANG_UART + help + Select the hardware method of sending mouse data to the host. The two possible methods are bitbang (software UART) and UART Hardware. + config HOST_BITBANG_UART + bool "Bitbang UART" + help + Use the Bitbang UART (software). + config HOST_HW_UART + bool "Hardware UART" + help + Use one of the ESP32 Hardware UART's. + endchoice + + config HOST_RTSNI + int "RTSNi GPIO pin number" + range 0 46 + default 35 + help + GPIO number (IOxx) used to connect the RTSN line with the ESP32. See schematic for actual used value. May change with revisions. + + config HOST_MPXI + int "MPXi GPIO pin number" + range 0 46 + default 12 + help + GPIO number (IOxx) used to connect the MPX line with the ESP32. See schematic for actual used value. May change with revisions. + + config HOST_KDI4 + int "KDI4 GPIO pin number" + range 0 46 + default 13 + help + GPIO number (IOxx) used to connect the KDI4 line with the ESP32. See schematic for actual used value. May change with revisions. + + endmenu + + menu "WiFi" + + config IF_WIFI_ENABLED + bool "Enable WiFi connectivity" + default false + help + Allow interface to act as an Access Point to allow external connectivity. Once connected the WiFi is intended to be used for making + key mapping changes. + This is an experimental feature and under development. + + config IF_WIFI_EN_KEY + int "WiFi Enable GPIO pin number" + range 0 46 + default 34 + depends on IF_WIFI_ENABLED + help + GPIO number (IOxx) used by the WiFi En switch to enable wifi connectivity. + + config IF_WIFI_SSID + string "Default SSID in Access Point Mode" + default "sharpkey" + depends on IF_WIFI_ENABLED + help + The SSID broadcast whilst the sharpkey module advertises wireless connectivity. + + config IF_WIFI_DEFAULT_SSID_PWD + string "Default password for initial connection to Access Point Mode" + default "sharpkey" + depends on IF_WIFI_ENABLED + help + The initial password needed to connect and logon to access point. + + config IF_WIFI_MAX_RETRIES + int "Maximum number of connection retries." + range 0 100 + default 10 + depends on IF_WIFI_ENABLED + help + Number of retries allowed for making a wireless connection with a client. + + config IF_WIFI_AP_CHANNEL + int "Channel of the Access Point." + range 0 13 + default 7 + depends on IF_WIFI_ENABLED + help + Channel use by the Access Point, default is 7. + + config IF_WIFI_SSID_HIDDEN + int "Broadcast SSID?" + range 0 1 + default 0 + depends on IF_WIFI_ENABLED + help + Broadcast the SSID (0) or hide it (1). + + config IF_WIFI_MAX_CONNECTIONS + int "Maximum simultaneous connections." + range 0 20 + default 5 + depends on IF_WIFI_ENABLED + help + Maximum number of simultaneous open connections supported. + + endmenu + + menu "Debug Options" + + config DEBUG_SERIAL + bool "Serial debug output" + default false + help + Enable debug output (non ESP logging) on the serial port. + + config DEBUG_DISABLE_KDB + bool "Disable input mode actuation of the KDB data bus" + default false + help + Disable the Host KDB input configuration step, useful feature for debugging. + + config DEBUG_DISABLE_KDO + bool "Disable output mode actuation of the KDO strobe row" + default false + help + Disable the Host KDO output configuration step, useful feature for debugging. + + config DEBUG_DISABLE_RTSNI + bool "Disable input mode actuation of the RTSNi signal" + default false + help + Disable the Host RTSNi input configuration step, useful feature for debugging. + + config DEBUG_DISABLE_MPXI + bool "Disable input mode actuation of the MPXi signal" + default false + help + Disable the Host MPXi input configuration step, useful feature for debugging. + + config DEBUG_DISABLE_KDI + bool "Disable input mode actuation of the KDI4 signal" + default false + help + Disable the Host KDI input configuration step, useful feature for debugging. + endmenu + + config PWRLED + int "GPIO pin number used for Power On and Status LED" + range 0 46 + default 25 + help + GPIO number (IOxx) used to control the Power On/Status LED. + Some GPIOs are used for other purposes (flash connections, etc.) and cannot be used to I2C. + GPIOs 35-39 are input-only so cannot be used as outputs. + +endmenu + diff --git a/main/KeyInterface.cpp b/main/KeyInterface.cpp deleted file mode 120000 index 8b319c1..0000000 --- a/main/KeyInterface.cpp +++ /dev/null @@ -1 +0,0 @@ -../../sharpkey/main/KeyInterface.cpp \ No newline at end of file diff --git a/main/KeyInterface.cpp b/main/KeyInterface.cpp new file mode 100644 index 0000000..2e327c1 --- /dev/null +++ b/main/KeyInterface.cpp @@ -0,0 +1,211 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// Name: KeyInterface.cpp +// Created: Mar 2022 +// Version: v1.0 +// Author(s): Philip Smart +// Description: Base class with virtual abstraction of key methods on which all host interfaces, +// instantiated as a singleton, are based. This module comprises all common interface +// code and the header contains the virtual abstraction methods overriden by the +// sub-class which forms an actual interface. +// Credits: +// Copyright: (c) 2019-2022 Philip Smart +// +// History: Mar 2022 - Initial write. +// v1.01 May 2022 - Initial release version. +// +// 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 "KeyInterface.h" + + +// Method to reconfigure the GPIO on ADC2. This is necessary due to an ESP32 issue regarding WiFi Client mode and ADC2. If the +// pins are input and wrong value the WiFi Client mode wont connect, if they are output it will connect!! A few issues with the +// ESP32 you have to work around, next design, try use ADC2 as outputs only! +void KeyInterface::reconfigADC2Ports(bool setAsOutput) +{ + // Locals. + // + gpio_config_t io_conf; + + // Configure 4 inputs to be the Strobe Row Number which is used to index the virtual key matrix and the strobe data returned. + #if !defined(CONFIG_DEBUG_DISABLE_KDB) + io_conf.intr_type = GPIO_INTR_DISABLE; + io_conf.mode = (setAsOutput == true ? GPIO_MODE_OUTPUT : GPIO_MODE_INPUT); + io_conf.pin_bit_mask = (1ULL<suspend = suspendIf; +} + +// 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 KeyInterface::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 test to see if the interface is running and not suspended. +// Two modes, one just tests and returns the state, the second waits in a loop until the interface runs. +bool KeyInterface::isRunning(bool waitForRelease) +{ + // If flag set, loop waiting for the suspended flag to be set. + while(waitForRelease == true && this->suspended == true) + { + vTaskDelay(1); + } + + // Return the suspended status. + return(this->suspended); +} + +// Base initialisation for generic hardware used by all sub-classes. The sub-class invokes the init +// method manually from within it's init method. +void KeyInterface::init(const char *subClassName, NVS *hdlNVS, LED *hdlLED, HID *hdlHID, uint32_t ifMode) +{ + // Locals + #define INITTAG "init" + + // Store the NVS object. + this->nvs = hdlNVS; + + // Store the LED object. + this->led = hdlLED; + + // Store the HID object. + this->hid = hdlHID; + + // Store the sub-class name for later use, ie. NVS key access. + this->subClassName = subClassName; + + // Set LED to on. + led->setLEDMode(LED::LED_MODE_ON, LED::LED_DUTY_CYCLE_OFF, 0, 0L, 0L); + + // All done, no return code! + return; +} + +// Base initialisation for generic hardware used by all sub-classes. The sub-class invokes the init +// method manually from within it's init method. +// This method doesnt initialise hardware, used for objects probing this object data. +void KeyInterface::init(const char *subClassName, NVS *hdlNVS, HID *hdlHID) +{ + // Locals + #define INITTAG "init" + + // Store the NVS object. + this->nvs = hdlNVS; + + // Store the HID object. + this->hid = hdlHID; + + // Store the sub-class name for later use, ie. NVS key access. + this->subClassName = subClassName; + + // All done, no return code! + return; +} + + +// Constructor, basically initialise the Singleton interface and let the threads loose. +//KeyInterface::KeyInterface(uint32_t ifMode) +//{ +// // init(className(__PRETTY_FUNCTION__), ifMode); +//} + +// Basic constructor, do nothing! +//KeyInterface::KeyInterface(void) +//{ +// // +//} diff --git a/main/LED.cpp b/main/LED.cpp deleted file mode 120000 index b67f606..0000000 --- a/main/LED.cpp +++ /dev/null @@ -1 +0,0 @@ -../../sharpkey/main/LED.cpp \ No newline at end of file diff --git a/main/LED.cpp b/main/LED.cpp new file mode 100644 index 0000000..db504a3 --- /dev/null +++ b/main/LED.cpp @@ -0,0 +1,341 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// Name: LED.cpp +// Created: Mar 2022 +// Version: v1.0 +// Author(s): Philip Smart +// Description: Base class for the encapsulation and control methods of an LED used primarily to +// indicate to users the status of the application. +// Credits: +// Copyright: (c) 2019-2022 Philip Smart +// +// History: Mar 2022 - Initial write. +// v1.01 May 2022 - Initial release version. +// +// 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 "LED.h" + +// Method to set the LED mode, duty cycle and duty period. Once the current LED cycle has come to an end, the control +// thread will replace the working configuration with the new configuration set here. +// +bool LED::setLEDMode(enum LED_MODE mode, enum LED_DUTY_CYCLE dutyCycle, uint32_t maxBlinks, uint64_t usDutyPeriod, uint64_t msInterPeriod) +{ + // Locals. + // + bool result = true; + + // If a setup is already waiting to be processed, exit with fail. This could be stacked into a vector but not really beneficial. + if(ledCtrl.newConfig.updated == false) + { + // Ensure we have exclusive access, the LED can be controlled by numerous threads, so ensure only one can access and setup at a time. + if(xSemaphoreTake(ledCtrl.mutexInternal, (TickType_t)1000) == pdTRUE) + { + ledCtrl.newConfig.mode = mode; + ledCtrl.newConfig.dutyCycle = dutyCycle; + ledCtrl.newConfig.maxBlinks = maxBlinks; + ledCtrl.newConfig.dutyPeriod = usDutyPeriod; + ledCtrl.newConfig.interPeriod = msInterPeriod; + ledCtrl.newConfig.updated = true; + + // Release mutex so other threads can set the LED. + xSemaphoreGive(ledCtrl.mutexInternal); + } else + { + result = false; + } + } else + { + result = false; + } + return(result); +} + +// Thread method to provide LED control. +IRAM_ATTR void LED::ledInterface(void *pvParameters) +{ + // Locals. + // + uint32_t LED_MASK; + uint64_t delayTimer = 0LL; + uint64_t curTime = 0LL; + enum LEDSTATE { + LEDSTATE_IDLE = 0, + LEDSTATE_BLINK_MARK = 1, + LEDSTATE_BLINK_SPACE = 2, + LEDSTATE_BLINK_INTER = 3, + } fsmState = LEDSTATE_IDLE; + #define LEDIFTAG "ledInterface" + + // Map the instantiating object so we can access its methods and data. + LED* pThis = (LED*)pvParameters; + + // Set the LED GPIO mask according to the defined pin. This code needs updating if GPIO1 pins are used. + LED_MASK = (1 << pThis->ledCtrl.ledPin); + + // Sign on. + ESP_LOGW("ledInterface", "Starting LED control thread."); + + // Turn off LED. + GPIO.out_w1tc = LED_MASK; + + // Loops forever. + for(;;) + { + // Check stack space, report if it is getting low. + if(uxTaskGetStackHighWaterMark(NULL) < 1024) + { + ESP_LOGW(LEDIFTAG, "THREAD STACK SPACE(%d)\n",uxTaskGetStackHighWaterMark(NULL)); + } + + // If a new configuration is set, then once the running FSM has returned to idle, update the configuration prior to the next FSM run. + // + if(pThis->ledCtrl.newConfig.updated) + { + // Take control of the Mutex so we are able to take on the data without a new setup clashing. If the Mutex is taken then continue on with the state machine logic till next loop. + if(xSemaphoreTake(pThis->ledCtrl.mutexInternal, (TickType_t)1) == pdTRUE) + { + pThis->ledCtrl.currentConfig = pThis->ledCtrl.newConfig; + pThis->ledCtrl.currentConfig.valid = true; + pThis->ledCtrl.newConfig.updated = false; + pThis->ledCtrl.blinkCnt = 0; + + // Got new setup so release mutex. + xSemaphoreGive(pThis->ledCtrl.mutexInternal); + } + } + + // Only run if we have a valid configuration. + if(pThis->ledCtrl.currentConfig.valid) + { + do { + // Get the current timer value, only run the FSM when the timer is idle. + timer_get_counter_value(TIMER_GROUP_0, TIMER_1, &curTime); + if(curTime >= delayTimer) + { + // Ensure the timer is stopped. + timer_pause(TIMER_GROUP_0, TIMER_1); + delayTimer = 0LL; + + // Mini finite state machine for LED control. + switch(fsmState) + { + case LEDSTATE_IDLE: + // For on/off, no need for the FSM, just apply setting to LED and loop. + switch(pThis->ledCtrl.currentConfig.mode) + { + case LED_MODE_ON: + // Turn on LED. + GPIO.out_w1ts = LED_MASK; + delayTimer = 1000UL; + break; + + case LED_MODE_BLINK_ONESHOT: + // If the number of blinks is not 0 then on reaching the count, switch to LED off mode. + if(pThis->ledCtrl.currentConfig.maxBlinks > 0 && pThis->ledCtrl.blinkCnt++ >= pThis->ledCtrl.currentConfig.maxBlinks) + { + pThis->ledCtrl.currentConfig.mode = LED_MODE_OFF; + } else + { + fsmState = LEDSTATE_BLINK_MARK; + } + break; + + case LED_MODE_BLINK: + // Normal blink mode increments the count which is used for determining inter blink period. + pThis->ledCtrl.blinkCnt++; + fsmState = LEDSTATE_BLINK_MARK; + break; + + case LED_MODE_OFF: + default: + // Turn off LED. + GPIO.out_w1tc = LED_MASK; + delayTimer = 1000UL; + break; + + } + break; + + case LEDSTATE_BLINK_MARK: + // Turn on LED. + GPIO.out_w1ts = LED_MASK; + + // Next state, SPACE. + fsmState = LEDSTATE_BLINK_SPACE; + + // Calculate time to SPACE. + switch(pThis->ledCtrl.currentConfig.dutyCycle) + { + case LED_DUTY_CYCLE_10: + delayTimer = (pThis->ledCtrl.currentConfig.dutyPeriod / 10); + break; + case LED_DUTY_CYCLE_20: + delayTimer = ((pThis->ledCtrl.currentConfig.dutyPeriod / 10) * 2); + break; + case LED_DUTY_CYCLE_30: + delayTimer = ((pThis->ledCtrl.currentConfig.dutyPeriod / 10) * 3); + break; + case LED_DUTY_CYCLE_40: + delayTimer = ((pThis->ledCtrl.currentConfig.dutyPeriod / 10) * 4); + break; + case LED_DUTY_CYCLE_50: + delayTimer = ((pThis->ledCtrl.currentConfig.dutyPeriod / 10) * 5); + break; + case LED_DUTY_CYCLE_60: + delayTimer = ((pThis->ledCtrl.currentConfig.dutyPeriod / 10) * 6); + break; + case LED_DUTY_CYCLE_70: + delayTimer = ((pThis->ledCtrl.currentConfig.dutyPeriod / 10) * 7); + break; + case LED_DUTY_CYCLE_80: + delayTimer = ((pThis->ledCtrl.currentConfig.dutyPeriod / 10) * 8); + break; + case LED_DUTY_CYCLE_90: + delayTimer = ((pThis->ledCtrl.currentConfig.dutyPeriod / 10) * 9); + break; + // We shouldnt be here if duty cycle is off, so back to idle. + case LED_DUTY_CYCLE_OFF: + default: + GPIO.out_w1tc = LED_MASK; + delayTimer = 0; + fsmState = LEDSTATE_IDLE; + break; + } + break; + + case LEDSTATE_BLINK_SPACE: + // Turn off LED. + GPIO.out_w1tc = LED_MASK; + + // Calculate time to next MARK. + delayTimer = pThis->ledCtrl.currentConfig.dutyPeriod - delayTimer; + + // Now add an interblink delay prior to next blink. + fsmState = LEDSTATE_BLINK_INTER; + break; + + case LEDSTATE_BLINK_INTER: + // If we are in normal mode with a blink limit set and limit reached or in limited mode, then add an interblink delay as configured. + if((pThis->ledCtrl.currentConfig.mode == LED_MODE_BLINK && pThis->ledCtrl.currentConfig.maxBlinks > 0 && pThis->ledCtrl.blinkCnt >= pThis->ledCtrl.currentConfig.maxBlinks) || + (pThis->ledCtrl.currentConfig.mode == LED_MODE_BLINK_ONESHOT)) + { + // Interblink delay is given in milli-seconds, so multiply up and set delay. + delayTimer = pThis->ledCtrl.currentConfig.interPeriod * 1000; + + // Reset blink counter to trigger next interperiod delay. + if(pThis->ledCtrl.currentConfig.mode == LED_MODE_BLINK) + pThis->ledCtrl.blinkCnt = 0; + } + + // We return to IDLE to allow time for reconfiguration if requested. + fsmState = LEDSTATE_IDLE; + break; + + // Unknown or not programmed state, return to IDLE. + default: + fsmState = LEDSTATE_IDLE; + break; + } + + // If a new delay is requested, reset the value in the timer and start. + if(delayTimer > 0LL) + { + timer_set_counter_value(TIMER_GROUP_0, TIMER_1, 0LL); + timer_start(TIMER_GROUP_0, TIMER_1); + } + } + + // Give the OS some time... + taskYIELD(); + } while(fsmState != LEDSTATE_IDLE); + + } + } +} + +// Method to set the GPIO pin to be used for LED output. +void LED::ledInit(uint8_t ledPin) +{ + // Initialise variables. + this->ledCtrl.currentConfig.valid = false; + this->ledCtrl.currentConfig.updated = false; + this->ledCtrl.currentConfig.mode = LED_MODE_OFF; + this->ledCtrl.currentConfig.dutyCycle = LED_DUTY_CYCLE_OFF; + this->ledCtrl.currentConfig.dutyPeriod = 0LL; + this->ledCtrl.currentConfig.interPeriod = 0LL; + this->ledCtrl.newConfig = this->ledCtrl.currentConfig; + + // Store GPIO pin to which LED is connected. + this->ledCtrl.ledPin = ledPin; + + // Configure a timer to be used for the LED blink rate. + timer_config_t timerConfig = { + .alarm_en = TIMER_ALARM_DIS, // No alarm, were not using interrupts as we are in a dedicated thread. + .counter_en = TIMER_PAUSE, // Timer paused until required. + .intr_type = TIMER_INTR_LEVEL, // No interrupts used. + .counter_dir = TIMER_COUNT_UP, // Timing a fixed period. + .auto_reload = TIMER_AUTORELOAD_DIS, // No need for auto reload, fixed time period. + .divider = 80 // 1Mhz operation giving 1uS resolution. + }; + ESP_ERROR_CHECK(timer_init(TIMER_GROUP_0, TIMER_1, &timerConfig)); + ESP_ERROR_CHECK(timer_set_counter_value(TIMER_GROUP_0, TIMER_1, 0)); + + // Setup mutex's. + ledCtrl.mutexInternal = xSemaphoreCreateMutex(); + + // Core 0 - Application + // LED control thread - dedicated thread to control the LED according to set mode. + ESP_LOGW("ledInit", "Starting LEDif thread..."); + ::xTaskCreatePinnedToCore(&this->ledInterface, "ledif", 4096, this, 0, &this->TaskLEDIF, 0); +} + +// Constructor, basically initialise the Singleton interface and let the control thread loose. +LED::LED(uint32_t hwPin) +{ + // Store the class name for later use, ie. NVS key access. + this->className = getClassName(__PRETTY_FUNCTION__); + + // Configure the Power LED used for activity and user interaction. Initial state is ON until a keyboard is detected when it turns off and only blinks on keyboard activity. + ledInit(hwPin); + + // Initial state, turn on LED to indicate LED control is working. + setLEDMode(LED::LED_MODE_ON, LED::LED_DUTY_CYCLE_OFF, 0, 0L, 0L); +} + +// Basic constructor, do nothing! +LED::LED(void) +{ + // Store the class name for later use, ie. NVS key access. + this->className = getClassName(__PRETTY_FUNCTION__); +} diff --git a/main/MZ2528.cpp b/main/MZ2528.cpp deleted file mode 120000 index 9e11183..0000000 --- a/main/MZ2528.cpp +++ /dev/null @@ -1 +0,0 @@ -../../sharpkey/main/MZ2528.cpp \ No newline at end of file diff --git a/main/MZ2528.cpp b/main/MZ2528.cpp new file mode 100644 index 0000000..1910c93 --- /dev/null +++ b/main/MZ2528.cpp @@ -0,0 +1,1244 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// Name: MZ2528.cpp +// Created: Jan 2022 +// Version: v1.0 +// Author(s): Philip Smart +// Description: MZ2500/2800 Key Matrix logic. +// This source file contains the application logic to obtain PS/2 scan codes, map them +// into a virtual keyboard matrix as per the Sharp MZ series key matrix and the +// logic to transmit the virtual key matrix to the MZ2500/2800. +// +// The application uses a modified version of the PS2KeyAdvanced +// https://github.com/techpaul/PS2KeyAdvanced class from Paul Carpenter. +// +// The application uses the Espressif Development environment with Arduino components. +// This is necessary for the PS2KeyAdvanced class, which I may in future convert to +// use esp-idf library calls rather than Arduino. +// +// The Espressif environment is necessary in order to have more control over the build. +// It is important, for timing, that Core 1 is dedicated to MZ Interface +// logic and Core 0 is used for all other RTOS/Interrupts tasks. +// +// Credits: +// Copyright: (c) 2022 Philip Smart +// +// History: Jan 2022 - Initial write. +// Feb 2022 - Updates and fixes. Added logic to detect PS/2 disconnect and reconnect, +// added 3 alternative maps selected by ALT+F1 (MZ2500), ALT+F2(MZ2000) +// ALT+F3(MZ80B) due to slight differences in the key layout. +// Added framework for wifi so that the interface can enter AP mode to +// acquire local net parameters then connect to local net. Needs the web +// interface writing. +// Mar 2022 - Copied from the mz25key and seperated logic into modules as the sharpkey +// supports multiple host types. +// v1.01 May 2022 - Initial release version. +// v1.02 Jun 2022 - Updated interface to yield Core 1 when no key has been pressed. This is +// necessary to allow Bluetooth and NVS to work (even though BT is pinned +// to core 0, the NVS seems to require both CPU's). +// +// 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 +#include +#include +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_log.h" +#include "Arduino.h" +#include "driver/gpio.h" +#include "soc/timer_group_struct.h" +#include "soc/timer_group_reg.h" +#include "sys/stat.h" +#include "esp_littlefs.h" +#include "PS2KeyAdvanced.h" +#include "sdkconfig.h" +#include "MZ2528.h" + +// Tag for ESP main application logging. +#define MAINTAG "mz25key" + + +// Method to connect and interact with the MZ-2500 keyboard controller. This method is seperate from the MZ-2800 +// as the scan is different and as it is time critical it needs to be per target machine. +// +// The basic requirement is to: +// 1. Detect a falling edge on the RTSN signal +// 2. Read the provided ROW number. +// 3. Lookup the matrix data for given ROW. +// 4. Output data to LS257 Mux. +// 5. Wait for RTSN to return inactive. +// 5. Loop +// +// The hidInterface method is responsible for obtaining scancodes from either a PS/2 or Bluetooth Keyboard and +// creating the corresponding virtual matrix. +// +// NB: As this method holds Core 1 under spinlock, no FreeRTOS or Arduino access +// can be made except for basic I/O ports. The spinlock has to be released for non +// I/O work. +// +// The MZ timing period is ~600ns RTSN Low to High (Latch Row data coming from MZ machine), +// MPX (connected direct to external Quad 2-1 Mux goes high as RTSN goes low, the MPX +// signal is held high for 300ns then goes low for 300ns. The period when RTSN is low is +// when the MZ machine reads the scan row data. The cycle is ~1.2uS repeating, 14 rows +// so ~16.8uS for one full key matrix, The MZ machine if stuck in a tight loop will take +// approx 100uS to scan the matrix so the Gate Arrays are over sampling 6 times. +// +// WARNING: The GPIO's are configurable via menuconfig BUT it is assumed all except RTSNi +// are in the first GPIO bank and RTSNi is in the second GPIO bank. Modify the code +// if RTSNi is set in the first bank or KDB[3:0], KDI4 are in the second bank. +// +IRAM_ATTR void MZ2528::mz25Interface( void * pvParameters ) +{ + // Locals. + bool critical = false; + volatile uint32_t gpioIN; + volatile uint8_t strobeRow = 1; + + // Mask values declared as variables, let the optimiser decide wether they are constants or placed in-memory. + uint32_t rowBitMask = (1 << CONFIG_HOST_KDB3) | (1 << CONFIG_HOST_KDB2) | (1 << CONFIG_HOST_KDB1) | (1 << CONFIG_HOST_KDB0); + uint32_t colBitMask = (1 << CONFIG_HOST_KDO7) | (1 << CONFIG_HOST_KDO6) | (1 << CONFIG_HOST_KDO5) | (1 << CONFIG_HOST_KDO4) | + (1 << CONFIG_HOST_KDO3) | (1 << CONFIG_HOST_KDO2) | (1 << CONFIG_HOST_KDO1) | (1 << CONFIG_HOST_KDO0); + uint32_t KDB3_MASK = (1 << CONFIG_HOST_KDB3); + uint32_t KDB2_MASK = (1 << CONFIG_HOST_KDB2); + uint32_t KDB1_MASK = (1 << CONFIG_HOST_KDB1); + uint32_t KDB0_MASK = (1 << CONFIG_HOST_KDB0); + uint32_t KDI4_MASK = (1 << CONFIG_HOST_KDI4); + uint32_t RTSNI_MASK = (1 << (CONFIG_HOST_RTSNI - 32)); + //uint32_t MPXI_MASK = (1 << CONFIG_HOST_MPXI); + + // Retrieve pointer to object in order to access data. + MZ2528* pThis = (MZ2528*)pvParameters; + + // Initialise the MUTEX which prevents this core from being released to other tasks. + pThis->mzMutex = portMUX_INITIALIZER_UNLOCKED; + + // Setup starting state. + GPIO.out_w1ts = colBitMask; + + // Sign on. + ESP_LOGW(MAINTAG, "Starting mz25Interface thread, colBitMask=%08x, rowBitMask=%08x.", colBitMask, rowBitMask); + + // Permanent loop, just wait for an RTSN strobe, latch the row, lookup matrix and output. + // Timings with Power LED = LED Off to On = 108ns, LED On to Off = 392ns + for(;;) + { + // Suspend processing if there are no new key presses or a suspend request has been made, ie from WiFi interface. + if(pThis->yieldHostInterface == true) + { + // Exit spinlock. + if(critical) portEXIT_CRITICAL(&pThis->mzMutex); + + // Requested to suspend? + if(pThis->suspendRequested()) + { + // Setting the ADC2 ports to output mode is required due to the ESP32 Client Mode which has many issues, one is if the ports are set to input and receiving data + // it fails to start! + // ESP32 WiFi/ADC2 workaround. The ESP32 wont connect to a router in station mode if the ADC2 pins are set to input and have an alternating signal present. + pThis->reconfigADC2Ports(true); + + // All bits to 1, ie. inactive - this is necessary otherwise the host could see a key being held. + GPIO.out_w1ts = colBitMask; + //GPIO.out_w1ts = KDB3_MASK | KDB2_MASK | KDB1_MASK | KDB0_MASK | KDI4_MASK | MPXI_MASK; + + // Yield until the core is released. + pThis->yield(0); + + // Restore the GPIO. + pThis->reconfigADC2Ports(false); + } else + { + // All bits to 1, ie. inactive - this is necessary otherwise the host could see a key being held. The normal state for inputs is all high + // if no key has been pressed. + GPIO.out_w1ts = colBitMask; + + // Yield to allow other tasks to run. + while(pThis->yieldHostInterface == true) vTaskDelay(0); + } + + // Enter spinlock. + portENTER_CRITICAL(&pThis->mzMutex); + critical = true; + } + + // Detect RTSN going high, the MZ will send the required row during this cycle. + if(REG_READ(GPIO_IN1_REG) & RTSNI_MASK) + { + // Read the GPIO ports to get latest Row and KDI4 states. + gpioIN = REG_READ(GPIO_IN_REG); + + // Assemble the required matrix row from the configured bits. + strobeRow = ((gpioIN&KDB3_MASK) >> (CONFIG_HOST_KDB3-3)) | ((gpioIN&KDB2_MASK) >> (CONFIG_HOST_KDB2-2)) | ((gpioIN&KDB1_MASK) >> (CONFIG_HOST_KDB1-1)) | ((gpioIN&KDB0_MASK) >> CONFIG_HOST_KDB0); + + // Clear all KDO bits - clear state = '1' + GPIO.out_w1ts = colBitMask; // Reset all scan data bits to '1', inactive. + + // KDI4 indicates if row data is needed or a single byte ANDing all the keys together, ie. to detect a key press without strobing all rows. + if(gpioIN & KDI4_MASK) + { + // Set all required KDO bits according to keyMatrix, set state = '0'. + GPIO.out_w1tc = pThis->mzControl.keyMatrixAsGPIO[strobeRow]; // Set to '0' active bits. + } else + { + // Set all required KDO bits according to the strobe all value. set state = '0'. + GPIO.out_w1tc = pThis->mzControl.strobeAllAsGPIO; // Set to '0' active bits. + } + + // Wait for RTSN to go low. No lockup guarding as timing is critical also the watchdog is disabled, if RTSN never goes low then the user has probably unplugged the interface! + while((REG_READ(GPIO_IN1_REG) & RTSNI_MASK) && pThis->yieldHostInterface == false); + } + + // Logic to feed the watchdog if needed. Watchdog disabled in menuconfig but if enabled this will need to be used. + //TIMERG0.wdt_wprotect=TIMG_WDT_WKEY_VALUE; // write enable + //TIMERG0.wdt_feed=1; // feed dog + //TIMERG0.wdt_wprotect=0; // write protect + //TIMERG1.wdt_wprotect=TIMG_WDT_WKEY_VALUE; // write enable + //TIMERG1.wdt_feed=1; // feed dog + //TIMERG1.wdt_wprotect=0; // write protect + } +} + +// Method to connect and interact with the MZ-2800 keyboard controller. This method is seperate from the MZ-2500 +// as the scan is different and as it is time critical it needs to be per target machine. +// +// The basic requirement is to: +// 1. Detect a rising edge on the RTSN signal +// 2. Wait at least 200ns before sampling KD4 +// 3. Wait at least 650ns before reading ROW. +// 4. Read the provided ROW number. +// 5. If KD4 = 0 then output logical AND of all columns to LS257 Mux. +// 6. If KD4 = 1 then lookup data for given row and output to LS257 Mux. +// 7. Wait for RTSN to return low. +// 7. Loop +// +// The hidInterface method is responsible for obtaining scancodes from either a PS/2 or Bluetooth Keyboard and +// creating the corresponding virtual matrix. +// +// NB: As this method holds Core 1 under spinlock, no FreeRTOS or Arduino access +// can be made except for basic I/O ports. The spinlock has to be released for non +// I/O work. +// +// The MZ 2800 timing period is 1.78uS RTSN going active high, KD4 changing state 150ns after RTSN goes active, +// ROW number being set 650ns after RTSN goes active. MPX directly controls the LS257 latch so only need to write out +// and 8 bit value prior to RTSN going inactive. +// Normally the keyboard is in STROBE ALL mode. When a key is pressed, it commences a scan and when it arrives at the pressed +// key, RTSN cycle is suspended for varying amounts of time (ie 500us or 19ms) as the controller is looking for debounce and repeat. +// +// WARNING: The GPIO's are configurable via menuconfig BUT it is assumed all except RTSNi +// are in the first GPIO bank and RTSNi is in the second GPIO bank. Modify the code +// if RTSNi is set in the first bank or KDB[3:0], KDI4 are in the second bank. +// +IRAM_ATTR void MZ2528::mz28Interface( void * pvParameters ) +{ + // Locals. + bool critical = false; + volatile uint32_t gpioIN; + volatile uint8_t strobeRow = 1; + + // Mask values declared as variables, let the optimiser decide wether they are constants or placed in-memory. + uint32_t rowBitMask = (1 << CONFIG_HOST_KDB3) | (1 << CONFIG_HOST_KDB2) | (1 << CONFIG_HOST_KDB1) | (1 << CONFIG_HOST_KDB0); + uint32_t colBitMask = (1 << CONFIG_HOST_KDO7) | (1 << CONFIG_HOST_KDO6) | (1 << CONFIG_HOST_KDO5) | (1 << CONFIG_HOST_KDO4) | + (1 << CONFIG_HOST_KDO3) | (1 << CONFIG_HOST_KDO2) | (1 << CONFIG_HOST_KDO1) | (1 << CONFIG_HOST_KDO0); + uint32_t KDB3_MASK = (1 << CONFIG_HOST_KDB3); + uint32_t KDB2_MASK = (1 << CONFIG_HOST_KDB2); + uint32_t KDB1_MASK = (1 << CONFIG_HOST_KDB1); + uint32_t KDB0_MASK = (1 << CONFIG_HOST_KDB0); + uint32_t KDI4_MASK = (1 << CONFIG_HOST_KDI4); + uint32_t RTSNI_MASK = (1 << (CONFIG_HOST_RTSNI - 32)); + //uint32_t MPXI_MASK = (1 << CONFIG_HOST_MPXI); + + // Retrieve pointer to object in order to access data. + MZ2528* pThis = (MZ2528*)pvParameters; + + // Initialise the MUTEX which prevents this core from being released to other tasks. + pThis->mzMutex = portMUX_INITIALIZER_UNLOCKED; + + // Sign on. + ESP_LOGW(MAINTAG, "Starting mz28Interface thread, colBitMask=%08x, rowBitMask=%08x.", colBitMask, rowBitMask); + + // Permanent loop, just wait for an RTSN strobe, latch the row, lookup matrix and output. + for(;;) + { + // Suspend processing if there are no new key presses or a suspend request has been made, ie from WiFi interface. + if(pThis->yieldHostInterface == true) + { + // Exit spinlock. + if(critical) portEXIT_CRITICAL(&pThis->mzMutex); + + // Requested to suspend? + if(pThis->suspendRequested()) + { + // Setting the ADC2 ports to output mode is required due to the ESP32 Client Mode which has many issues, one is if the ports are set to input and receiving data + // it fails to start! + // ESP32 WiFi/ADC2 workaround. The ESP32 wont connect to a router in station mode if the ADC2 pins are set to input and have an alternating signal present. + pThis->reconfigADC2Ports(true); + + // All bits to 1, ie. inactive - this is necessary otherwise the host could see a key being held. + GPIO.out_w1ts = colBitMask; + //GPIO.out_w1ts = KDB3_MASK | KDB2_MASK | KDB1_MASK | KDB0_MASK | KDI4_MASK | MPXI_MASK; + + // Yield until the core is released. + pThis->yield(0); + + // Restore the GPIO. + pThis->reconfigADC2Ports(false); + } else + { + // All bits to 1, ie. inactive - this is necessary otherwise the host could see a key being held. The normal state for inputs is all high + // if no key has been pressed. + GPIO.out_w1ts = colBitMask; + + // Yield to allow other tasks to run. + while(pThis->yieldHostInterface == true) vTaskDelay(0); + } + + // Enter spinlock. + portENTER_CRITICAL(&pThis->mzMutex); + critical = true; + } + + // Detect RTSN going high, the MZ will send the required row during this cycle. + if(REG_READ(GPIO_IN1_REG) & RTSNI_MASK) + { + // Slight delay needed as KD4 lags behind RTSN by approx 200ns and ROW number lags 850ns behind RTSN. + for(volatile uint32_t delay=0; delay < 8; delay++); + + // Read the GPIO ports to get latest Row and KDI4 states. + gpioIN = REG_READ(GPIO_IN_REG); + + // Assemble the required matrix row from the configured bits. + strobeRow = ((gpioIN&KDB3_MASK) >> (CONFIG_HOST_KDB3-3)) | ((gpioIN&KDB2_MASK) >> (CONFIG_HOST_KDB2-2)) | ((gpioIN&KDB1_MASK) >> (CONFIG_HOST_KDB1-1)) | ((gpioIN&KDB0_MASK) >> CONFIG_HOST_KDB0); + + // Clear all KDO bits - clear state = '1' + GPIO.out_w1ts = colBitMask; // Reset all scan data bits to '1', inactive. + + // Another short delay once the row has been assembled as we dont want to change the latch setting too soon, changing to soon leads to ghosting on previous row. + for(volatile uint32_t delay=0; delay < 5; delay++); + + // KDI4 indicates if row data is needed or a single byte ANDing all the keys together, ie. to detect a key press without strobing all rows. + if(gpioIN & KDI4_MASK) + { + // Set all required KDO bits according to keyMatrix, set state = '0'. + GPIO.out_w1tc = pThis->mzControl.keyMatrixAsGPIO[strobeRow]; // Set to '0' active bits. + } else + { + // Set all required KDO bits according to the strobe all value. set state = '0'. + GPIO.out_w1tc = pThis->mzControl.strobeAllAsGPIO; // Set to '0' active bits. + } + + // Wait for RTSN to go low. No lockup guarding as timing is critical also the watchdog is disabled, if RTSN never goes low then the user has probably unplugged the interface! + while((REG_READ(GPIO_IN1_REG) & RTSNI_MASK) && pThis->yieldHostInterface == false); + } + + // Logic to feed the watchdog if needed. Watchdog disabled in menuconfig but if enabled this will need to be used. + //TIMERG0.wdt_wprotect=TIMG_WDT_WKEY_VALUE; // write enable + //TIMERG0.wdt_feed=1; // feed dog + //TIMERG0.wdt_wprotect=0; // write protect + //TIMERG1.wdt_wprotect=TIMG_WDT_WKEY_VALUE; // write enable + //TIMERG1.wdt_feed=1; // feed dog + //TIMERG1.wdt_wprotect=0; // write protect + } +} + +// Method to select keyboard configuration options. When a key sequence is pressed, ie. SHIFT+CTRL+ESC then the fourth simultaneous key is the required option and given to this +// method to act on. Options can be machine model, keyboard map etc. +// +void MZ2528::selectOption(uint8_t optionCode) +{ + // Locals. + // + bool updated = true; + #define SELOPTTAG "selectOption" + + // Simple switch to decode the required option and act on it. + switch(optionCode) + { + // Select a keymap using 1..8 or default (STANDARD) using 0. + case PS2_KEY_1: + this->mzConfig.params.activeKeyboardMap = KEYMAP_UK_WYSE_KB3926; + break; + case PS2_KEY_2: + this->mzConfig.params.activeKeyboardMap = KEYMAP_JAPAN_OADG109; + break; + case PS2_KEY_3: + this->mzConfig.params.activeKeyboardMap = KEYMAP_JAPAN_SANWA_SKBL1; + break; + case PS2_KEY_4: + this->mzConfig.params.activeKeyboardMap = KEYMAP_NOT_ASSIGNED_4; + break; + case PS2_KEY_5: + this->mzConfig.params.activeKeyboardMap = KEYMAP_NOT_ASSIGNED_5; + break; + case PS2_KEY_6: + this->mzConfig.params.activeKeyboardMap = KEYMAP_NOT_ASSIGNED_6; + break; + case PS2_KEY_7: + this->mzConfig.params.activeKeyboardMap = KEYMAP_UK_PERIBOARD_810; + break; + case PS2_KEY_8: + this->mzConfig.params.activeKeyboardMap = KEYMAP_UK_OMOTON_K8508; + break; + case PS2_KEY_0: + this->mzConfig.params.activeKeyboardMap = KEYMAP_STANDARD; + break; + + // Select the active machine model. If we are connected to an MZ-2500 host then it is possible to enable an MZ-2000/MZ-80B mapping. + case PS2_KEY_END: + this->mzConfig.params.activeMachineModel = (this->mzControl.mode2500 ? MZ_2500 : MZ_2800); + break; + case PS2_KEY_DN_ARROW: + if(this->mzControl.mode2500) + { + this->mzConfig.params.activeMachineModel = MZ_2000; + } + break; + case PS2_KEY_PGDN: + if(this->mzControl.mode2500) + { + this->mzConfig.params.activeMachineModel = MZ_80B; + } + break; + + // Unknown option so ignore. + default: + updated = false; + break; + } + + // If an update was made, persist it for power cycles. + // + if(updated) + { + this->mzControl.persistConfig = true; + } + + return; +} + +// Method to refresh the transposed matrix used in the MZ interface. The normal key matrix is transposed to save valuable time +// because even though a core is dedicated to the MZ interface the timing is critical and the ESP-32 doesnt have spare horse power! +// +void MZ2528::updateMirrorMatrix(void) +{ + // Locals. + + // To save time in the MZ Interface, a mirror keyMatrix is built up, 32bit (GPIO Bank 0) wide, with the keyMatrix 8 bit data + // mapped onto the configured pins in the 32bit register. This saves precious time in order to meet the tight 1.2uS cycle. + // + mzControl.noKeyPressed = true; + for(int idx=0; idx < 15; idx++) + { + mzControl.keyMatrixAsGPIO[idx] = (((mzControl.keyMatrix[idx] >> 7) & 0x01) ^ 0x01) << CONFIG_HOST_KDO7 | + (((mzControl.keyMatrix[idx] >> 6) & 0x01) ^ 0x01) << CONFIG_HOST_KDO6 | + (((mzControl.keyMatrix[idx] >> 5) & 0x01) ^ 0x01) << CONFIG_HOST_KDO5 | + (((mzControl.keyMatrix[idx] >> 4) & 0x01) ^ 0x01) << CONFIG_HOST_KDO4 | + (((mzControl.keyMatrix[idx] >> 3) & 0x01) ^ 0x01) << CONFIG_HOST_KDO3 | + (((mzControl.keyMatrix[idx] >> 2) & 0x01) ^ 0x01) << CONFIG_HOST_KDO2 | + (((mzControl.keyMatrix[idx] >> 1) & 0x01) ^ 0x01) << CONFIG_HOST_KDO1 | + (((mzControl.keyMatrix[idx] ) & 0x01) ^ 0x01) << CONFIG_HOST_KDO0 ; + if(mzControl.keyMatrixAsGPIO[idx] != 0x00) mzControl.noKeyPressed = false; + } + + // Re-calculate the Strobe All (KD4 = 1) signal, this indicates if any bit (key) in the matrix is active. + mzControl.strobeAll = 0xFF; + mzControl.strobeAllAsGPIO = 0x00000000; + for(int idx2=0; idx2 < 15; idx2++) + { + mzControl.strobeAll &= mzControl.keyMatrix[idx2]; + } + + // To speed up the mzInterface logic, pre-calculate the strobeAll value as a 32bit GPIO output value. + mzControl.strobeAllAsGPIO |= (((mzControl.strobeAll >> 7) & 0x01) ^ 0x01) << CONFIG_HOST_KDO7 | + (((mzControl.strobeAll >> 6) & 0x01) ^ 0x01) << CONFIG_HOST_KDO6 | + (((mzControl.strobeAll >> 5) & 0x01) ^ 0x01) << CONFIG_HOST_KDO5 | + (((mzControl.strobeAll >> 4) & 0x01) ^ 0x01) << CONFIG_HOST_KDO4 | + (((mzControl.strobeAll >> 3) & 0x01) ^ 0x01) << CONFIG_HOST_KDO3 | + (((mzControl.strobeAll >> 2) & 0x01) ^ 0x01) << CONFIG_HOST_KDO2 | + (((mzControl.strobeAll >> 1) & 0x01) ^ 0x01) << CONFIG_HOST_KDO1 | + (((mzControl.strobeAll ) & 0x01) ^ 0x01) << CONFIG_HOST_KDO0 ; + if(mzControl.strobeAllAsGPIO != 0x00) mzControl.noKeyPressed = false; + return; +} + +// Method to map the PS2 scan code into a key matrix representation which the MZ-2500/2800 is expecting. +// +uint32_t MZ2528::mapKey(uint16_t scanCode) +{ + // Locals. + uint8_t idx; + bool changed = false; + bool matchExact = false; + uint8_t keyCode = (scanCode & 0xFF); + bool mapped = false; + #define MAPKEYTAG "mapKey" + + // Intercept control keys and set state variables. + // + // + if(scanCode & PS2_BREAK) + { + // Any break key clears the option select flag. + this->mzControl.optionSelect = false; + + // Clear any feature LED blinking. + led->setLEDMode(LED::LED_MODE_OFF, LED::LED_DUTY_CYCLE_OFF, 0, 0L, 0L); + } else + { + // Special mapping to allow selection of keyboard options. If the user presses CTRL+SHIFT+ESC then a flag becomes active and should a fourth key be pressed before a BREAK then the fourth key is taken as an option key and processed accordingly. + if(this->mzControl.optionSelect == true && keyCode != PS2_KEY_ESC) { mapped = true; this->mzControl.optionSelect = false; selectOption(keyCode); } + if(keyCode == PS2_KEY_ESC && (scanCode & PS2_CTRL) && (scanCode & PS2_SHIFT)) { mapped = true; this->mzControl.optionSelect = true; } + + // Special mapping to allow selection of keyboard options. If the user presses CTRL+SHIFT+ESC then a flag becomes active and should a fourth key be pressed before a BREAK then the fourth key is taken as an option key and processed accordingly. + if(this->mzControl.optionSelect == true && keyCode != PS2_KEY_ESC) + { + mapped = true; + this->mzControl.optionSelect = false; + selectOption(keyCode); + } + if(keyCode == PS2_KEY_ESC && (scanCode & PS2_CTRL) && (scanCode & PS2_SHIFT) && this->mzControl.optionSelect == false) + { + // Prime flag ready for fourth option key and start LED blinking periodically. + mapped = true; + this->mzControl.optionSelect = true; + led->setLEDMode(LED::LED_MODE_BLINK, LED::LED_DUTY_CYCLE_50, 1, 500L, 500L); + } + } + + // If the key has been mapped as a special key, no further processing. + if(mapped == true) + { + ESP_LOGW(MAPKEYTAG, "Mapped special key\n"); + } else + { + // Loop through the entire conversion table to find a match on this key, if found appy the conversion to the virtual + // switch matrix. + // + for(idx=0, changed=false, matchExact=false; idx < mzControl.kmeRows && (changed == false || (changed == true && matchExact == false)); idx++) + { + // Match key code? Make sure the current machine and keymap match as well. + if(mzControl.kme[idx].ps2KeyCode == (uint8_t)(scanCode&0xFF) && ((mzControl.kme[idx].machine == MZ_ALL) || (mzControl.kme[idx].machine & mzConfig.params.activeMachineModel) != 0) && ((mzControl.kme[idx].keyboardModel & mzConfig.params.activeKeyboardMap) != 0)) + { + // Match Raw, Shift, Function, Control, ALT or ALT-Gr? + if( (((mzControl.kme[idx].ps2Ctrl & PS2CTRL_SHIFT) == 0) && ((mzControl.kme[idx].ps2Ctrl & PS2CTRL_FUNC) == 0) && ((mzControl.kme[idx].ps2Ctrl & PS2CTRL_CTRL) == 0) && ((mzControl.kme[idx].ps2Ctrl & PS2CTRL_ALT) == 0) && ((mzControl.kme[idx].ps2Ctrl & PS2CTRL_ALTGR) == 0)) || + ((scanCode & PS2_SHIFT) && (mzControl.kme[idx].ps2Ctrl & PS2CTRL_SHIFT) != 0) || + ((scanCode & PS2_CTRL) && (mzControl.kme[idx].ps2Ctrl & PS2CTRL_CTRL) != 0) || + ((scanCode & PS2_ALT) && (mzControl.kme[idx].ps2Ctrl & PS2CTRL_ALT) != 0) || + ((scanCode & PS2_ALT_GR) && (mzControl.kme[idx].ps2Ctrl & PS2CTRL_ALTGR) != 0) || + ((scanCode & PS2_GUI) && (mzControl.kme[idx].ps2Ctrl & PS2CTRL_GUI) != 0) || + ((scanCode & PS2_FUNCTION) && (mzControl.kme[idx].ps2Ctrl & PS2CTRL_FUNC) != 0) ) + { + // Exact entry match, data + control key? On an exact match we only process the first key. On a data only match we fall through to include additional data and control key matches to allow for un-mapped key combinations, ie. Japanese characters. + matchExact = (((scanCode & PS2_SHIFT) && (mzControl.kme[idx].ps2Ctrl & PS2CTRL_SHIFT) != 0) || ((scanCode & PS2_SHIFT) == 0 && (mzControl.kme[idx].ps2Ctrl & PS2CTRL_SHIFT) == 0)) && + (((scanCode & PS2_CTRL) && (mzControl.kme[idx].ps2Ctrl & PS2CTRL_CTRL) != 0) || ((scanCode & PS2_CTRL) == 0 && (mzControl.kme[idx].ps2Ctrl & PS2CTRL_CTRL) == 0)) && + (((scanCode & PS2_ALT) && (mzControl.kme[idx].ps2Ctrl & PS2CTRL_ALT) != 0) || ((scanCode & PS2_ALT) == 0 && (mzControl.kme[idx].ps2Ctrl & PS2CTRL_ALT) == 0)) && + (((scanCode & PS2_ALT_GR) && (mzControl.kme[idx].ps2Ctrl & PS2CTRL_ALTGR) != 0) || ((scanCode & PS2_ALT_GR) == 0 && (mzControl.kme[idx].ps2Ctrl & PS2CTRL_ALTGR) == 0)) && + (((scanCode & PS2_GUI) && (mzControl.kme[idx].ps2Ctrl & PS2CTRL_GUI) != 0) || ((scanCode & PS2_GUI) == 0 && (mzControl.kme[idx].ps2Ctrl & PS2CTRL_GUI) == 0)) && + (((scanCode & PS2_FUNCTION) && (mzControl.kme[idx].ps2Ctrl & PS2CTRL_FUNC) != 0) || ((scanCode & PS2_FUNCTION) == 0 && (mzControl.kme[idx].ps2Ctrl & PS2CTRL_FUNC) == 0)) ? true : false; + + // If the exact flag is set, skip as we cannot process this entry when we dont have an exact match. + if(matchExact == false && (mzControl.kme[idx].ps2Ctrl & PS2CTRL_EXACT) != 0) + continue; + + // RELEASE (PS2_BREAK == 1) or PRESS? + if((scanCode & PS2_BREAK)) + { + // Special case for the PAUSE / BREAK key. The underlying logic has been modified to send a BREAK key event immediately + // after a PAUSE make, this is necessary as the Sharp MZ machines require SHIFT (pause) BREAK so the PS/2 CTRL+BREAK wont + // work (unless logic is added to insert a SHIFT, pause, add BREAK). The solution was to generate a BREAK event + // and add a slight delay for the key matrix to register it. + if((scanCode&0x00FF) == PS2_KEY_PAUSE) + { + vTaskDelay(100); + } + + // Loop through all the row/column combinations and if valid, apply to the matrix. + for(int row=0; row < PS2TBL_MZ_MAX_MKROW; row++) + { + // Reset the matrix bit according to the lookup table. 1 = No key, 0 = key in the matrix. + if(mzControl.kme[idx].mkRow[row] != 0xFF) + { + mzControl.keyMatrix[mzControl.kme[idx].mkRow[row]] |= mzControl.kme[idx].mkKey[row]; + changed = true; + } + } + + // Loop through all the key releases associated with this key and reset the relevant matrix bit which was cleared on + // initial keydown. + // + for(int row=0; row < PS2TBL_MZ_MAX_BRKROW; row++) + { + if(mzControl.kme[idx].brkRow[row] != 0xFF) + { + mzControl.keyMatrix[mzControl.kme[idx].brkRow[row]] &= ~mzControl.kme[idx].brkKey[row]; + changed = true; + } + } + } else + { + // Loop through all the key releases associated with this key and clear the relevant matrix bit. + // This is done first so as to avoid false key detection in the MZ logic. + // + for(int row=0; row < PS2TBL_MZ_MAX_BRKROW; row++) + { + if(mzControl.kme[idx].brkRow[row] != 0xFF) + { + mzControl.keyMatrix[mzControl.kme[idx].brkRow[row]] |= mzControl.kme[idx].brkKey[row]; + changed = true; + } + } + + // If a release key has been actioned, update the matrix and insert a slight pause to avoid + // the MZ logic seeing the released keys in combination with the newly pressed keys. + if(changed) + { + updateMirrorMatrix(); + changed = false; + vTaskDelay(10); + } + + // Loop through all the row/column combinations and if valid, apply to the matrix. + for(int row=0; row < PS2TBL_MZ_MAX_MKROW; row++) + { + // Set the matrix bit according to the lookup table. 1 = No key, 0 = key in the matrix. + if(mzControl.kme[idx].mkRow[row] != 0xFF) + { + mzControl.keyMatrix[mzControl.kme[idx].mkRow[row]] &= ~mzControl.kme[idx].mkKey[row]; + changed = true; + } + } + } + + // Only spend time updating signals if an actual change occurred. Some keys arent valid so no change will be effected. + if(changed) + { + updateMirrorMatrix(); + } + } // match key or a special function + } // match key code + } // for loop + } // mapped + + // Return flag to indicate if a match occurred and the matrix updated. + return((uint32_t)changed); +} + +// Primary HID thread, running on Core 0. +// This thread is responsible for receiving PS/2 or BT scan codes and mapping them to an MZ-2500/2800 keyboard matrix. +// +IRAM_ATTR void MZ2528::hidInterface( void * pvParameters ) +{ + // Locals. + uint16_t scanCode = 0x0000; + + // Map the instantiating object so we can access its methods and data. + MZ2528* pThis = (MZ2528*)pvParameters; + + // Thread never exits, just polls the keyboard and updates the matrix. + while(1) + { + // Check stack space, report if it is getting low. + if(uxTaskGetStackHighWaterMark(NULL) < 1024) + { + ESP_LOGW(MAPKEYTAG, "THREAD STACK SPACE(%d)",uxTaskGetStackHighWaterMark(NULL)); + } + + // Check for PS/2 keyboard scan codes. + while((scanCode = pThis->hid->read()) != 0) + { + // Scan Code Breakdown: + // Define name bit description + // PS2_BREAK 15 1 = Break key code + // (MSB) 0 = Make Key code + // PS2_SHIFT 14 1 = Shift key pressed as well (either side) + // 0 = No shift key + // PS2_CTRL 13 1 = Ctrl key pressed as well (either side) + // 0 = No Ctrl key + // PS2_CAPS 12 1 = Caps Lock ON + // 0 = Caps lock OFF + // PS2_ALT 11 1 = Left Alt key pressed as well + // 0 = No Left Alt key + // PS2_ALT_GR 10 1 = Right Alt (Alt GR) key pressed as well + // 0 = No Right Alt key + // PS2_GUI 9 1 = GUI key pressed as well (either) + // 0 = No GUI key + // PS2_FUNCTION 8 1 = FUNCTION key non-printable character (plus space, tab, enter) + // 0 = standard character key + // 7-0 PS/2 Key code. + // + // BREAK code means all keys released so clear out flags and send update. + ESP_LOGW(MAPKEYTAG, "SCANCODE:%04x",scanCode); + + // Update the virtual matrix with the new key value. + pThis->mapKey(scanCode); + + // Toggle LED to indicate data flow. + if((scanCode & PS2_BREAK) == 0) + pThis->led->setLEDMode(LED::LED_MODE_BLINK_ONESHOT, LED::LED_DUTY_CYCLE_10, 1, 100L, 0L); + } + + // If all keys have been releaased or a suspend is requested, set the yieldInterface flag. This flag is intentional due to time + // being critical in the host interface thread. then after a short count, + if(pThis->mzControl.noKeyPressed == true || pThis->suspendRequested()) + { + vTaskDelay(10); + pThis->yieldHostInterface = true; + } + else { + pThis->yieldHostInterface = false; + } + + // NVS writes require both CPU cores to be free, so we any config write must wait until the host interface is idle. + if(pThis->yieldHostInterface == true && pThis->mzControl.persistConfig == true) + { + if(pThis->nvs->persistData(pThis->getClassName(__PRETTY_FUNCTION__), &pThis->mzConfig, sizeof(t_mzConfig)) == false) + { + ESP_LOGW(SELOPTTAG, "Persisting MZ-2500/MZ-2800 configuration data failed, updates will not persist in future power cycles."); + pThis->led->setLEDMode(LED::LED_MODE_BLINK_ONESHOT, LED::LED_DUTY_CYCLE_10, 200, 1000L, 0L); + } else + // Few other updates so make a commit here to ensure data is flushed and written. + if(pThis->nvs->commitData() == false) + { + ESP_LOGW(SELOPTTAG, "NVS Commit writes operation failed, some previous writes may not persist in future power cycles."); + pThis->led->setLEDMode(LED::LED_MODE_BLINK_ONESHOT, LED::LED_DUTY_CYCLE_10, 200, 500L, 0L); + } + + // Clear flag so we dont persist in a loop. + pThis->mzControl.persistConfig = false; + } + + // Yield if the suspend flag is set. + pThis->yield(10); + } +} + +// A method to load the keyboard mapping table into memory for use in the interface mapping logic. If no persistence file exists or an error reading persistence occurs, the keymap +// uses the internal static default. If no persistence file exists and attempt is made to create it with a copy of the inbuilt static map so that future operations all +// work with persistence such that modifications can be made. +// +bool MZ2528::loadKeyMap(void) +{ + // Locals. + // + bool result = false; + int fileRows = 0; + struct stat keyMapFileNameStat; + + // See if the file exists, if it does, get size so we can compute number of mapping rows. + if(stat(mzControl.keyMapFileName.c_str(), &keyMapFileNameStat) == -1) + { + ESP_LOGW(MAINTAG, "No keymap file, using inbuilt definitions."); + } else + { + // Get number of rows in the file. + fileRows = keyMapFileNameStat.st_size/sizeof(t_keyMapEntry); + + // Subsequent reloads, delete memory prior to building new map, primarily to conserve precious resources rather than trying the memory allocation trying to realloc and then having to copy. + if(mzControl.kme != NULL && mzControl.kme != PS2toMZ.kme) + { + delete mzControl.kme; + mzControl.kme = NULL; + } + + // Allocate memory for the new keymap table. + mzControl.kme = new t_keyMapEntry[fileRows]; + if(mzControl.kme == NULL) + { + ESP_LOGW(MAINTAG, "Failed to allocate memory for keyboard map, fallback to inbuilt!"); + } else + { + // Open the keymap extension file for binary reading to add data to our map table. + std::fstream keyFileIn(mzControl.keyMapFileName.c_str(), std::ios::in | std::ios::binary); + + int idx=0; + while(keyFileIn.good()) + { + keyFileIn.read((char *)&mzControl.kme[idx], sizeof(t_keyMapEntry)); + if(keyFileIn.good()) + { + idx++; + } + } + // Any errors, we wind back and use the inbuilt mapping table. + if(keyFileIn.bad()) + { + keyFileIn.close(); + ESP_LOGW(MAINTAG, "Failed to read data from keymap extension file:%s, fallback to inbuilt!", mzControl.keyMapFileName.c_str()); + } else + { + // No longer need the file. + keyFileIn.close(); + + // Max rows in the KME table. + mzControl.kmeRows = fileRows; + + // Good to go, map ready for use with the interface. + result = true; + } + } + } + + // Any failures, free up memory and use the inbuilt mapping table. + if(result == false) + { + if(mzControl.kme != NULL && mzControl.kme != PS2toMZ.kme) + { + delete mzControl.kme; + mzControl.kme = NULL; + } + + // No point allocating memory if no extensions exist or an error occurs, just point to the static table. + mzControl.kme = PS2toMZ.kme; + mzControl.kmeRows = PS2TBL_MZ_MAXROWS; + + // Persist the data so that next load comes from file. + saveKeyMap(); + } + + // Return code. Either memory map was successfully loaded, true or failed, false. + return(result); +} + +// Method to save the current keymap out to an extension file. +// +bool MZ2528::saveKeyMap(void) +{ + // Locals. + // + bool result = false; + int idx = 0; + + // Has a map been defined? Cannot save unless loadKeyMap has been called which sets mzControl.kme to point to the internal keymap or a new memory resident map. + // + if(mzControl.kme == NULL) + { + ESP_LOGW(MAINTAG, "KeyMap hasnt yet been defined, need to call loadKeyMap."); + } else + { + // Request mutex from NVS to prevent it from accessing the NVS - LittleFS is based on NVS. + if(nvs->takeMutex() == true) + { + // Open file for binary writing, trunc specified to clear out the file, we arent appending. + std::fstream keyFileOut(mzControl.keyMapFileName.c_str(), std::ios::out | std::ios::binary | std::ios::trunc); + + // Loop whilst no errors and data rows still not written. + while(keyFileOut.good() && idx < mzControl.kmeRows) + { + keyFileOut.write((char *)&mzControl.kme[idx], sizeof(t_keyMapEntry)); + idx++; + } + if(keyFileOut.bad()) + { + ESP_LOGW(MAINTAG, "Failed to write data from the keymap to file:%s, deleting as state is unknown!", mzControl.keyMapFileName.c_str()); + keyFileOut.close(); + std::remove(mzControl.keyMapFileName.c_str()); + } else + { + // Success. + keyFileOut.close(); + result = true; + } + + // Relinquish mutex now write is complete. + nvs->giveMutex(); + } + } + + // Return code. Either memory map was successfully saved, true or failed, false. + return(result); +} + +// Public method to open a keymap file for data upload. +// This method opens the file and makes any validation checks as necessary. +// +bool MZ2528::createKeyMapFile(std::fstream &outFile) +{ + // Locals. + // + bool result = true; + std::string fileName; + + // Attempt to open a temporary keymap file for writing. + // + fileName = mzControl.keyMapFileName; + replaceExt(fileName, "tmp"); + outFile.open(fileName.c_str(), std::ios::out | std::ios::binary | std::ios::trunc); + if(outFile.bad()) + { + result = false; + } + + // Send result. + return(result); +} + +// Public method to validate and store data provided by caller into an open file created by 'createKeyMapFile'. +// +bool MZ2528::storeDataToKeyMapFile(std::fstream &outFile, char *data, int size) +{ + // Locals. + // + bool result = true; + + // Check that the file is still writeable then add data. + if(outFile.good()) + { + outFile.write(data, size); + } + if(outFile.bad()) + { + result = false; + } + + // Send result. + return(result); +} + +// Polymorphic alternative to take a vector of bytes for writing to the output file. +// +bool MZ2528::storeDataToKeyMapFile(std::fstream & outFile, std::vector& dataArray) +{ + // Locals. + // + bool result = true; + char data[1]; + + // Check that the file is still writeable then add data. Not best for performace but ease of use and minimum memory. + if(outFile.good()) + { + for(std::size_t idx = 0; idx < dataArray.size(); idx++) + { + data[0] = (char)dataArray[idx]; + outFile.write((char *)&data, 1); + } + } + if(outFile.bad()) + { + result = false; + } + + // Send result. + return(result); +} + +// Public method to close and commit a data file, created by 'createKeyMapFile' and populated by 'storeDataToKeyMapFile'. +// This involves renaming the original keymap file, closing the new file and renaming it to the original keymap filename. +// +bool MZ2528::closeAndCommitKeyMapFile(std::fstream &outFile, bool cleanupOnly) +{ + // Locals. + // + bool result = true; + std::string fileName; + + // Check the file is still accessible and close. + // + outFile.close(); + if(!cleanupOnly) + { + if(outFile.good()) + { + // Rename the original file. + fileName = mzControl.keyMapFileName; + replaceExt(fileName, "bak"); + // Remove old backup file. Dont worry if it is not there! + std::remove(fileName.c_str()); + replaceExt(fileName, "tmp"); + // Rename new file to active. + if(std::rename(fileName.c_str(), mzControl.keyMapFileName.c_str()) != 0) + { + result = false; + } + } else + { + result = false; + } + } + + // Send result. + return(result); +} + +// Method to return the keymap column names as header strings. +// +void MZ2528::getKeyMapHeaders(std::vector& headerList) +{ + // Add the names. + // + headerList.push_back(PS2TBL_PS2KEYCODE_NAME); + headerList.push_back(PS2TBL_PS2CTRL_NAME); + headerList.push_back(PS2TBL_KEYBOARDMODEL_NAME); + headerList.push_back(PS2TBL_MACHINE_NAME); + headerList.push_back(PS2TBL_MZ_MK_ROW1_NAME); + headerList.push_back(PS2TBL_MZ_MK_KEY1_NAME); + headerList.push_back(PS2TBL_MZ_MK_ROW2_NAME); + headerList.push_back(PS2TBL_MZ_MK_KEY2_NAME); + headerList.push_back(PS2TBL_MZ_MK_ROW3_NAME); + headerList.push_back(PS2TBL_MZ_MK_KEY3_NAME); + headerList.push_back(PS2TBL_MZ_BRK_ROW1_NAME); + headerList.push_back(PS2TBL_MZ_BRK_KEY1_NAME); + headerList.push_back(PS2TBL_MZ_BRK_ROW2_NAME); + headerList.push_back(PS2TBL_MZ_BRK_KEY2_NAME); + + return; +} + +// A method to return the Type of data for a given column in the KeyMap table. +// +void MZ2528::getKeyMapTypes(std::vector& typeList) +{ + // Add the types. + // + typeList.push_back(PS2TBL_PS2KEYCODE_TYPE); + typeList.push_back(PS2TBL_PS2CTRL_TYPE); + typeList.push_back(PS2TBL_KEYBOARDMODEL_TYPE); + typeList.push_back(PS2TBL_MACHINE_TYPE); + typeList.push_back(PS2TBL_MZ_MK_ROW1_TYPE); + typeList.push_back(PS2TBL_MZ_MK_KEY1_TYPE); + typeList.push_back(PS2TBL_MZ_MK_ROW2_TYPE); + typeList.push_back(PS2TBL_MZ_MK_KEY2_TYPE); + typeList.push_back(PS2TBL_MZ_MK_ROW3_TYPE); + typeList.push_back(PS2TBL_MZ_MK_KEY3_TYPE); + typeList.push_back(PS2TBL_MZ_BRK_ROW1_TYPE); + typeList.push_back(PS2TBL_MZ_BRK_KEY1_TYPE); + typeList.push_back(PS2TBL_MZ_BRK_ROW2_TYPE); + typeList.push_back(PS2TBL_MZ_BRK_KEY2_TYPE); + + return; +} + +// Method to return a list of key:value entries for a given keymap column. This represents the +// feature which can be selected and the value it uses. Features can be combined by ORing the values +// together. +bool MZ2528::getKeyMapSelectList(std::vector>& selectList, std::string option) +{ + // Locals. + // + bool result = true; + + // Build up a map, depending on the list required, of name to value. This list can then be used + // by a user front end to select an option based on a name and return its value. + if(option.compare(PS2TBL_PS2CTRL_TYPE) == 0) + { + selectList.push_back(std::make_pair(PS2TBL_PS2CTRL_SEL_SHIFT, PS2CTRL_SHIFT)); + selectList.push_back(std::make_pair(PS2TBL_PS2CTRL_SEL_CTRL, PS2CTRL_CTRL)); + selectList.push_back(std::make_pair(PS2TBL_PS2CTRL_SEL_CAPS, PS2CTRL_CAPS)); + selectList.push_back(std::make_pair(PS2TBL_PS2CTRL_SEL_ALT, PS2CTRL_ALT)); + selectList.push_back(std::make_pair(PS2TBL_PS2CTRL_SEL_ALTGR, PS2CTRL_ALTGR)); + selectList.push_back(std::make_pair(PS2TBL_PS2CTRL_SEL_GUI, PS2CTRL_GUI)); + selectList.push_back(std::make_pair(PS2TBL_PS2CTRL_SEL_FUNC, PS2CTRL_FUNC)); + selectList.push_back(std::make_pair(PS2TBL_PS2CTRL_SEL_EXACT, PS2CTRL_EXACT)); + } + else if(option.compare(PS2TBL_KEYBOARDMODEL_TYPE) == 0) + { + selectList.push_back(std::make_pair(KEYMAP_SEL_STANDARD, KEYMAP_STANDARD)); + selectList.push_back(std::make_pair(KEYMAP_SEL_UK_WYSE_KB3926, KEYMAP_UK_WYSE_KB3926)); + selectList.push_back(std::make_pair(KEYMAP_SEL_JAPAN_OADG109, KEYMAP_JAPAN_OADG109)); + selectList.push_back(std::make_pair(KEYMAP_SEL_JAPAN_SANWA_SKBL1, KEYMAP_JAPAN_SANWA_SKBL1)); + selectList.push_back(std::make_pair(KEYMAP_SEL_NOT_ASSIGNED_4, KEYMAP_NOT_ASSIGNED_4)); + selectList.push_back(std::make_pair(KEYMAP_SEL_NOT_ASSIGNED_5, KEYMAP_NOT_ASSIGNED_5)); + selectList.push_back(std::make_pair(KEYMAP_SEL_NOT_ASSIGNED_6, KEYMAP_NOT_ASSIGNED_6)); + selectList.push_back(std::make_pair(KEYMAP_SEL_UK_PERIBOARD_810, KEYMAP_UK_PERIBOARD_810)); + selectList.push_back(std::make_pair(KEYMAP_SEL_UK_OMOTON_K8508, KEYMAP_UK_OMOTON_K8508)); + } + else if(option.compare(PS2TBL_MACHINE_TYPE) == 0) + { + selectList.push_back(std::make_pair(MZ2528_SEL_ALL, MZ_ALL)); + selectList.push_back(std::make_pair(MZ2528_SEL_MZ_80B, MZ_80B)); + selectList.push_back(std::make_pair(MZ2528_SEL_MZ_2000, MZ_2000)); + selectList.push_back(std::make_pair(MZ2528_SEL_MZ_2500, MZ_2500)); + selectList.push_back(std::make_pair(MZ2528_SEL_MZ_2800, MZ_2800)); + } + else if(option.compare(PS2TBL_MZ_MK_ROW1_TYPE) == 0 || option.compare(PS2TBL_MZ_MK_ROW2_TYPE) == 0 || option.compare(PS2TBL_MZ_MK_ROW3_TYPE) == 0 || option.compare(PS2TBL_MZ_BRK_ROW1_TYPE) == 0 || option.compare(PS2TBL_MZ_BRK_ROW2_TYPE) == 0) + { + for(int idx=0; idx < 15; idx++) + { + std::string rowStr = "Strobe_Row_" + std::to_string(idx); + selectList.push_back(std::make_pair(rowStr.c_str(), idx)); + } + selectList.push_back(std::make_pair("Disabled", 255)); + } else + { + // Not found! + result = false; + } + + // Return result, false if the option not found, true otherwise. + // + return(result); +} + + +// Method to read the Keymap array, 1 row at a time and return it to the caller. +// +bool MZ2528::getKeyMapData(std::vector& dataArray, int *row, bool start) +{ + // Locals. + // + bool result = false; + + // If start flag is set, set row to 0. + if(start == true) + { + (*row) = 0; + } + + // Bound check and if still valid, push data onto the vector. + if((*row) >= mzControl.kmeRows) + { + result = true; + } else + { + dataArray.push_back(mzControl.kme[*row].ps2KeyCode); + dataArray.push_back(mzControl.kme[*row].ps2Ctrl); + dataArray.push_back(mzControl.kme[*row].keyboardModel); + dataArray.push_back(mzControl.kme[*row].machine); + for(int idx=0; idx < PS2TBL_MZ_MAX_MKROW; idx++) + { + dataArray.push_back(mzControl.kme[*row].mkRow[idx]); + dataArray.push_back(mzControl.kme[*row].mkKey[idx]); + } + for(int idx=0; idx < PS2TBL_MZ_MAX_BRKROW; idx++) + { + dataArray.push_back(mzControl.kme[*row].brkRow[idx]); + dataArray.push_back(mzControl.kme[*row].brkKey[idx]); + } + (*row) = (*row) + 1; + } + + // True if no more rows, false if additional rows can be read. + return(result); +} + + +// Initialisation routine. Start two threads, one to handle the incoming PS/2 keyboard data and map it, the second to handle the host interface. +void MZ2528::init(uint32_t ifMode, NVS *hdlNVS, LED *hdlLED, HID *hdlHID) +{ + // Initialise the basic components. + init(hdlNVS, hdlHID); + + // Mode is important for configuring hardware and launching correct interface. + mzControl.mode2500 = (ifMode == 2500 ? true : false); + + // Invoke the prototype init which initialises common variables and devices shared by all subclass. + KeyInterface::init(getClassName(__PRETTY_FUNCTION__), hdlNVS, hdlLED, hdlHID, ifMode); + + // Create a task pinned to core 1 which will fulfill the MZ-2500/2800 interface. This task has the highest priority + // and it will also hold spinlock and manipulate the watchdog to ensure a scan cycle timing can be met. This means + // all other tasks running on Core 1 will suspend. The PS/2 controller will be serviced with core 0. + // + // Core 1 - MZ Interface + if(mzControl.mode2500) + { + ESP_LOGW(MAINTAG, "Starting mz25if thread..."); + ::xTaskCreatePinnedToCore(&this->mz25Interface, "mz25if", 4096, this, 25, &this->TaskHostIF, 1); + } else + { + ESP_LOGW(MAINTAG, "Starting mz28if thread..."); + ::xTaskCreatePinnedToCore(&this->mz28Interface, "mz28if", 2048, this, (configMAX_PRIORITIES - 1), &this->TaskHostIF, 1); + } + vTaskDelay(1500); + + // Core 0 - Application + // HID Interface handler thread. + ESP_LOGW(MAINTAG, "Starting hidInterface thread..."); + ::xTaskCreatePinnedToCore(&this->hidInterface, "hidIf", 4096, this, 0, &this->TaskHIDIF, 0); + vTaskDelay(1500); +} + +// Initialisation routine without hardware. +void MZ2528::init(NVS *hdlNVS, HID *hdlHID) +{ + // Initialise control variables. + mzControl.strobeAll = 0xFF; + mzControl.strobeAllAsGPIO = 0x00000000; + for(int idx=0; idx < NUMELEM(mzControl.keyMatrix); idx++) { mzControl.keyMatrix[idx] = 0xFF; } + for(int idx=0; idx < NUMELEM(mzControl.keyMatrixAsGPIO); idx++) { mzControl.keyMatrixAsGPIO[idx] = 0x00000000; } + mzControl.mode2500 = true; + mzControl.optionSelect = false; + mzControl.keyMapFileName = mzControl.fsPath.append("/").append(MZ2528IF_KEYMAP_FILE); + mzControl.kmeRows = 0; + mzControl.kme = NULL; + mzControl.noKeyPressed = true; + mzControl.persistConfig = false; + yieldHostInterface = true; + + // Invoke the prototype init which initialises common variables and devices shared by all subclass. + KeyInterface::init(getClassName(__PRETTY_FUNCTION__), hdlNVS, hdlHID); + + // Load the keyboard mapping table into memory. If the file doesnt exist, create it. + loadKeyMap(); + + // Retrieve configuration, if it doesnt exist, set defaults. + // + if(nvs->retrieveData(getClassName(__PRETTY_FUNCTION__), &this->mzConfig, sizeof(t_mzConfig)) == false) + { + ESP_LOGW(MAINTAG, "MZ-2500/MZ-2800 configuration set to default, no valid config in NVS found."); + mzConfig.params.activeKeyboardMap = KEYMAP_STANDARD; + mzConfig.params.activeMachineModel = (mzControl.mode2500 ? MZ_2500 : MZ_2800); + + // Persist the data for next time. + if(nvs->persistData(getClassName(__PRETTY_FUNCTION__), &this->mzConfig, sizeof(t_mzConfig)) == false) + { + ESP_LOGW(MAINTAG, "Persisting Default MZ-2500/MZ-2800 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(MAINTAG, "NVS Commit writes operation failed, some previous writes may not persist in future power cycles."); + } + } +} + +// Constructor, basically initialise the Singleton interface and let the threads loose. +MZ2528::MZ2528(uint32_t ifMode, NVS *hdlNVS, LED *hdlLED, HID *hdlHID, const char* fsPath) +{ + // Setup the default path on the underlying filesystem. + this->mzControl.fsPath = fsPath; + + // Initialise the interface. + init(ifMode, hdlNVS, hdlLED, hdlHID); +} + +// Constructor, initialise the Singleton interface without hardware. +MZ2528::MZ2528(NVS *hdlNVS, HID *hdlHID, const char* fsPath) +{ + // Setup the default path on the underlying filesystem. + this->mzControl.fsPath = fsPath; + + // Initialise the interface. + init(hdlNVS, hdlHID); +} + +// Constructor, used for version reporting so no hardware is initialised. +MZ2528::MZ2528(void) +{ + return; +} + +// Destructor - only ever called when the class is used for version reporting. +MZ2528::~MZ2528(void) +{ + return; +} diff --git a/main/MZ5665.cpp b/main/MZ5665.cpp deleted file mode 120000 index 08ec66d..0000000 --- a/main/MZ5665.cpp +++ /dev/null @@ -1 +0,0 @@ -../../sharpkey/main/MZ5665.cpp \ No newline at end of file diff --git a/main/MZ5665.cpp b/main/MZ5665.cpp new file mode 100644 index 0000000..f1677b3 --- /dev/null +++ b/main/MZ5665.cpp @@ -0,0 +1,1052 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// Name: MZ5665.cpp +// Created: Apr 2022 +// Version: v1.0 +// Author(s): Philip Smart +// Description: HID (PS/2 or BT keyboard) to Sharp MZ-5600/MZ-6500 series Interface logic. +// This source file contains the singleton class containing logic to obtain +// PS/2 or BT scan codes, map them into Sharp MZ-5600/MZ-6500 keys and transmit the key +// to the MZ-5600/MZ-6500 host. +// +// The class uses a modified version of the PS2KeyAdvanced +// https://github.com/techpaul/PS2KeyAdvanced class from Paul Carpenter. +// +// The whole application of which this class is a member, uses the Espressif Development +// environment with Arduino components. This is necessary for the PS2KeyAdvanced class, +// which I may in future convert to use esp-idf library calls rather than Arduino. +// +// Credits: +// Copyright: (c) 2022 Philip Smart +// +// History: Apr 2022 - Initial framework, waiting on arrival of real machine to progress further. +// v1.01 Jun 2022 - Updates to reflect changes realised in other modules due to addition of +// bluetooth and suspend logic due to NVS issues using both cores. +// +// 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 +#include +#include +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/queue.h" +#include "driver/gpio.h" +#include "esp_log.h" +#include "soc/timer_group_struct.h" +#include "soc/timer_group_reg.h" +#include "driver/timer.h" +#include "sys/stat.h" +#include "esp_littlefs.h" +#include "PS2KeyAdvanced.h" +#include "sdkconfig.h" +#include "MZ5665.h" + +// Tag for ESP main application logging. +#define MAINTAG "mz5665key" + +// FreeRTOS Queue handle to pass messages from the HID Keyboard Mapper into the MZ5665 transmission logic. +static QueueHandle_t xmitQueue; + +// MZ-5600/MZ-6500 Protocol +// ------------------------ +// + + +// Function to push a keycode onto the key queue ready for transmission. +// +void MZ5665::pushKeyToQueue(uint32_t key) +{ + // Locals. + t_xmitQueueMessage xmitMsg; + #define PUSHKEYTAG "pushKeyToQueue" + + xmitMsg.keyCode = key; + if( xQueueSend(xmitQueue, (void *)&xmitMsg, 10) != pdPASS) + { + ESP_LOGW(PUSHKEYTAG, "Failed to put scancode:%04x into xmitQueue", key); + } + return; +} + +// Method to realise the MZ-5600/MZ-6500 4 wire serial protocol in order to transmit key presses to the +// MZ-5600/MZ-6500. +// This method uses Core 1 and it will hold it in a spinlock as necessary to ensure accurate timing. +// A key is passed into the method via the FreeRTOS Queue handle xmitQueue. +IRAM_ATTR void MZ5665::mzInterface( void * pvParameters ) +{ + // Locals. + //t_xmitQueueMessage rcvMsg; + + // Mask values declared as variables, let the optimiser decide wether they are constants or placed in-memory. +// uint32_t X1DATA_MASK = (1 << CONFIG_HOST_KDO0); + //uint64_t delayTimer = 0LL; + //uint64_t curTime = 0LL; + //bool bitStart = true; + //uint32_t bitCount = 0; + //enum XMITSTATE { + // FSM_IDLE = 0, + // FSM_STARTXMIT = 1, + // FSM_HEADER = 2, + // FSM_START = 3, + // FSM_DATA = 4, + // FSM_STOP = 5, + // FSM_ENDXMIT = 6 + //} state = FSM_IDLE; + + // Retrieve pointer to object in order to access data. + MZ5665* pThis = (MZ5665*)pvParameters; + + // Initialise the MUTEX which prevents this core from being released to other tasks. + pThis->mzMutex = portMUX_INITIALIZER_UNLOCKED; + + // Initial delay needed because the xQueue will assert probably on a suspended task ALL if delay not inserted! + vTaskDelay(1000); + + // Sign on. + ESP_LOGW(MAINTAG, "Starting MZ-6500 thread."); + +// // X1 data out default state is high. +// GPIO.out_w1ts = X1DATA_MASK; +// +// // Configure a timer to be used for X1 protocol spacing with 1uS resolution. The default clock source is the APB running at 80MHz. +// timer_config_t timerConfig = { +// .alarm_en = TIMER_ALARM_DIS, // No alarm, were not using interrupts as we are in a dedicated thread. +// .counter_en = TIMER_PAUSE, // Timer paused until required. +// .intr_type = TIMER_INTR_LEVEL, // No interrupts used. +// .counter_dir = TIMER_COUNT_UP, // Timing a fixed period. +// .auto_reload = TIMER_AUTORELOAD_DIS, // No need for auto reload, fixed time period. +// .divider = 80 // 1Mhz operation giving 1uS resolution. +// }; +// ESP_ERROR_CHECK(timer_init(TIMER_GROUP_0, TIMER_0, &timerConfig)); +// ESP_ERROR_CHECK(timer_set_counter_value(TIMER_GROUP_0, TIMER_0, 0)); +// +// // Permanent loop, wait for an incoming message on the key to send queue, read it then transmit to the X1, repeat! +// for(;;) +// { +// // Get the current timer value, only run the FSM when the timer is idle. +// timer_get_counter_value(TIMER_GROUP_0, TIMER_0, &curTime); +// if(curTime >= delayTimer) +// { +// // Ensure the timer is stopped. +// timer_pause(TIMER_GROUP_0, TIMER_0); +// delayTimer = 0LL; +// +// // Finite state machine to retrieve a key for transmission then serialise it according to the X1 protocol. +// switch(state) +// { +// case FSM_IDLE: +// // Yield if the suspend flag is set. +// pThis->yield(0); +// +// // Check stack space, report if it is getting low. +// if(uxTaskGetStackHighWaterMark(NULL) < 1024) +// { +// ESP_LOGW(MAINTAG, "THREAD STACK SPACE(%d)\n",uxTaskGetStackHighWaterMark(NULL)); +// } +// +// // If a new message arrives, start the serialiser to send it to the X1. +// if(xQueueReceive(xmitQueue, (void *)&rcvMsg, 0) == pdTRUE) +// { +// ESP_LOGW(MAINTAG, "Received:%08x, %d", rcvMsg.keyCode, rcvMsg.modeB); +// state = FSM_STARTXMIT; +// +// // Create, initialise and hold a spinlock so the current core is bound to this one method. +// portENTER_CRITICAL(&pThis->mzMutex); +// } +// break; +// +// case FSM_STARTXMIT: +// // Ensure all variables and states correct before entering serialisation. +// bitStart = true; +// GPIO.out_w1ts = X1DATA_MASK; +// state = FSM_HEADER; +// if(rcvMsg.modeB) +// bitCount = 24; +// else +// bitCount = 16; +// break; +// +// case FSM_HEADER: +// if(bitStart) +// { +// // Send out the header by bringing X1DATA low for 1000us then high for 700uS. +// GPIO.out_w1tc = X1DATA_MASK; +// delayTimer = pThis->mzCtrl.modeB ? 400LL : 1000LL; +// } else +// { +// // Bring high for 700us. +// GPIO.out_w1ts = X1DATA_MASK; +// delayTimer = pThis->mzCtrl.modeB ? 200LL : 700LL; +// state = FSM_DATA; // Jump past the Start Bit, I think the header is the actual start bit as there is an error in the X1 Center specs. +// } +// bitStart = !bitStart; +// break; +// +// // The original X1 Center specification shows a start bit but this doesnt seem necessary, in fact it is interpreted as a data bit, hence the +// // FSM jumps this state. +// case FSM_START: +// if(bitStart) +// { +// // Send out the start bit by bringing X1DATA low for 250us then high for 750uS. +// GPIO.out_w1tc = X1DATA_MASK; +// delayTimer = pThis->mzCtrl.modeB ? 250LL : 250LL; +// } else +// { +// // Bring high for 750us. +// GPIO.out_w1ts = X1DATA_MASK; +// delayTimer = pThis->mzCtrl.modeB ? 250LL : 750LL; +// state = FSM_DATA; +// } +// bitStart = !bitStart; +// break; +// +// case FSM_DATA: +// if(bitCount > 0) +// { +// if(bitStart) +// { +// // Send out the data bit by bringing X1DATA low for 250us then high for 1750uS when bit = 1 else 750uS when bit = 0. +// GPIO.out_w1tc = X1DATA_MASK; +// delayTimer = 250LL; +// delayTimer = pThis->mzCtrl.modeB ? 250LL : 250LL; +// } else +// { +// // Bring X1DATA high... +// GPIO.out_w1ts = X1DATA_MASK; +// +// // ... Mode A 1750us as bit = 1, mode B 750uS. +// if((rcvMsg.modeB && rcvMsg.keyCode & 0x800000) || (!rcvMsg.modeB && rcvMsg.keyCode & 0x8000)) +// { +// delayTimer = pThis->mzCtrl.modeB ? 750LL : 1750LL; +// } else +// // ... Mode A 750us as bit = 0, mode B 250uS. +// { +// delayTimer = pThis->mzCtrl.modeB ? 250LL : 750LL; +// } +// rcvMsg.keyCode = (rcvMsg.keyCode << 1); +// bitCount--; +// } +// bitStart = !bitStart; +// } else +// { +// state = FSM_STOP; +// } +// break; +// +// case FSM_STOP: +// if(bitStart) +// { +// // Send out the stop bit, same in Mode A and B, by bringing X1DATA low for 250us then high for 250uS. +// GPIO.out_w1tc = X1DATA_MASK; +// delayTimer = 250LL; +// delayTimer = pThis->mzCtrl.modeB ? 250LL : 250LL; +// } else +// { +// // Bring high for 250us. +// GPIO.out_w1ts = X1DATA_MASK; +// delayTimer = pThis->mzCtrl.modeB ? 250LL : 250LL; +// state = FSM_ENDXMIT; +// } +// bitStart = !bitStart; +// break; +// +// case FSM_ENDXMIT: +// // End of critical timing loop, release the core. +// portEXIT_CRITICAL(&pThis->mzMutex); +// state = FSM_IDLE; +// break; +// +// } +// +// // If a new delay is requested, set the value into the timer and start. +// if(delayTimer > 0LL) +// { +// timer_set_counter_value(TIMER_GROUP_0, TIMER_0, 0LL); +// timer_start(TIMER_GROUP_0, TIMER_0); +// } +// } +// +// // Logic to feed the watchdog if needed. Watchdog disabled in menuconfig but if enabled this will need to be used. +// //TIMERG0.wdt_wprotect=TIMG_WDT_WKEY_VALUE; // write enable +// //TIMERG0.wdt_feed=1; // feed dog +// //TIMERG0.wdt_wprotect=0; // write protect +// //TIMERG1.wdt_wprotect=TIMG_WDT_WKEY_VALUE; // write enable +// //TIMERG1.wdt_feed=1; // feed dog +// //TIMERG1.wdt_wprotect=0; // write protect +// } +} + +// Method to select keyboard configuration options. When a key sequence is pressed, ie. SHIFT+CTRL+ESC then the fourth simultaneous key is the required option and given to this +// method to act on. Options can be machine model, keyboard map etc. +// +void MZ5665::selectOption(uint8_t optionCode) +{ + // Locals. + // + bool updated = true; + #define SELOPTTAG "selectOption" + + // Simple switch to decode the required option and act on it. + switch(optionCode) + { + // Select a keymap using 1..8 or default (STANDARD) using 0. + case PS2_KEY_1: + this->mzConfig.params.activeKeyboardMap = KEYMAP_UK_WYSE_KB3926; + break; + case PS2_KEY_2: + this->mzConfig.params.activeKeyboardMap = KEYMAP_JAPAN_OADG109; + break; + case PS2_KEY_3: + this->mzConfig.params.activeKeyboardMap = KEYMAP_JAPAN_SANWA_SKBL1; + break; + case PS2_KEY_4: + this->mzConfig.params.activeKeyboardMap = KEYMAP_NOT_ASSIGNED_4; + break; + case PS2_KEY_5: + this->mzConfig.params.activeKeyboardMap = KEYMAP_NOT_ASSIGNED_5; + break; + case PS2_KEY_6: + this->mzConfig.params.activeKeyboardMap = KEYMAP_NOT_ASSIGNED_6; + break; + case PS2_KEY_7: + this->mzConfig.params.activeKeyboardMap = KEYMAP_UK_PERIBOARD_810; + break; + case PS2_KEY_8: + this->mzConfig.params.activeKeyboardMap = KEYMAP_UK_OMOTON_K8508; + break; + case PS2_KEY_0: + this->mzConfig.params.activeKeyboardMap = KEYMAP_STANDARD; + break; + +// // Select the model of the host to enable specific mappings. +// case PS2_KEY_END: +// this->mzConfig.params.activeMachineModel = X1_ORIG; +// break; +// case PS2_KEY_DN_ARROW: +// this->mzConfig.params.activeMachineModel = X1_TURBO; +// break; +// case PS2_KEY_PGDN: +// this->mzConfig.params.activeMachineModel = X1_TURBOZ; +// break; +// case PS2_KEY_INSERT: +// this->mzConfig.params.activeMachineModel = X1_ALL; +// break; +// +// // Switch to keyboard Mode A. This mode is not persisted. +// case PS2_KEY_HOME: +// updated = false; +// this->mzCtrl.modeB = false; +// break; +// // Switch to keyboard Mode B. This mode is not persisted. +// case PS2_KEY_PGUP: +// updated = false; +// this->mzCtrl.modeB = true; +// break; +// + // Unknown option so ignore. + default: + updated = false; + break; + } + + // If an update was made, persist it for power cycles. + // + if(updated) + { + if(this->nvs->persistData(this->getClassName(__PRETTY_FUNCTION__), &this->mzConfig, sizeof(t_mzConfig)) == false) + { + ESP_LOGW(SELOPTTAG, "Persisting MZ-6500 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); + } else + // Few other updates so make a commit here to ensure data is flushed and written. + if(this->nvs->commitData() == false) + { + ESP_LOGW(SELOPTTAG, "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); + } + } + + return; +} + +// Method to take a PS/2 key and control data and map it into an MZ-6500 key and control equivalent, updating state values accordingly (ie. CAPS). +// A mapping table is used which maps a key and state values into an MZ-6500 key and control values, the emphasis being on readability and easy configuration +// as opposed to concatenated byte tables. +// +uint32_t MZ5665::mapKey(uint16_t scanCode) +{ + // Locals. + uint32_t idx; + uint8_t keyCode = (scanCode & 0xFF); + bool mapped = false; + bool matchExact = false; + uint32_t mappedKey = 0x00000000; + #define MAPKEYTAG "mapKey" + + // Intercept control keys and set state variables. + // + // + if(scanCode & PS2_BREAK) + { + // if((keyCode == PS2_KEY_L_SHIFT || keyCode == PS2_KEY_R_SHIFT) && (scanCode & PS2_SHIFT) == 0) { mapped=true; this->mzCtrl.keyCtrl |= X1_CTRL_SHIFT; } + // if((keyCode == PS2_KEY_L_CTRL || keyCode == PS2_KEY_R_CTRL) && (scanCode & PS2_CTRL) == 0) { mapped=true; this->mzCtrl.keyCtrl |= X1_CTRL_CTRL; } + + // Any break key clears the option select flag. + this->mzCtrl.optionSelect = false; + + // Clear any feature LED blinking. + led->setLEDMode(LED::LED_MODE_OFF, LED::LED_DUTY_CYCLE_OFF, 0, 0L, 0L); + } else + { + // if((keyCode == PS2_KEY_L_SHIFT || keyCode == PS2_KEY_R_SHIFT) && (scanCode & PS2_SHIFT)) { mapped=true; this->mzCtrl.keyCtrl &= ~X1_CTRL_SHIFT; } + // if((keyCode == PS2_KEY_L_CTRL || keyCode == PS2_KEY_R_CTRL) && (scanCode & PS2_CTRL)) { mapped=true; this->mzCtrl.keyCtrl &= ~X1_CTRL_CTRL; } + // if(keyCode == PS2_KEY_L_ALT) { mapped = true; this->mzCtrl.keyCtrl ^= X1_CTRL_KANA; } + // if(keyCode == PS2_KEY_R_ALT) { mapped = true; this->mzCtrl.keyCtrl ^= X1_CTRL_GRAPH; } + // if(keyCode == PS2_KEY_CAPS) { mapped = true; this->mzCtrl.keyCtrl ^= X1_CTRL_CAPS; } + // Special mapping to allow selection of keyboard options. If the user presses CTRL+SHIFT+ESC then a flag becomes active and should a fourth key be pressed before a BREAK then the fourth key is taken as an option key and processed accordingly. + if(this->mzCtrl.optionSelect == true) { mapped = true; this->mzCtrl.optionSelect = false; selectOption(keyCode); } + if(keyCode == PS2_KEY_ESC && (scanCode & PS2_CTRL) && (scanCode & PS2_SHIFT)) { mapped = true; this->mzCtrl.optionSelect = true; } + if(this->mzCtrl.optionSelect == true && keyCode != PS2_KEY_ESC) + { + mapped = true; + this->mzCtrl.optionSelect = false; + selectOption(keyCode); + } + if(keyCode == PS2_KEY_ESC && (scanCode & PS2_CTRL) && (scanCode & PS2_SHIFT) && this->mzCtrl.optionSelect == false) + { + // Prime flag ready for fourth option key and start LED blinking periodically. + mapped = true; + this->mzCtrl.optionSelect = true; + led->setLEDMode(LED::LED_MODE_BLINK, LED::LED_DUTY_CYCLE_50, 1, 500L, 500L); + } + } + + // If the key already mapped, ie. due to control signals, send the update as <0x00> so the MZ5665 knows the current control signal state. + if(mapped == true) + { + ESP_LOGW(MAPKEYTAG, "Mapped special key:%02x\n", this->mzCtrl.keyCtrl); + mappedKey = (this->mzCtrl.keyCtrl << 8) | 0x00; + } else + { + // Loop through the entire conversion table to find a match on this key, if found map to MZ5665 equivalent. + // switch matrix. + // + for(idx=0, mapped=false, matchExact=false; idx < mzCtrl.kmeRows && (mapped == false || (mapped == true && matchExact == false)); idx++) + { + // Match key code? Make sure the current machine and keymap match as well. + if(mzCtrl.kme[idx].ps2KeyCode == (uint8_t)(scanCode&0xFF) && ((mzCtrl.kme[idx].machine == MZ5665_ALL) || ((mzCtrl.kme[idx].machine & mzConfig.params.activeMachineModel) != 0)) && ((mzCtrl.kme[idx].keyboardModel & mzConfig.params.activeKeyboardMap) != 0)) + { + // If CAPS lock is set in the table and in the scanCode, invert SHIFT so we send the correct value. + if((scanCode & PS2_CAPS) && (mzCtrl.kme[idx].ps2Ctrl & PS2CTRL_CAPS) != 0) + { + scanCode ^= PS2_SHIFT; + } + + // Match Raw, Shift, Function, Control, ALT or ALT-Gr? + if( (((mzCtrl.kme[idx].ps2Ctrl & PS2CTRL_SHIFT) == 0) && ((mzCtrl.kme[idx].ps2Ctrl & PS2CTRL_CTRL) == 0) && ((mzCtrl.kme[idx].ps2Ctrl & PS2CTRL_KANA) == 0) && ((mzCtrl.kme[idx].ps2Ctrl & PS2CTRL_GRAPH) == 0) && ((mzCtrl.kme[idx].ps2Ctrl & PS2CTRL_GUI) == 0) && ((mzCtrl.kme[idx].ps2Ctrl & PS2CTRL_FUNC) == 0)) || + ((scanCode & PS2_SHIFT) && (mzCtrl.kme[idx].ps2Ctrl & PS2CTRL_SHIFT) != 0) || + ((scanCode & PS2_CTRL) && (mzCtrl.kme[idx].ps2Ctrl & PS2CTRL_CTRL) != 0) || + // ((this->mzCtrl.keyCtrl & X1_CTRL_KANA) == 0 && (mzCtrl.kme[idx].ps2Ctrl & PS2CTRL_KANA) != 0) || + // ((this->mzCtrl.keyCtrl & X1_CTRL_GRAPH) == 0 && (mzCtrl.kme[idx].ps2Ctrl & PS2CTRL_GRAPH) != 0) || + ((scanCode & PS2_GUI) && (mzCtrl.kme[idx].ps2Ctrl & PS2CTRL_GUI) != 0) || + ((scanCode & PS2_FUNCTION) && (mzCtrl.kme[idx].ps2Ctrl & PS2CTRL_FUNC) != 0) ) + { + + // Exact entry match, data + control key? On an exact match we only process the first key. On a data only match we fall through to include additional data and control key matches to allow for un-mapped key combinations, ie. Japanese characters. + matchExact = (((scanCode & PS2_SHIFT) && (mzCtrl.kme[idx].ps2Ctrl & PS2CTRL_SHIFT) != 0) || ((scanCode & PS2_SHIFT) == 0 && (mzCtrl.kme[idx].ps2Ctrl & PS2CTRL_SHIFT) == 0)) && + (((scanCode & PS2_CTRL) && (mzCtrl.kme[idx].ps2Ctrl & PS2CTRL_CTRL) != 0) || ((scanCode & PS2_CTRL) == 0 && (mzCtrl.kme[idx].ps2Ctrl & PS2CTRL_CTRL) == 0)) && + // (((this->mzCtrl.keyCtrl & X1_CTRL_KANA) == 0 && (mzCtrl.kme[idx].ps2Ctrl & PS2CTRL_KANA) != 0) || ((this->mzCtrl.keyCtrl & X1_CTRL_KANA) && (mzCtrl.kme[idx].ps2Ctrl & PS2CTRL_KANA) == 0)) && + // (((this->mzCtrl.keyCtrl & X1_CTRL_GRAPH) == 0 && (mzCtrl.kme[idx].ps2Ctrl & PS2CTRL_GRAPH) != 0) || ((this->mzCtrl.keyCtrl & X1_CTRL_GRAPH) && (mzCtrl.kme[idx].ps2Ctrl & PS2CTRL_GRAPH) == 0)) && + (((scanCode & PS2_GUI) && (mzCtrl.kme[idx].ps2Ctrl & PS2CTRL_GUI) != 0) || ((scanCode & PS2_GUI) == 0 && (mzCtrl.kme[idx].ps2Ctrl & PS2CTRL_GUI) == 0)) && + (((scanCode & PS2_FUNCTION) && (mzCtrl.kme[idx].ps2Ctrl & PS2CTRL_FUNC) != 0) || ((scanCode & PS2_FUNCTION) == 0 && (mzCtrl.kme[idx].ps2Ctrl & PS2CTRL_FUNC) == 0)) + ? true : false; + + // RELEASE (PS2_BREAK == 1) or PRESS? + if((scanCode & PS2_BREAK)) + { + // Special case for the PAUSE / BREAK key. The underlying logic has been modified to send a BREAK key event immediately + // after a PAUSE make, this is necessary as the Sharp machines require SHIFT (pause) BREAK so the PS/2 CTRL+BREAK wont + // work (unless logic is added to insert a SHIFT, pause, add BREAK). The solution was to generate a BREAK event + // when SHIFT+PAUSE is pressed. + if(keyCode == PS2_KEY_PAUSE) + { + vTaskDelay(100); + } + + // Mode A sends a release with 0x00. + // if(this->mzCtrl.modeB == false) + // { + // mappedKey = (0xFF << 8) | 0x00; + // mapped = true; + // // vTaskDelay(300); + // } else + // if(this->mzCtrl.modeB == true) + // { + // Clear only the bits relevant to the released key. + // mappedKey &= ((mzCtrl.kme[idx].x1Ctrl << 16) | (mzCtrl.kme[idx].x1Key2 << 8) | mzCtrl.kme[idx].x1Key); + // } + } else + { + // Mode A return the key in the table, mode B OR the key to build up a final map. + // if(this->mzCtrl.modeB == false) + // mappedKey = ((mzCtrl.kme[idx].x1Ctrl & this->mzCtrl.keyCtrl) << 8) | mzCtrl.kme[idx].x1Key; + // else + // mappedKey |= ((mzCtrl.kme[idx].x1Ctrl << 16) | (mzCtrl.kme[idx].x1Key2 << 8) | mzCtrl.kme[idx].x1Key); + // mapped = true; + } + } + } + } + } + return(mappedKey); +} + +// Primary HID thread, running on Core 0. +// This thread is responsible for receiving HID (PS/2 or BT) keyboard scan codes and mapping them to Sharp MZ5665 equivalent keys, updating state flags as needed. +// The HID data is received via interrupt. The data to be sent to the MZ5665 is pushed onto a FIFO queue. +// +IRAM_ATTR void MZ5665::hidInterface( void * pvParameters ) +{ + // Locals. + uint16_t scanCode = 0x0000; + uint32_t mzKey = 0x00000000; + + // Map the instantiating object so we can access its methods and data. + MZ5665* pThis = (MZ5665*)pvParameters; + + // Thread never exits, just polls the keyboard and updates the matrix. + while(1) + { + // Check stack space, report if it is getting low. + if(uxTaskGetStackHighWaterMark(NULL) < 1024) + { + ESP_LOGW(MAINTAG, "THREAD STACK SPACE(%d)\n",uxTaskGetStackHighWaterMark(NULL)); + } + + // Check for HID keyboard scan codes. + while((scanCode = pThis->hid->read()) != 0) + { + // Scan Code Breakdown: + // Define name bit description + // PS2_BREAK 15 1 = Break key code + // (MSB) 0 = Make Key code + // PS2_SHIFT 14 1 = Shift key pressed as well (either side) + // 0 = No shift key + // PS2_CTRL 13 1 = Ctrl key pressed as well (either side) + // 0 = No Ctrl key + // PS2_CAPS 12 1 = Caps Lock ON + // 0 = Caps lock OFF + // PS2_ALT 11 1 = Left Alt key pressed as well + // 0 = No Left Alt key + // PS2_ALT_GR 10 1 = Right Alt (Alt GR) key pressed as well + // 0 = No Right Alt key + // PS2_GUI 9 1 = GUI key pressed as well (either) + // 0 = No GUI key + // PS2_FUNCTION 8 1 = FUNCTION key non-printable character (plus space, tab, enter) + // 0 = standard character key + // 7-0 PS/2 Key code. + // + // BREAK code means all keys released so clear out flags and send update. + ESP_LOGW(MAPKEYTAG, "SCANCODE:%04x",scanCode); + + // Map the PS/2 key to an MZ5665 CTRL + KEY + mzKey = pThis->mapKey(scanCode); + if(mzKey != 0L) { pThis->pushKeyToQueue(mzKey); } + + // Toggle LED to indicate data flow. + if((scanCode & PS2_BREAK) == 0) + pThis->led->setLEDMode(LED::LED_MODE_BLINK_ONESHOT, LED::LED_DUTY_CYCLE_10, 1, 100L, 0L); + } + + // Yield if the suspend flag is set. + pThis->yield(10); + } +} + +// A method to load the keyboard mapping table into memory for use in the interface mapping logic. If no persistence file exists or an error reading persistence occurs, the keymap +// uses the internal static default. If no persistence file exists and attempt is made to create it with a copy of the inbuilt static map so that future operations all +// work with persistence such that modifications can be made. +// +bool MZ5665::loadKeyMap(void) +{ + // Locals. + // + bool result = false; + int fileRows = 0; + struct stat keyMapFileNameStat; + + // See if the file exists, if it does, get size so we can compute number of mapping rows. + if(stat(mzCtrl.keyMapFileName.c_str(), &keyMapFileNameStat) == -1) + { + ESP_LOGW(MAINTAG, "No keymap file, using inbuilt definitions."); + } else + { + // Get number of rows in the file. + fileRows = keyMapFileNameStat.st_size/sizeof(t_keyMapEntry); + + // Subsequent reloads, delete memory prior to building new map, primarily to conserve precious resources rather than trying the memory allocation trying to realloc and then having to copy. + if(mzCtrl.kme != NULL && mzCtrl.kme != PS2toMZ5665.kme) + { + delete mzCtrl.kme; + mzCtrl.kme = NULL; + } + + // Allocate memory for the new keymap table. + mzCtrl.kme = new t_keyMapEntry[fileRows]; + if(mzCtrl.kme == NULL) + { + ESP_LOGW(MAINTAG, "Failed to allocate memory for keyboard map, fallback to inbuilt!"); + } else + { + // Open the keymap extension file for binary reading to add data to our map table. + std::fstream keyFileIn(mzCtrl.keyMapFileName.c_str(), std::ios::in | std::ios::binary); + + int idx=0; + while(keyFileIn.good()) + { + keyFileIn.read((char *)&mzCtrl.kme[idx], sizeof(t_keyMapEntry)); + if(keyFileIn.good()) + { + idx++; + } + } + // Any errors, we wind back and use the inbuilt mapping table. + if(keyFileIn.bad()) + { + keyFileIn.close(); + ESP_LOGW(MAINTAG, "Failed to read data from keymap extension file:%s, fallback to inbuilt!", mzCtrl.keyMapFileName.c_str()); + } else + { + // No longer need the file. + keyFileIn.close(); + + // Max rows in the KME table. + mzCtrl.kmeRows = fileRows; + + // Good to go, map ready for use with the interface. + result = true; + } + } + } + + // Any failures, free up memory and use the inbuilt mapping table. + if(result == false) + { + if(mzCtrl.kme != NULL && mzCtrl.kme != PS2toMZ5665.kme) + { + delete mzCtrl.kme; + mzCtrl.kme = NULL; + } + + // No point allocating memory if no extensions exist or an error occurs, just point to the static table. + mzCtrl.kme = PS2toMZ5665.kme; + mzCtrl.kmeRows = PS2TBL_MZ5665_MAXROWS; + + // Persist the data so that next load comes from file. + saveKeyMap(); + } + + // Return code. Either memory map was successfully loaded, true or failed, false. + return(result); +} + +// Method to save the current keymap out to an extension file. +// +bool MZ5665::saveKeyMap(void) +{ + // Locals. + // + bool result = false; + int idx = 0; + + // Has a map been defined? Cannot save unless loadKeyMap has been called which sets mzCtrl.kme to point to the internal keymap or a new memory resident map. + // + if(mzCtrl.kme == NULL) + { + ESP_LOGW(MAINTAG, "KeyMap hasnt yet been defined, need to call loadKeyMap."); + } else + { + // Open file for binary writing, trunc specified to clear out the file, we arent appending. + std::fstream keyFileOut(mzCtrl.keyMapFileName.c_str(), std::ios::out | std::ios::binary | std::ios::trunc); + + // Loop whilst no errors and data rows still not written. + while(keyFileOut.good() && idx < mzCtrl.kmeRows) + { + keyFileOut.write((char *)&mzCtrl.kme[idx], sizeof(t_keyMapEntry)); + idx++; + } + if(keyFileOut.bad()) + { + ESP_LOGW(MAINTAG, "Failed to write data from the keymap to file:%s, deleting as state is unknown!", mzCtrl.keyMapFileName.c_str()); + keyFileOut.close(); + std::remove(mzCtrl.keyMapFileName.c_str()); + } else + { + // Success. + keyFileOut.close(); + result = true; + } + } + + // Return code. Either memory map was successfully saved, true or failed, false. + return(result); +} + +// Public method to open a keymap file for data upload. +// This method opens the file and makes any validation checks as necessary. +// +bool MZ5665::createKeyMapFile(std::fstream &outFile) +{ + // Locals. + // + bool result = true; + std::string fileName; + + // Attempt to open a temporary keymap file for writing. + // + fileName = mzCtrl.keyMapFileName; + replaceExt(fileName, "tmp"); + outFile.open(fileName.c_str(), std::ios::out | std::ios::binary | std::ios::trunc); + if(outFile.bad()) + { + result = false; + } + + // Send result. + return(result); +} + +// Public method to validate and store data provided by caller into an open file created by 'createKeyMapFile'. +// +bool MZ5665::storeDataToKeyMapFile(std::fstream &outFile, char *data, int size) +{ + // Locals. + // + bool result = true; + + // Check that the file is still writeable then add data. + if(outFile.good()) + { + outFile.write(data, size); + } + if(outFile.bad()) + { + result = false; + } + + // Send result. + return(result); +} + +// Polymorphic alternative to take a vector of bytes for writing to the output file. +// +bool MZ5665::storeDataToKeyMapFile(std::fstream & outFile, std::vector& dataArray) +{ + // Locals. + // + bool result = true; + char data[1]; + + // Check that the file is still writeable then add data. Not best for performace but ease of use and minimum memory. + if(outFile.good()) + { + for(std::size_t idx = 0; idx < dataArray.size(); idx++) + { + data[0] = (char)dataArray[idx]; + outFile.write((char *)&data, 1); + } + } + if(outFile.bad()) + { + result = false; + } + + // Send result. + return(result); +} + +// Public method to close and commit a data file, created by 'createKeyMapFile' and populated by 'storeDataToKeyMapFile'. +// This involves renaming the original keymap file, closing the new file and renaming it to the original keymap filename. +// +bool MZ5665::closeAndCommitKeyMapFile(std::fstream &outFile, bool cleanupOnly) +{ + // Locals. + // + bool result = true; + std::string fileName; + + // Check the file is still accessible and close. + // + outFile.close(); + if(!cleanupOnly) + { + if(outFile.good()) + { + // Rename the original file. + fileName = mzCtrl.keyMapFileName; + replaceExt(fileName, "bak"); + // Remove old backup file. Dont worry if it is not there! + std::remove(fileName.c_str()); + replaceExt(fileName, "tmp"); + // Rename new file to active. + if(std::rename(fileName.c_str(), mzCtrl.keyMapFileName.c_str()) != 0) + { + result = false; + } + } else + { + result = false; + } + } + + // Send result. + return(result); +} + +// Method to return the keymap column names as header strings. +// +void MZ5665::getKeyMapHeaders(std::vector& headerList) +{ + // Add the names. + // + headerList.push_back(PS2TBL_PS2KEYCODE_NAME); + headerList.push_back(PS2TBL_PS2CTRL_NAME); + headerList.push_back(PS2TBL_KEYBOARDMODEL_NAME); + headerList.push_back(PS2TBL_MACHINE_NAME); +// headerList.push_back(PS2TBL_X1MODE_NAME); +// headerList.push_back(PS2TBL_X1KEYCODE_NAME); +// headerList.push_back(PS2TBL_X1KEYCODE_BYTE2_NAME); +// headerList.push_back(PS2TBL_X1_CTRL_NAME); + + return; +} + +// A method to return the Type of data for a given column in the KeyMap table. +// +void MZ5665::getKeyMapTypes(std::vector& typeList) +{ + // Add the types. + // + typeList.push_back(PS2TBL_PS2KEYCODE_TYPE); + typeList.push_back(PS2TBL_PS2CTRL_TYPE); + typeList.push_back(PS2TBL_KEYBOARDMODEL_TYPE); + typeList.push_back(PS2TBL_MACHINE_TYPE); +// typeList.push_back(PS2TBL_X1MODE_TYPE); +// typeList.push_back(PS2TBL_X1KEYCODE_TYPE); +// typeList.push_back(PS2TBL_X1KEYCODE_BYTE2_TYPE); +// typeList.push_back(PS2TBL_X1CTRL_TYPE); + + return; +} + +// Method to return a list of key:value entries for a given keymap column. This represents the +// feature which can be selected and the value it uses. Features can be combined by ORing the values +// together. +bool MZ5665::getKeyMapSelectList(std::vector>& selectList, std::string option) +{ + // Locals. + // + bool result = true; + + // Build up a map, depending on the list required, of name to value. This list can then be used + // by a user front end to select an option based on a name and return its value. + if(option.compare(PS2TBL_PS2CTRL_TYPE) == 0) + { + selectList.push_back(std::make_pair(PS2TBL_PS2CTRL_SEL_SHIFT, PS2CTRL_SHIFT)); + selectList.push_back(std::make_pair(PS2TBL_PS2CTRL_SEL_CTRL, PS2CTRL_CTRL)); + selectList.push_back(std::make_pair(PS2TBL_PS2CTRL_SEL_CAPS, PS2CTRL_CAPS)); + selectList.push_back(std::make_pair(PS2TBL_PS2CTRL_SEL_KANA, PS2CTRL_KANA)); + selectList.push_back(std::make_pair(PS2TBL_PS2CTRL_SEL_GRAPH, PS2CTRL_GRAPH)); + selectList.push_back(std::make_pair(PS2TBL_PS2CTRL_SEL_GUI, PS2CTRL_GUI)); + selectList.push_back(std::make_pair(PS2TBL_PS2CTRL_SEL_FUNC, PS2CTRL_FUNC)); + selectList.push_back(std::make_pair(PS2TBL_PS2CTRL_SEL_EXACT, PS2CTRL_EXACT)); + } + else if(option.compare(PS2TBL_KEYBOARDMODEL_TYPE) == 0) + { + selectList.push_back(std::make_pair(KEYMAP_SEL_STANDARD, KEYMAP_STANDARD)); + selectList.push_back(std::make_pair(KEYMAP_SEL_UK_WYSE_KB3926, KEYMAP_UK_WYSE_KB3926)); + selectList.push_back(std::make_pair(KEYMAP_SEL_JAPAN_OADG109, KEYMAP_JAPAN_OADG109)); + selectList.push_back(std::make_pair(KEYMAP_SEL_JAPAN_SANWA_SKBL1, KEYMAP_JAPAN_SANWA_SKBL1)); + selectList.push_back(std::make_pair(KEYMAP_SEL_NOT_ASSIGNED_4, KEYMAP_NOT_ASSIGNED_4)); + selectList.push_back(std::make_pair(KEYMAP_SEL_NOT_ASSIGNED_5, KEYMAP_NOT_ASSIGNED_5)); + selectList.push_back(std::make_pair(KEYMAP_SEL_NOT_ASSIGNED_6, KEYMAP_NOT_ASSIGNED_6)); + selectList.push_back(std::make_pair(KEYMAP_SEL_UK_PERIBOARD_810, KEYMAP_UK_PERIBOARD_810)); + selectList.push_back(std::make_pair(KEYMAP_SEL_UK_OMOTON_K8508, KEYMAP_UK_OMOTON_K8508)); + } + else if(option.compare(PS2TBL_MACHINE_TYPE) == 0) + { + selectList.push_back(std::make_pair(MZ5665_SEL_ALL, MZ5665_ALL)); + } + else if(option.compare(PS2TBL_MZ5665_CTRL_TYPE) == 0) + { + selectList.push_back(std::make_pair(MZ5665_CTRL_SEL_GRAPH, MZ5665_CTRL_GRAPH)); + selectList.push_back(std::make_pair(MZ5665_CTRL_SEL_CAPS, MZ5665_CTRL_CAPS)); + selectList.push_back(std::make_pair(MZ5665_CTRL_SEL_KANA, MZ5665_CTRL_KANA)); + selectList.push_back(std::make_pair(MZ5665_CTRL_SEL_SHIFT, MZ5665_CTRL_SHIFT)); + selectList.push_back(std::make_pair(MZ5665_CTRL_SEL_CTRL, MZ5665_CTRL_CTRL)); + } else + { + // Not found! + result = false; + } + + // Return result, false if the option not found, true otherwise. + // + return(result); +} + + +// Method to read the Keymap array, 1 row at a time and return it to the caller. +// +bool MZ5665::getKeyMapData(std::vector& dataArray, int *row, bool start) +{ + // Locals. + // + bool result = false; + + // If start flag is set, set row to 0. + if(start == true) + { + (*row) = 0; + } + + // Bound check and if still valid, push data onto the vector. + if((*row) >= mzCtrl.kmeRows) + { + result = true; + } else + { + dataArray.push_back(mzCtrl.kme[*row].ps2KeyCode); + dataArray.push_back(mzCtrl.kme[*row].ps2Ctrl); + dataArray.push_back(mzCtrl.kme[*row].keyboardModel); + dataArray.push_back(mzCtrl.kme[*row].machine); + // dataArray.push_back(mzCtrl.kme[*row].x1Mode); + // dataArray.push_back(mzCtrl.kme[*row].x1Key); + // dataArray.push_back(mzCtrl.kme[*row].x1Key2); + // dataArray.push_back(mzCtrl.kme[*row].x1Ctrl); + (*row) = (*row) + 1; + } + + // True if no more rows, false if additional rows can be read. + return(result); +} + + +// Initialisation routine. Start two threads, one to handle the incoming PS/2 keyboard data and map it, the second to handle the host interface. +void MZ5665::init(uint32_t ifMode, NVS *hdlNVS, LED *hdlLED, HID *hdlHID) +{ + // Basic initialisation. + init(hdlNVS, hdlHID); + + // Invoke the prototype init which initialises common variables and devices shared by all subclass. + KeyInterface::init(getClassName(__PRETTY_FUNCTION__), hdlNVS, hdlLED, hdlHID, ifMode); + + // Create a task pinned to core 1 which will fulfill the Sharp MZ-6500 interface. This task has the highest priority + // and it will also hold spinlock and manipulate the watchdog to ensure a scan cycle timing can be met. This means + // all other tasks running on Core 1 will suspend as needed. The PS/2 controller will be serviced with core 0. + // + // Core 1 - MZ-6500 Interface + ESP_LOGW(MAINTAG, "Starting mz5600/mz6500 if thread..."); + ::xTaskCreatePinnedToCore(&this->mzInterface, "mzif", 4096, this, 25, &this->TaskHostIF, 1); + vTaskDelay(500); + + // Core 0 - Application + // HID Interface handler thread. + ESP_LOGW(MAINTAG, "Starting hidIf thread..."); + ::xTaskCreatePinnedToCore(&this->hidInterface, "hidIf", 8192, this, 22, &this->TaskHIDIF, 0); + + // Create queue for buffering incoming keys prior to transmitting to the MZ-6500. + xmitQueue = xQueueCreate(MAX_MZ5665_XMIT_KEY_BUF, sizeof(t_xmitQueueMessage)); +} + +// Initialisation routine without hardware. +void MZ5665::init(NVS *hdlNVS, HID *hdlHID) +{ + // Initialise control variables. + this->mzCtrl.keyCtrl = 0xFF; // Negative logic, 0 - active, 1 = inactive. + mzCtrl.optionSelect = false; + mzCtrl.keyMapFileName = mzCtrl.fsPath.append("/").append(MZ5665IF_KEYMAP_FILE); + mzCtrl.kmeRows = 0; + mzCtrl.kme = NULL; + + // Invoke the prototype init which initialises common variables and devices shared by all subclass. + KeyInterface::init(getClassName(__PRETTY_FUNCTION__), hdlNVS, hdlHID); + + // Load the keyboard mapping table into memory. If the file doesnt exist, create it. + loadKeyMap(); + + // Retrieve configuration, if it doesnt exist, set defaults. + // + if(nvs->retrieveData(getClassName(__PRETTY_FUNCTION__), &this->mzConfig, sizeof(t_mzConfig)) == false) + { + ESP_LOGW(MAINTAG, "MZ5665 configuration set to default, no valid config in NVS found."); + mzConfig.params.activeKeyboardMap = KEYMAP_STANDARD; + mzConfig.params.activeMachineModel = MZ5665_ALL; + + // Persist the data for next time. + if(nvs->persistData(getClassName(__PRETTY_FUNCTION__), &this->mzConfig, sizeof(t_mzConfig)) == false) + { + ESP_LOGW(MAINTAG, "Persisting Default MZ5665 configuration data failed, check NVS setup.\n"); + } + // Few other updates so make a commit here to ensure data is flushed and written. + else if(this->nvs->commitData() == false) + { + ESP_LOGW(SELOPTTAG, "NVS Commit writes operation failed, some previous writes may not persist in future power cycles."); + } + } +} + +// Constructor, basically initialise the Singleton interface and let the threads loose. +MZ5665::MZ5665(uint32_t ifMode, NVS *hdlNVS, LED *hdlLED, HID *hdlHID, const char* fsPath) +{ + // Setup the default path on the underlying filesystem. + this->mzCtrl.fsPath = fsPath; + + // Initialise the interface. + init(ifMode, hdlNVS, hdlLED, hdlHID); +} + +// Constructor, basic initialisation without hardware. +MZ5665::MZ5665(NVS *hdlNVS, HID *hdlHID, const char* fsPath) +{ + // Setup the default path on the underlying filesystem. + this->mzCtrl.fsPath = fsPath; + + // Initialise the interface. + init(hdlNVS, hdlHID); +} + +// Constructor, used for version reporting so no hardware is initialised. +MZ5665::MZ5665(void) +{ + return; +} + +// Destructor - only ever called when the class is used for version reporting. +MZ5665::~MZ5665(void) +{ + return; +} diff --git a/main/Mouse.cpp b/main/Mouse.cpp deleted file mode 120000 index 4f1a342..0000000 --- a/main/Mouse.cpp +++ /dev/null @@ -1 +0,0 @@ -../../sharpkey/main/Mouse.cpp \ No newline at end of file diff --git a/main/Mouse.cpp b/main/Mouse.cpp new file mode 100644 index 0000000..5277e22 --- /dev/null +++ b/main/Mouse.cpp @@ -0,0 +1,729 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// Name: Mouse.cpp +// Created: Mar 2022 +// Version: v1.0 +// Author(s): Philip Smart +// Description: PS/2 Mouse to Sharp Host Interface logic. +// This source file contains the singleton class containing logic to obtain +// PS/2 mouse data (position, keys etc), map them into Sharp compatible codes and +// transmit the data to the connected host. +// +// The whole application of which this class is a member, uses the Espressif Development +// environment with Arduino components. +// +// Credits: +// Copyright: (c) 2022 Philip Smart +// +// History: Mar 2022 - Initial write. +// v1.01 May 2022 - Initial release version. +// v1.02 Jun 2022 - Updates to reflect changes realised in other modules due to addition of +// bluetooth and suspend logic due to NVS issues using both cores. +// Updates to reflect moving functionality into the HID and to support +// Bluetooth as a primary mouse or secondary mouse. +// +// 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 +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/queue.h" +#include "driver/uart.h" +#include "driver/gpio.h" +#include "esp_log.h" +#include "Arduino.h" +#include "soc/timer_group_struct.h" +#include "soc/timer_group_reg.h" +#include "driver/timer.h" +#include "sdkconfig.h" +#include "Mouse.h" + +// Tag for ESP main application logging. +#define MAINTAG "Mouse" + +// Mouse Protocol +// -------------- +// +// +// The Sharp (X68000/X1/MZ-2500/MZ-2800) mouse uses an asynchronous serial protocol over two wires (MSDATA/MSCTRL). +// The MSCTRL signal is an enable signal, idle state = HIGH, it goes low prior to transmission of data by at least 1mS and goes high after +// transmission of last bit by ~2.56mS. +// The MSDATA signal is a standard asynchronous signal, idle = HIGH, 1 start bit, 8 data bits, 2 stop bits @ 4800baud. +// +// Protocol: +// Idle State (MSDATA/MSCTRL) = High. +// Transmission: MSCTRL -> LOW +// 1ms delay +// MSDATA -> low, first start bit. +// 3 bytes transmitted in a <1xStart><8xdata><2xstop> format. +// MSDATA -> high +// 2.56ms delay. +// MSCTRL -> HIGH +// Data bytes: +// CTRL = [7] - Mouse rolling forward when high, backward when low. +// [6] +// [5] - Mouse rolling left, right when low. +// [4] +// [3] +// [2] +// [1] - Right button pressed = HIGH. +// [0] - Left button pressed = HIGH. +// POS X [7:0] - X Position data. +// POS Y [7:0] - Y Position data. + + +// Method to realise the Sharp host Mouse protocol. +// This method uses Core 1 and it will hold it in a spinlock as necessary to ensure accurate timing. +// Mouse data is passed into the method via a direct object, using the FreeRTOS Queue creates a time lag resulting in the mouse data being out of sync with hand movement. +IRAM_ATTR void Mouse::hostInterface( void * pvParameters ) +{ + // Locals. + // + Mouse* pThis = (Mouse*)pvParameters; // Retrieve pointer to object in order to access data. + bool msctrlEdge = false; + uint8_t txBuf[4]; + uint32_t MSCTRL_MASK; + uint32_t MSDATA_MASK; + #ifdef CONFIG_HOST_BITBANG_UART + int txPos; + int txCnt; + uint32_t shiftReg; + uint64_t delayTimer = 0LL; + uint64_t curTime = 0LL; + uint32_t bitCount = 0; + enum HOSTXMITSTATE { + FSM_IDLE = 0, + FSM_STARTXMIT = 1, + FSM_STARTBIT = 2, + FSM_DATA = 3, + FSM_STOP = 4, + FSM_ENDXMIT = 5 + } state = FSM_IDLE; + #endif + + // Initialise the MUTEX which prevents this core from being released to other tasks. + pThis->x1Mutex = portMUX_INITIALIZER_UNLOCKED; + + if(pThis->hostControl.secondaryIf == false) + { + MSCTRL_MASK = (1 << CONFIG_HOST_KDB0); + MSDATA_MASK = (1 << CONFIG_HOST_KDB1); + } else + { + MSCTRL_MASK = (1 << CONFIG_HOST_KDB0); + MSDATA_MASK = (1 << CONFIG_HOST_KDI4); + } + + gpio_config_t ioConf; + ioConf.intr_type = GPIO_INTR_DISABLE; + ioConf.mode = GPIO_MODE_INPUT; + ioConf.pull_down_en = GPIO_PULLDOWN_DISABLE; + ioConf.pull_up_en = GPIO_PULLUP_ENABLE; + // Both Hardware UART and bitbang need MSCTRL setting as an input. + if(pThis->hostControl.secondaryIf == false) + { + ioConf.pin_bit_mask = (1ULL<hostControl.secondaryIf == false) + { + ioConf.pin_bit_mask = (1ULL<hostControl.secondaryIf == false) || curTime >= delayTimer) + { + // Ensure the timer is stopped. + timer_pause(TIMER_GROUP_0, TIMER_0); + delayTimer = 0LL; + + // Finite state machine to retrieve a key for transmission then serialise it according to the X1 protocol. + switch(state) + { + case FSM_IDLE: + // Yield if the suspend flag is set. + pThis->yield(0); + + // Check stack space, report if it is getting low. + if(uxTaskGetStackHighWaterMark(NULL) < 1024) + { + ESP_LOGW(MAINTAG, "THREAD STACK SPACE(%d)\n",uxTaskGetStackHighWaterMark(NULL)); + } + + if(pThis->hostControl.secondaryIf == false) + { + // Detect high to low edge. On mouse primary mode the MSCTRL signal forces the tempo. On mouse secondary mode (operating in tandem to keyboard), + // the timer forces the tempo. + // + msctrlEdge = (REG_READ(GPIO_IN_REG) & MSCTRL_MASK) != 0 ? true : msctrlEdge; + } + + // Wait for a window when MSCTRL goes low. + if(pThis->hostControl.secondaryIf == true || (msctrlEdge == true && (REG_READ(GPIO_IN_REG) & MSCTRL_MASK) == 0)) + { + // Wait for incoming mouse movement message. + if(pThis->xmitMsg.valid) + { + txBuf[0] = (uint8_t)pThis->xmitMsg.status; + txBuf[1] = (uint8_t)pThis->xmitMsg.xPos; + txBuf[2] = (uint8_t)pThis->xmitMsg.yPos; + pThis->xmitMsg.valid = false; // Shouldnt be a race state here but consider a mutex if mouse gets out of sync. + txBuf[3] = 0x00; + txPos = 0; + txCnt = 3; + } else + { + // Sharp host protocol requires us to send zero change messages on a regular period regardless of new data. + txBuf[0] = 0x00; + txBuf[1] = 0x00; + txBuf[2] = 0x00; + txBuf[3] = 0x00; + txPos = 0; + txCnt = 3; + } + + // Advance to first start bit. + state = FSM_STARTXMIT; + + // Clear edge detect for next loop. + msctrlEdge = false; + } + break; + + case FSM_STARTXMIT: + // Ensure all variables and states correct before entering serialisation. + GPIO.out_w1ts = MSDATA_MASK; + state = FSM_STARTBIT; + bitCount = 8; + shiftReg = txBuf[txPos++]; + txCnt--; + + // Create, initialise and hold a spinlock so the current core is bound to this one method. + portENTER_CRITICAL(&pThis->x1Mutex); + + break; + + case FSM_STARTBIT: + // Send out the start bit by bringing MSDATA low for 208us (4800 baud 1bit time period). + GPIO.out_w1tc = MSDATA_MASK; + delayTimer = BITBANG_UART_BIT_TIME; + state = FSM_DATA; + break; + + case FSM_DATA: + if(bitCount > 0) + { + // Setup the bit on MSDATA + if(shiftReg & 0x00000001) + { + GPIO.out_w1ts = MSDATA_MASK; + } else + { + GPIO.out_w1tc = MSDATA_MASK; + } + + // Shift the data to the next bit for transmission. + shiftReg = shiftReg >> 1; + + // 1 bit period. + delayTimer = BITBANG_UART_BIT_TIME; + + // 1 Less bit in frame. + bitCount--; + } else + { + state = FSM_STOP; + } + break; + + case FSM_STOP: + // Send out the stop bit, 2 are needed so just adjust the time delay. + GPIO.out_w1ts = MSDATA_MASK; + delayTimer = BITBANG_UART_BIT_TIME * 2; + state = FSM_ENDXMIT; + break; + + case FSM_ENDXMIT: + // End of critical timing loop, release the core so other tasks can run whilst we load up the next byte. + portEXIT_CRITICAL(&pThis->x1Mutex); + + // Any more bytes to transmit, loop and send if there are. + if(txCnt > 0) + { + state = FSM_STARTXMIT; + } else + { + // Reset timer for next loop. + delayTimer = 20000UL; + state = FSM_IDLE; + } + break; + } + + // If a new delay is requested, set the value into the timer and start. + if(delayTimer > 0LL) + { + timer_set_counter_value(TIMER_GROUP_0, TIMER_0, 0LL); + timer_start(TIMER_GROUP_0, TIMER_0); + } + } + #endif + + #ifdef CONFIG_HOST_HW_UART + // Get the current timer value, we need to wait 20ms between transmissions. + timer_get_counter_value(TIMER_GROUP_0, TIMER_0, &curTime); + if(curTime >= delayTimer) + { + // Wait for a window when MSCTRL goes low. + if(pThis->hostControl.secondaryIf == true || (REG_READ(GPIO_IN_REG) & MSCTRL_MASK) == 0) + { + // Ensure the timer is stopped, initialise to 0 and restart. + timer_pause(TIMER_GROUP_0, TIMER_0); + delayTimer = 20000LL; + timer_set_counter_value(TIMER_GROUP_0, TIMER_0, 0LL); + timer_start(TIMER_GROUP_0, TIMER_0); + + // Wait for incoming mouse movement message. + if(pThis->xmitMsg.valid) + { + txBuf[0] = (uint8_t)pThis->xmitMsg.status; + txBuf[1] = (uint8_t)pThis->xmitMsg.xPos; + txBuf[2] = (uint8_t)pThis->xmitMsg.yPos; + pThis->xmitMsg.valid = false; // Shouldnt be a race state here but consider a mutex if mouse gets out of sync. + txBuf[3] = 0x00; + txPos = 0; + txCnt = 3; + } else + { + // Sharp host protocol requires us to send zero change messages on a regular period regardless of new data. + txBuf[0] = 0x00; + txBuf[1] = 0x00; + txBuf[2] = 0x00; + txBuf[3] = 0x00; + txPos = 0; + txCnt = 3; + } + + // Send the bytes and wait. + uart_write_bytes(pThis->hostControl.uartNum, (const char *)txBuf, 3); + + // This method doesnt actually return after the last byte is transmitted, it returns well before, so we tack on a 10ms delay which is the width for 3 bytes at 4800 baud. + uart_wait_tx_done(pThis->hostControl.uartNum, 25000); + vTaskDelay(10); + } + + // Check stack space, report if it is getting low. + if(uxTaskGetStackHighWaterMark(NULL) < 1024) + { + ESP_LOGW(MAPKEYTAG, "THREAD STACK SPACE(%d)\n",uxTaskGetStackHighWaterMark(NULL)); + } + + // Yield if the suspend flag is set. + pThis->yield(0); + } + #endif + + // Logic to feed the watchdog if needed. Watchdog disabled in menuconfig but if enabled this will need to be used. + //TIMERG0.wdt_wprotect=TIMG_WDT_WKEY_VALUE; // write enable + //TIMERG0.wdt_feed=1; // feed dog + //TIMERG0.wdt_wprotect=0; // write protect + //TIMERG1.wdt_wprotect=TIMG_WDT_WKEY_VALUE; // write enable + //TIMERG1.wdt_feed=1; // feed dog + //TIMERG1.wdt_wprotect=0; // write protect + } +} + +// Primary HID routine. +// This method is responsible for receiving HID (PS/2 or BT) mouse scan data and mapping it into Sharp compatible mouse data. +// The HID mouse data once received is mapped and pushed onto a FIFO queue for transmission to the host. +// +void Mouse::mouseReceiveData(HID::t_mouseMessageElement mouseMessage) +{ + // Locals. + uint8_t status; + + // Invert Y as the Sharp host is inverted compared to a PS/2 on the Y axis. + mouseMessage.yPos = -mouseMessage.yPos; + + // Initialise the status flag, on the Sharp host it is <1><0> + status = (((mouseMessage.xPos >> 8) & 0x01) << 4) | (mouseMessage.status & 0x0F ); + + // Check bounds and set flags accordingly. + if(mouseMessage.xPos > 127) + { + mouseMessage.xPos = 127; // Maximum resolution of Sharp host X movement. + status |= (1UL << 4); // Set overflow bit. + } + if(mouseMessage.xPos < -128) + { + mouseMessage.xPos = -128; // Minimum resolution of Sharp host X movement. + status |= (1UL << 5); // Set underflow bit. + } + if(mouseMessage.yPos > 127) + { + mouseMessage.yPos = 127; // Maximum resolution of Sharp host Y movement. + status |= (1UL << 6); // Set overflow bit. + } + if(mouseMessage.yPos < -128) + { + mouseMessage.yPos = -128; // Minimum resolution of Sharp host Y movement. + status |= (1UL << 7); // Set underflow bit. + } + + // Convert back to 8bit 2's compliment and store in the host message to the host thread. + xmitMsg.xPos = (int8_t)mouseMessage.xPos; + xmitMsg.yPos = (int8_t)mouseMessage.yPos; + xmitMsg.status = status; + xmitMsg.wheel = mouseMessage.wheel; + xmitMsg.valid = true; + + return; +} + +// A method to return the Type of data for a given column in the KeyMap table. +// +void Mouse::getMouseConfigTypes(std::vector& typeList) +{ + // Add the types. + // + typeList.push_back(HID_MOUSE_HOST_SCALING_TYPE); + typeList.push_back(HID_MOUSE_SCALING_TYPE); + typeList.push_back(HID_MOUSE_RESOLUTION_TYPE); + typeList.push_back(HID_MOUSE_SAMPLING_TYPE); + return; +} + +// Method to return a list of key:value entries for a given config category. This represents the +// feature which can be selected and the value it uses. Features can be combined by ORing the values +// together. +bool Mouse::getMouseSelectList(std::vector>& selectList, std::string option) +{ + // Locals. + // + bool result = true; + + // Build up a map, depending on the list required, of name to value. This list can then be used + // by a user front end to select an option based on a name and return its value. + if(option.compare(HID_MOUSE_HOST_SCALING_TYPE) == 0) + { + selectList.push_back(std::make_pair("ACTIVE", mouseConfig.host.scaling)); + selectList.push_back(std::make_pair(HID_MOUSE_HOST_SCALING_1_1_NAME, HID::HID_MOUSE_HOST_SCALING_1_1)); + selectList.push_back(std::make_pair(HID_MOUSE_HOST_SCALING_1_2_NAME, HID::HID_MOUSE_HOST_SCALING_1_2)); + selectList.push_back(std::make_pair(HID_MOUSE_HOST_SCALING_1_3_NAME, HID::HID_MOUSE_HOST_SCALING_1_3)); + selectList.push_back(std::make_pair(HID_MOUSE_HOST_SCALING_1_4_NAME, HID::HID_MOUSE_HOST_SCALING_1_4)); + selectList.push_back(std::make_pair(HID_MOUSE_HOST_SCALING_1_5_NAME, HID::HID_MOUSE_HOST_SCALING_1_5)); + } + else if(option.compare(HID_MOUSE_SCALING_TYPE) == 0) + { + selectList.push_back(std::make_pair("ACTIVE", mouseConfig.mouse.scaling)); + selectList.push_back(std::make_pair(HID_MOUSE_SCALING_1_1_NAME, HID::HID_MOUSE_SCALING_1_1)); + selectList.push_back(std::make_pair(HID_MOUSE_SCALING_2_1_NAME, HID::HID_MOUSE_SCALING_2_1)); + } + else if(option.compare(HID_MOUSE_RESOLUTION_TYPE) == 0) + { + selectList.push_back(std::make_pair("ACTIVE", mouseConfig.mouse.resolution)); + selectList.push_back(std::make_pair(HID_MOUSE_RESOLUTION_1_1_NAME, HID::HID_MOUSE_RESOLUTION_1_1)); + selectList.push_back(std::make_pair(HID_MOUSE_RESOLUTION_1_2_NAME, HID::HID_MOUSE_RESOLUTION_1_2)); + selectList.push_back(std::make_pair(HID_MOUSE_RESOLUTION_1_4_NAME, HID::HID_MOUSE_RESOLUTION_1_4)); + selectList.push_back(std::make_pair(HID_MOUSE_RESOLUTION_1_8_NAME, HID::HID_MOUSE_RESOLUTION_1_8)); + } + else if(option.compare(HID_MOUSE_SAMPLING_TYPE) == 0) + { + selectList.push_back(std::make_pair("ACTIVE", mouseConfig.mouse.sampleRate)); + selectList.push_back(std::make_pair(HID_MOUSE_SAMPLE_RATE_10_NAME, HID::HID_MOUSE_SAMPLE_RATE_10)); + selectList.push_back(std::make_pair(HID_MOUSE_SAMPLE_RATE_20_NAME, HID::HID_MOUSE_SAMPLE_RATE_20)); + selectList.push_back(std::make_pair(HID_MOUSE_SAMPLE_RATE_40_NAME, HID::HID_MOUSE_SAMPLE_RATE_40)); + selectList.push_back(std::make_pair(HID_MOUSE_SAMPLE_RATE_60_NAME, HID::HID_MOUSE_SAMPLE_RATE_60)); + selectList.push_back(std::make_pair(HID_MOUSE_SAMPLE_RATE_80_NAME, HID::HID_MOUSE_SAMPLE_RATE_80)); + selectList.push_back(std::make_pair(HID_MOUSE_SAMPLE_RATE_100_NAME, HID::HID_MOUSE_SAMPLE_RATE_100)); + selectList.push_back(std::make_pair(HID_MOUSE_SAMPLE_RATE_200_NAME, HID::HID_MOUSE_SAMPLE_RATE_200)); + } else + { + // Not found! + result = false; + } + + // Return result, false if the option not found, true otherwise. + // + return(result); +} + +// Public method to set the mouse configuration parameters. +// +bool Mouse::setMouseConfigValue(std::string paramName, std::string paramValue) +{ + // Locals. + // + bool dataError = false; + int value(0); + std::stringstream testVal(paramValue); + + // Match the parameter name to a known mouse parameter, type and data check the parameter value and assign to the config accordingly. + if(paramName.compare(HID_MOUSE_HOST_SCALING_TYPE) == 0) + { + // Exception handling is disabled, stringstream is used to catch bad input. + dataError = (static_cast(testVal >> value) ? false : true); + if(dataError == false) + { + if(value >= to_underlying(HID::HID_MOUSE_HOST_SCALING_1_1) && value <= to_underlying(HID::HID_MOUSE_HOST_SCALING_1_5)) + { + mouseConfig.host.scaling = static_cast(value); + hid->setMouseHostScaling(mouseConfig.host.scaling); + } else + { + dataError = true; + } + } + } + if(paramName.compare(HID_MOUSE_SCALING_TYPE) == 0) + { + dataError = (static_cast(testVal >> value) ? false : true); + if(dataError == false) + { + if(value >= to_underlying(HID::HID_MOUSE_SCALING_1_1) && value <= to_underlying(HID::HID_MOUSE_SCALING_2_1)) + { + mouseConfig.mouse.scaling = static_cast(value); + hid->setMouseScaling(mouseConfig.mouse.scaling); + } else + { + dataError = true; + } + } + } + if(paramName.compare(HID_MOUSE_RESOLUTION_TYPE) == 0) + { + dataError = (static_cast(testVal >> value) ? false : true); + if(dataError == false) + { + if(value >= to_underlying(HID::HID_MOUSE_RESOLUTION_1_1) && value <= to_underlying(HID::HID_MOUSE_RESOLUTION_1_8)) + { + mouseConfig.mouse.resolution = static_cast(value); + hid->setMouseResolution(mouseConfig.mouse.resolution); + } else + { + dataError = true; + } + } + } + if(paramName.compare(HID_MOUSE_SAMPLING_TYPE) == 0) + { + dataError = (static_cast(testVal >> value) ? false : true); + if(dataError == false) + { + if(value >= to_underlying(HID::HID_MOUSE_SAMPLE_RATE_10) && value <= to_underlying(HID::HID_MOUSE_SAMPLE_RATE_200)) + { + mouseConfig.mouse.sampleRate = static_cast(value); + hid->setMouseSampleRate(mouseConfig.mouse.sampleRate); + } else + { + dataError = true; + } + } + } + + // Error = true, success = false. + return(dataError); +} + +// Method to save (persist) the configuration into NVS RAM. +bool Mouse::persistConfig(void) +{ + // Locals. + bool result = true; + + // Persist the data for next time. + if(nvs->persistData(getClassName(__PRETTY_FUNCTION__), &this->mouseConfig, sizeof(t_mouseConfig)) == false) + { + ESP_LOGW(MAINTAG, "Persisting Mouse configuration data failed, check NVS setup.\n"); + result = false; + } + // Few other updates so make a commit here to ensure data is flushed and written. + else if(nvs->commitData() == false) + { + ESP_LOGW(MAINTAG, "NVS Commit writes operation failed, some previous writes may not persist in future power cycles."); + } + + // Request persistence in the HID module. + result |= hid->persistConfig(); + + // Error = false, success = true. + return(result); +} + +// Initialisation routine. Start two threads, one to handle the incoming PS/2 mouse data and map it, the second to handle the host interface. +void Mouse::init(uint32_t ifMode, NVS *hdlNVS, LED *hdlLED, HID *hdlHID) +{ + // Initialise control variables. + #ifdef CONFIG_HOST_HW_UART + hostControl.uartNum = UART_NUM_2; + hostControl.uartBufferSize = 256; + hostControl.uartQueueSize = 10; + #endif + + // Initialise the basic components. + init(hdlNVS, hdlHID); + + // Invoke the prototype init which initialises common variables and devices shared by all subclass. + KeyInterface::init(getClassName(__PRETTY_FUNCTION__), hdlNVS, hdlLED, hdlHID, ifMode); + + // There are two build possibilities, hardware UART and BITBANG. I initially coded using hardware but whilst trying to find a bug, wrote a bitbang + // technique and both are fit for purpose, so enabling either yields the same result. + #ifdef CONFIG_HOST_HW_UART + // Prepare the UART to be used for communications with the Sharp host. + // The Sharp host Mouse uses an Asynchronous protocol with 2 stop bits no parity 4800 baud. + // + uart_config_t uartConfig = { + .baud_rate = 4800, + .data_bits = UART_DATA_8_BITS, + .parity = UART_PARITY_DISABLE, + .stop_bits = UART_STOP_BITS_2, + .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, + .rx_flow_ctrl_thresh = 122, + .source_clk = UART_SCLK_APB, + }; + + // Configure UART parameters and pin assignments, software flow control, not RTS/CTS. + // The mouse only uses a Tx line, the MSCTRL line is used as a gate signal, so assign the Rx line to an unused pin. + ESP_ERROR_CHECK(uart_param_config(hostControl.uartNum, &uartConfig)); + ESP_ERROR_CHECK(uart_set_pin(hostControl.uartNum, CONFIG_HOST_KDB1, CONFIG_HOST_KDB2, -1, -1)); + // Install UART driver. Use RX/TX buffers without event queue. + ESP_ERROR_CHECK(uart_driver_install(hostControl.uartNum, hostControl.uartBufferSize, hostControl.uartBufferSize, 0, NULL, 0)); + #endif + + // Register the streaming callback for the mouse, this will receive data, process it and send to the hostInterface for transmission to the host. + hid->setDataCallback(&Mouse::mouseReceiveData, this); + + // Create a task pinned to core 1 which will fulfill the Sharp Mouse host interface. This task has the highest priority + // and it will also hold spinlock and manipulate the watchdog to ensure a scan cycle timing can be met. This means + // all other tasks running on Core 1 will suspend as needed. The HID mouse controller will be serviced with core 0. + // + // Core 1 - Sharp Mouse Host Interface + ESP_LOGW(MAINTAG, "Starting mouseIf thread..."); + ::xTaskCreatePinnedToCore(&this->hostInterface, "mouseIf", 4096, this, 25, &this->TaskHostIF, 1); + vTaskDelay(500); +} + +// Initialisation routine without hardware. +void Mouse::init(NVS *hdlNVS, HID *hdlHID) +{ + // Invoke the prototype init which initialises common variables and devices shared by all subclass. + KeyInterface::init(getClassName(__PRETTY_FUNCTION__), hdlNVS, hdlHID); + + // Retrieve configuration, if it doesnt exist, set defaults. + // + if(nvs->retrieveData(getClassName(__PRETTY_FUNCTION__), &this->mouseConfig, sizeof(t_mouseConfig)) == false) + { + ESP_LOGW(MAINTAG, "Mouse configuration set to default, no valid config in NVS found."); + mouseConfig.mouse.resolution= HID::HID_MOUSE_RESOLUTION_1_8; + mouseConfig.mouse.scaling = HID::HID_MOUSE_SCALING_1_1; + mouseConfig.mouse.sampleRate= HID::HID_MOUSE_SAMPLE_RATE_60; + mouseConfig.host.scaling = HID::HID_MOUSE_HOST_SCALING_1_2; + + // Persist the data for next time. + if(nvs->persistData(getClassName(__PRETTY_FUNCTION__), &this->mouseConfig, sizeof(t_mouseConfig)) == false) + { + ESP_LOGW(MAINTAG, "Persisting Default Mouse configuration data failed, check NVS setup.\n"); + } + // Few other updates so make a commit here to ensure data is flushed and written. + else if(nvs->commitData() == false) + { + ESP_LOGW(MAINTAG, "NVS Commit writes operation failed, some previous writes may not persist in future power cycles."); + } + } +} + +// Constructor, basically initialise the Singleton interface and let the threads loose. +Mouse::Mouse(uint32_t ifMode, NVS *hdlNVS, LED *hdlLED, HID *hdlHID) +{ + // Operating in uni-mode. + hostControl.secondaryIf = false; + + // Initialise the interface + init(ifMode, hdlNVS, hdlLED, hdlHID); +} + +// Constructor, basic initialisation without hardware. +Mouse::Mouse(NVS *hdlNVS, HID *hdlHID) +{ + // Operating in uni-mode. + hostControl.secondaryIf = false; + + // Initialise the interface + init(hdlNVS, hdlHID); +} + +// Constructor for use when mouse operates in tandem with a keyboard. +Mouse::Mouse(uint32_t ifMode, NVS *hdlNVS, LED *hdlLED, HID *hdlHID, bool secondaryIf) +{ + // The interface can act in primary mode, ie. sole interface or secondary mode where it acts in tandem to a keyboard host. Slight processing differences occur + // in secondary mode, for example, the pin used to output mouse data differs. + hostControl.secondaryIf = secondaryIf; + + // Initialise the interface + init(ifMode, hdlNVS, hdlLED, hdlHID); +} + +// Constructor, used for version reporting so no hardware is initialised. +Mouse::Mouse(void) +{ + return; +} + +// Destructor - only ever called when the class is used for version reporting. +Mouse::~Mouse(void) +{ + return; +} diff --git a/main/NVS.cpp b/main/NVS.cpp deleted file mode 120000 index cd4cee6..0000000 --- a/main/NVS.cpp +++ /dev/null @@ -1 +0,0 @@ -../../sharpkey/main/NVS.cpp \ No newline at end of file diff --git a/main/NVS.cpp b/main/NVS.cpp new file mode 100644 index 0000000..53a6132 --- /dev/null +++ b/main/NVS.cpp @@ -0,0 +1,293 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// Name: NVS.cpp +// Created: Mar 2022 +// Version: v1.0 +// Author(s): Philip Smart +// Description: Base class for encapsulating the Espressif C API for the Non Volatile Storage. +// Credits: +// Copyright: (c) 2019-2022 Philip Smart +// +// History: Mar 2022 - Initial write. +// v1.01 May 2022 - Initial release version. +// +// 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 "freertos/semphr.h" +#include "esp_log.h" +#include "esp_system.h" +#include "nvs_flash.h" +#include "nvs.h" +#include "driver/gpio.h" +#include "soc/timer_group_struct.h" +#include "soc/timer_group_reg.h" +#include "driver/timer.h" +#include "sdkconfig.h" +#include "NVS.h" + +// Method to externally take the NVS mutex for situations where another IDF module requires access to the NVS subsystem. +// +bool NVS::takeMutex(void) +{ + // Locals. + bool result = false; + + // Ensure a handle has been opened to the NVS. + if(nvsCtrl.nvsHandle != (nvs_handle_t)0) + { + // Request exclusive access. + if(xSemaphoreTake(nvsCtrl.mutexInternal, (TickType_t)1000) == pdTRUE) + { + result = true; + } + } + return(result); +} + +// Method to release the NVS mutex previously taken. +void NVS::giveMutex(void) +{ + // Locals. + + // Release mutex, external access now possible to the input devices. + xSemaphoreGive(nvsCtrl.mutexInternal); +} + +// Method to persist data into the NVS RAM. This method takes a pointer to any memory object and writes it into the NVS using the handle opened at initialisation time. +// +bool NVS::persistData(const char *key, void *pData, uint32_t size) +{ + // Locals. + // + esp_err_t nvsStatus; + bool result = true; + #define NVSPERSISTTAG "persistData" + + // Ensure a handle has been opened to the NVS. + if(nvsCtrl.nvsHandle != (nvs_handle_t)0) + { + // Ensure we have exclusive access before accessing NVS. + if(xSemaphoreTake(nvsCtrl.mutexInternal, (TickType_t)1000) == pdTRUE) + { + // Write a binary blob of data straight from memory pointed to by pData for readSize bytes into the NVS. This allows for individual variables or entire structures. + nvsStatus = nvs_set_blob(this->nvsCtrl.nvsHandle, key, pData, size); + if(nvsStatus != ESP_OK) + { + ESP_LOGW(NVSPERSISTTAG, "Failed to persist NVS data, key:%s, size:%d, nvsStatus:%d", key, size, nvsStatus); + result = false; + } + } else + { + result = false; + } + } else + { + result = false; + } + + // NB: Mutex only released in COMMIT. + + // Return result code. + return(result); +} + +// Method to retrieve persisted data from the NVS RAM. This method takes a pointer to a pre-allocated memoery block along with size and retrieves a data block from NVS upto size bytes. +// +bool NVS::retrieveData(const char *key, void *pData, uint32_t size) +{ + // Locals. + // + esp_err_t nvsStatus; + size_t readSize = size; + bool result = true; + #define NVSRTRVTAG "retrieveData" + + // Ensure a handle has been opened to the NVS. + if(nvsCtrl.nvsHandle != (nvs_handle_t)0) + { + // Ensure we have exclusive access before accessing NVS. + if(xSemaphoreTake(nvsCtrl.mutexInternal, (TickType_t)1000) == pdTRUE) + { + // Get a binary blob of data straight into the memory pointed to by pData for readSize. This allows for individual variables or entire structures. + nvsStatus = nvs_get_blob(this->nvsCtrl.nvsHandle, key, pData, &readSize); + if(nvsStatus != ESP_OK || readSize != size) + { + ESP_LOGW(NVSRTRVTAG, "Failed to retrieve NVS data, key:%s, size:%d, requested size:%d, nvsStatus:%d", key, readSize, size, nvsStatus); + result = false; + } + + // Release mutex, external access now possible to the input devices. + xSemaphoreGive(nvsCtrl.mutexInternal); + } else + { + result = false; + } + } else + { + result = false; + } + + // Return result code. + return(result); +} + +// Method to ensure all data written to NVS is flushed and committed. This step is necessary as a write may be buffered and requires flushing to ensure persistence. +// +bool NVS::commitData(void) +{ + // Locals. + // + esp_err_t nvsStatus; + bool result = true; + #define NVSCOMMITTAG "commitData" + + // Ensure a handle has been opened to the NVS. + if(nvsCtrl.nvsHandle != (nvs_handle_t)0) + { + + // Check that the Mutex has been taken, if we grab it then it hasnt been taken in the persistData method, so exit as a call to persistData is mandatory. + if(xSemaphoreTake(nvsCtrl.mutexInternal, (TickType_t)0) == pdTRUE) + { + xSemaphoreGive(nvsCtrl.mutexInternal); + } else + { + // Request a commit transaction and return response accordingly. + nvsStatus = nvs_commit(this->nvsCtrl.nvsHandle); + if(nvsStatus != ESP_OK) + { + ESP_LOGW(NVSCOMMITTAG, "Failed to commit pending NVS data."); + result = false; + } + + // Release mutex, external access now possible to the input devices. + xSemaphoreGive(nvsCtrl.mutexInternal); + } + } else + { + result = false; + } + + // Return result code. + return(result); +} + +// Method to erase all the NVS and return to factory default state. The method closes any open handle, +// de-initialises the NVS then performs a flash erase. +// +void NVS::eraseAll(void) +{ + // Locals. + // + #define NVSERATAG "eraseAll" + + // Ensure we have exclusive access before accessing NVS. + while(xSemaphoreTake(nvsCtrl.mutexInternal, (TickType_t)1000) != pdTRUE); + + // Ensure a handle has been opened to the NVS. + if(nvsCtrl.nvsHandle != (nvs_handle_t)0) + { + // Close open handle. + nvs_close(nvsCtrl.nvsHandle); + nvsCtrl.nvsHandle = NULL; + } + + // Stop the flash driver. + nvs_flash_deinit(); + + ESP_LOGW(NVSERATAG, "Erasing flash, disable for production!\n"); + ESP_ERROR_CHECK(nvs_flash_erase()); + + // Release mutex, external access now possible to the input devices. + xSemaphoreGive(nvsCtrl.mutexInternal); + + return; +} + +// Method to initialise the NVS subsystem. +void NVS::init(void) +{ + // Locals. + esp_err_t nvsStatus; + #define NVSINITTAG "nvsInit" + + // Initialise variables. + nvsCtrl.nvsHandle = (nvs_handle_t)0; + + //ESP_LOGW(NVSINITTAG, "Erasing flash, disable for production!\n"); + //ESP_ERROR_CHECK(nvs_flash_erase()); + + // Initialize NVS + ESP_LOGW(NVSINITTAG, "Initialising NVS."); + nvsStatus = nvs_flash_init(); + if(nvsStatus == ESP_ERR_NVS_NO_FREE_PAGES || nvsStatus == ESP_ERR_NVS_NEW_VERSION_FOUND) + { + // NVS partition was truncated and needs to be erased + ESP_ERROR_CHECK(nvs_flash_erase()); + + // Retry nvs_flash_init + nvsStatus = nvs_flash_init(); + } + ESP_ERROR_CHECK(nvsStatus); + + // Setup mutex's. + nvsCtrl.mutexInternal = xSemaphoreCreateMutex(); + + return; +} + +// Method to open a namespace on the NVS given a key. +// +bool NVS::open(std::string keyName) +{ + // Locals. + bool result = true; + #define NVSOPENTAG "nvsOpen" + + // Only process if no handle has been opened. Currently only coded for one session at a time. + if(nvsCtrl.nvsHandle == (nvs_handle_t)0) + { + // Store the key name under which all data is stored. + this->nvsCtrl.nvsKeyName = keyName; + + // Open handle to persistence using the base-class name as the key which represents the global namespace. Sub-classes and objects accessing the public methods will + // use there own class name as a sub-key which represents the class namespace within NVS. Data is then stored within the class namespace using a key:value pair. + esp_err_t nvsStatus = nvs_open(nvsCtrl.nvsKeyName.c_str(), NVS_READWRITE, &this->nvsCtrl.nvsHandle); + if (nvsStatus != ESP_OK) + { + ESP_LOGW(NVSOPENTAG, "Error (%s) opening NVS handle!\n", esp_err_to_name(nvsStatus)); + result = false; + } + } else + { + result = false; + } + return(result); +} + +// Basic constructor, init variables! +NVS::NVS(void) +{ + // Store the class name for later use, ie. NVS key access. + this->nvsCtrl.nvsClassName = getClassName(__PRETTY_FUNCTION__); +} diff --git a/main/PC9801.cpp b/main/PC9801.cpp deleted file mode 120000 index 1e56610..0000000 --- a/main/PC9801.cpp +++ /dev/null @@ -1 +0,0 @@ -../../sharpkey/main/PC9801.cpp \ No newline at end of file diff --git a/main/PC9801.cpp b/main/PC9801.cpp new file mode 100644 index 0000000..79e25e9 --- /dev/null +++ b/main/PC9801.cpp @@ -0,0 +1,1054 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// Name: PC9801.cpp +// Created: Apr 2022 +// Version: v1.0 +// Author(s): Philip Smart +// Description: HID (PS/2 or BT keyboard) to NEC PC-9801 series Interface logic. +// This source file contains the singleton class containing logic to obtain +// PS/2 or BT scan codes, map them into NEC PC-9801 key codes and transmit the key +// data to the NEC PC-9801 host. +// +// The class uses a modified version of the PS2KeyAdvanced +// https://github.com/techpaul/PS2KeyAdvanced class from Paul Carpenter. +// +// The whole application of which this class is a member, uses the Espressif Development +// environment with Arduino components. This is necessary for the PS2KeyAdvanced class, +// which I may in future convert to use esp-idf library calls rather than Arduino. +// +// Credits: +// Copyright: (c) 2022 Philip Smart +// +// History: Apr 2022 - Initial framework, waiting on arrival of real machine to progress further. +// v1.01 Jun 2022 - Updates to reflect changes realised in other modules due to addition of +// bluetooth and suspend logic due to NVS issues using both cores. +// +// 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 +#include +#include +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/queue.h" +#include "driver/uart.h" +#include "driver/gpio.h" +#include "esp_log.h" +#include "soc/timer_group_struct.h" +#include "soc/timer_group_reg.h" +#include "driver/timer.h" +#include "sys/stat.h" +#include "esp_littlefs.h" +#include "PS2KeyAdvanced.h" +#include "sdkconfig.h" +#include "PC9801.h" + +// Tag for ESP main application logging. +#define MAINTAG "pc9801key" + +// FreeRTOS Queue handle to pass messages from the HID Keyboard Mapper into the PC9801 transmission logic. +static QueueHandle_t xmitQueue; +static QueueHandle_t rcvQueue; + +// NEC PC-9801 Protocol +// -------------------- +// +// The NEC PC-9801 uses an asynchronous serial protocol over two wires (+ 2 control wires and +5V/GND). The basic protocol is 19200, 8 bits, 1 start bit, 1 stop bit, ODD parity at TTL level (ie. not RS-232). +// The 4 signals are (from the SharpKey side): +// DATA (TxD) - Data sent serially from SharpKey to PC-9801 +// /RST (RxD) - Reset signal from PC-9801 to SharpKey, active low. Doubles up for later models as the Rxd Serial connection for the PC-9801 to send the SharpKey some commands. +// /RDY - Ready signal from PC-9801 to SharpKey, active low. When active, PC-9801 can receive key data. +// /RTY - Retry signal from PC-9801 to SharpKey, active low. When active, PC-9801 requires the SharpKey to retransmit the last key. +// +//// Protocol: +// Idle State (RXD/TXD) = High. +// +// +// The following keymaps (from the TMK project, https://github.com/tmk/tmk_keyboard/wiki/PC-9801-Keyboard) show the PC-9801 keyboard layout +// and generated scan codes. +// +// ,---. ,---. ,------------------------. ,------------------------. ,------------------------. +// | 60| | 61| | 62 | 63 | 64 | 65 | 66 | | 67 | 68 | 69 | 6A | 6B | | 52 | 53 | 54 | 55 | 56 | +// `---' `---' `------------------------' `------------------------' `------------------------' +// ,-------------------------------------------------------------. ,-------. ,---------------. +// | 00 | 01| 02| 03| 04| 05| 06| 07| 08| 09| 0A| 0B| 0C| 0D| 0E | | 38| 39| | 3E| 3F| 4D| 41| +// |-------------------------------------------------------------| |-------| |---------------| +// | 0F | 10| 11| 12| 13| 14| 15| 16| 17| 18| 19| 1A| 1B| | | 36| 37| | 42| 43| 44| 45| +// |-------------------------------------------------------` 1C | |-------| |---------------| +// | 74| 71| 1D| 1E| 1F| 20| 21| 22| 23| 24| 25| 26| 27| 28| | | 3A | | 46| 47| 48| 49| +// |-------------------------------------------------------------| |-------| |---------------| +// | 70 | 29| 2A| 2B| 2C| 2D| 2E| 2F| 30| 31| 32| 33| 70/7D | | 3B| 3C| | 4A| 4B| 4C| 4D| +// `-------------------------------------------------------------' |-------| |---------------| +// | 72 | 77| 73| 51 | 34 | 35 | 79| 78| 5E| | 3D | | 4E| 4F| 50| 1C| +// `-------------------------------------------------------' `-------' `---------------' +// 5E: HOME on PC98XL keyboard +// 79: ALT on DIGITAL WAVE Dboard +// 77,78,79: Left Win, right Win and Application on PC-9821 keyboard +// +// ,---. ,---. ,------------------------. ,------------------------. ,------------------------. +// |Stp| |Cpy| | F1 | F2 | F3 | F4 | F5 | | F6 | F7 | F8 | F9 | F10| | F11| F12| F13| F14| F15| +// `---' `---' `------------------------' `------------------------' `------------------------' +// ,-------------------------------------------------------------. ,-------. ,---------------. +// | Esc| 1| 2| 3| 4| 5| 6| 7| 8| 9| 0| -| =|JPY|Bspc| |Ins|Del| |Hom|Hlp| -| /| +// |-------------------------------------------------------------| |-------| |---------------| +// | Tab | Q| W| E| R| T| Y| U| I| O| P| [| ]| | |RUp|RDn| | 7| 8| 9| *| +// |-------------------------------------------------------`Enter| |-------| |---------------| +// |Ctl|Cap| A| S| D| F| G| H| J| K| L| ;| '| \| | | Up | | 4| 5| 6| +| +// |-------------------------------------------------------------| |-------| |---------------| +// | Shift | Z| X| C| V| B| N| M| ,| .| /| RO| Shift | |Lef|Rig| | 1| 2| 3| =| +// `-------------------------------------------------------------' |-------| |---------------| +// |Kana|Win|GRP|NFER| Space |XFER| ^2|Win| ^3| | Down | | 0| ,| .| ^1| +// `-------------------------------------------------------' `-------' `---------------' +// ^1: Enter on keypad is identical to Enter on alphanumeric portion +// ^2: ALT on DIGITAL WAVE Dboard while Menu on PC-9821 keyboard +// ^3: HOME on PC98XL keyboard +// ^4: CAPS and Kana are locking keys, either mechanically or by firmware emulation +// +// The following keymaps are for the later NEC keyboad, PC-9801-114 KBDPO3 PC-PTOS +// ,-------------------------------------------------------------------------------------------. +// | 73| 62| 63| 64| 65| 66| 67| 68| 69| 6A| 6B| 52| 53| 54| 55| 56| 36| 37| | | | | | +// `-------------------------------------------------------------------------------------------' +// ,-------------------------------------------------------------. ,-------. ,-------------------. +// | 00 | 01| 02| 03| 04| 05| 06| 07| 08| 09| 0A| 0B| 0C| 0D| 0E | | 38| 39| | 3F| 3E| 61| 4D| 60| +// |-------------------------------------------------------------| |-------| |-------------------| +// | 0F | 10| 11| 12| 13| 14| 15| 16| 17| 18| 19| 1A| 1B| | | 3A | | 42| 43| 44| | 41| +// |-------------------------------------------------------` 1C | |-------| |-------------------| +// | 74| 71| 1D| 1E| 1F| 20| 21| 22| 23| 24| 25| 26| 27| 28| | | 3B| 3C| | 46| 47| 48| | 45| +// |-------------------------------------------------------------| |-------| |-------------------| +// | | 70 | 29| 2A| 2B| 2C| 2D| 2E| 2F| 30| 31| 32| 33| 70/7D | | 3D | | 4A| 4B| 4C| | 4D| +// |-------------------------------------------------------------| |-------| |-----------| 1C|---| +// | | | 51 | 34 | 35 | | 72 | | | | | 4E| 4F| 50| | 49| +// `-------------------------------------------------------------' `-------' `-------------------' +// +// ,-------------------------------------------------------------------------------------------. +// |FNC| F1| F2| F3| F4| F5| F6| F7| F8| F9|F10|F11|F12|F13|F14|F15|F16|F17|F18|F19|F20|F21|F22| +// `-------------------------------------------------------------------------------------------' +// ,-------------------------------------------------------------. ,-------. ,-------------------. +// | Esc| 1| 2| 3| 4| 5| 6| 7| 8| 9| 0| -| ^|JPY|Bskp| |Skp|Rst| |Hlp|Hom|Cpy| =|Stp| +// |-------------------------------------------------------------| |-------| |-------------------| +// | Tab | Q| W| E| R| T| Y| U| I| O| P| @| [| | | Up | | 7| 8| 9| II| /| +// |-------------------------------------------------------`Enter| |-------| |-------------------| +// |Ctl|Cap| A| S| D| F| G| H| J| K| L| ;| :| ]| | |Lef|Rig| | 4| 5| 6| I| *| +// |-------------------------------------------------------------| |-------| |-------------------| +// |SW1|Shift| Z| X| C| V| B| N| M| ,| .| /| RO| Shift | | Down | | 1| 2| 3| | -| +// |-------------------------------------------------------------' |-------| |-----------|Ent|---| +// |SW2| Eisu | MHEN | Space | HENK | HIRA | KANA |Z/H| | Exec | | 0|000| .| | +| +// `-------------------------------------------------------------' `-------' `-------------------' + + +// Function to push a keycode onto the key queue ready for transmission. +// +IRAM_ATTR void PC9801::pushKeyToQueue(uint32_t key) +{ + // Locals. + t_xmitQueueMessage xmitMsg; + #define PUSHKEYTAG "pushKeyToQueue" + + xmitMsg.keyCode = key; + if( xQueueSend(xmitQueue, (void *)&xmitMsg, 10) != pdPASS) + { + ESP_LOGW(PUSHKEYTAG, "Failed to put scancode:%04x into xmitQueue", key); + } + return; +} + +// Function to push a host command onto the processing queue. +// +IRAM_ATTR void PC9801::pushHostCmdToQueue(uint8_t cmd) +{ + // Locals. + t_rcvQueueMessage rcvMsg; + #define PUSHCMDTAG "pushHostCmdToQueue" + + rcvMsg.hostCmd = cmd; + if( xQueueSend(rcvQueue, (void *)&rcvMsg, 10) != pdPASS) + { + ESP_LOGW(PUSHCMDTAG, "Failed to put host command:%02x onto rcvQueue", cmd); + } + return; +} + +// Method to realise the NEC PC-9801 4 wire serial protocol in order to transmit key presses to the +// NEC PC-9801. +// +// This method uses Core 1 and it will hold it in a spinlock as necessary to ensure accurate timing. +// A key is passed into the method via the FreeRTOS Queue handle xmitQueue. +// +IRAM_ATTR void PC9801::pcInterface( void * pvParameters ) +{ + // Locals. + t_xmitQueueMessage rcvMsg; + uint8_t uartData[128]; + int uartXmitCnt; + size_t uartRcvCnt; + + // Retrieve pointer to object in order to access data. + PC9801* pThis = (PC9801*)pvParameters; + + // Initialise the MUTEX which prevents this core from being released to other tasks. + pThis->pcMutex = portMUX_INITIALIZER_UNLOCKED; + + // Initial delay needed because the xQueue will assert probably on a suspended task ALL if delay not inserted! + vTaskDelay(1000); + + // Sign on. + ESP_LOGW(MAINTAG, "Starting NEC PC-9801 thread."); + + // Permanent loop, wait for an incoming message on the key to send queue, read it then transmit to the PC-9801, repeat! + for(;;) + { + // Check stack space, report if it is getting low. + if(uxTaskGetStackHighWaterMark(NULL) < 1024) + { + ESP_LOGW(MAINTAG, "THREAD STACK SPACE(%d)\n",uxTaskGetStackHighWaterMark(NULL)); + } + + if(xQueueReceive(xmitQueue, (void *)&rcvMsg, 0) == pdTRUE) + { + ESP_LOGW(MAINTAG, "Received:%08x\n", rcvMsg.keyCode); + + // Allow for multi byte transmissions, MSB sent first. + if(rcvMsg.keyCode != 0x00000000) + { + uartXmitCnt = 0; + while((rcvMsg.keyCode & 0xff000000) == 0x00) { rcvMsg.keyCode = rcvMsg.keyCode << 8; } + for(int idx=0; idx < 4 && (rcvMsg.keyCode & 0xff000000) != 0x00; idx++) + { + if((rcvMsg.keyCode & 0xff000000) != 0) + { + uartData[idx] = (uint8_t)((rcvMsg.keyCode & 0xFF000000) >> 24); + uartXmitCnt++; + } + rcvMsg.keyCode = rcvMsg.keyCode << 8; + } + if(uartXmitCnt > 0) + { + ESP_LOGW(MAINTAG, "Received:%08x, Count=%d\n", rcvMsg.keyCode, uartXmitCnt); + uart_write_bytes(pThis->pcCtrl.uartNum, (const char *)uartData, uartXmitCnt); + } + } + } + + // Get command data from the PC-9801 if any sent- this is where the PC-9801, later models, can send any relevant commands for processing. + uart_get_buffered_data_len(pThis->pcCtrl.uartNum, &uartRcvCnt); + if(uartRcvCnt > 0) + { + do { + uartRcvCnt = uart_read_bytes(pThis->pcCtrl.uartNum, uartData, (128 - 1), 20 / portTICK_PERIOD_MS); + for(int idx=0; idx < uartRcvCnt; idx++) + { + // Filter out polling commands and send valid commands to the rcvQueue. + if(uartData[idx] != 0x40 && uartData[idx] != 0x41) + { + pThis->pushHostCmdToQueue(uartData[idx]); + } + } + } while(uartRcvCnt > 0); + } + + // Yield if the suspend flag is set. + pThis->yield(50); + + // Logic to feed the watchdog if needed. Watchdog disabled in menuconfig but if enabled this will need to be used. + //TIMERG0.wdt_wprotect=TIMG_WDT_WKEY_VALUE; // write enable + //TIMERG0.wdt_feed=1; // feed dog + //TIMERG0.wdt_wprotect=0; // write protect + //TIMERG1.wdt_wprotect=TIMG_WDT_WKEY_VALUE; // write enable + //TIMERG1.wdt_feed=1; // feed dog + //TIMERG1.wdt_wprotect=0; // write protect + } +} + +// Method to select keyboard configuration options. When a key sequence is pressed, ie. SHIFT+CTRL+ESC then the fourth simultaneous key is the required option and given to this +// method to act on. Options can be machine model, keyboard map etc. +// +void PC9801::selectOption(uint8_t optionCode) +{ + // Locals. + // + bool updated = true; + #define SELOPTTAG "selectOption" + + // Simple switch to decode the required option and act on it. + switch(optionCode) + { + // Select a keymap using 1..8 or default (STANDARD) using 0. + case PS2_KEY_1: + this->pcConfig.params.activeKeyboardMap = KEYMAP_UK_WYSE_KB3926; + break; + case PS2_KEY_2: + this->pcConfig.params.activeKeyboardMap = KEYMAP_JAPAN_OADG109; + break; + case PS2_KEY_3: + this->pcConfig.params.activeKeyboardMap = KEYMAP_JAPAN_SANWA_SKBL1; + break; + case PS2_KEY_4: + this->pcConfig.params.activeKeyboardMap = KEYMAP_NOT_ASSIGNED_4; + break; + case PS2_KEY_5: + this->pcConfig.params.activeKeyboardMap = KEYMAP_NOT_ASSIGNED_5; + break; + case PS2_KEY_6: + this->pcConfig.params.activeKeyboardMap = KEYMAP_NOT_ASSIGNED_6; + break; + case PS2_KEY_7: + this->pcConfig.params.activeKeyboardMap = KEYMAP_UK_PERIBOARD_810; + break; + case PS2_KEY_8: + this->pcConfig.params.activeKeyboardMap = KEYMAP_UK_OMOTON_K8508; + break; + case PS2_KEY_0: + this->pcConfig.params.activeKeyboardMap = KEYMAP_STANDARD; + break; + + // Unknown option so ignore. + default: + updated = false; + break; + } + + // If an update was made, persist it for power cycles. + // + if(updated) + { + this->pcCtrl.persistConfig = true; + } + + return; +} + +// Method to take a PS/2 key and control data and map it into an NEC PC-9801 key and control equivalent, updating state values accordingly (ie. CAPS). +// A mapping table is used which maps a key and state values into an NEC PC-9801 key and control values, the emphasis being on readability and easy configuration +// as opposed to concatenated byte tables. +// +uint32_t PC9801::mapKey(uint16_t scanCode) +{ + // Locals. + uint32_t idx; + uint8_t keyCode = (scanCode & 0xFF); + bool mapped = false; + bool matchExact = false; + uint32_t mappedKey = 0x00000000; + #define MAPKEYTAG "mapKey" + + // Intercept control keys and set state variables. + // + // + if(scanCode & PS2_BREAK) + { + // if((keyCode == PS2_KEY_L_SHIFT || keyCode == PS2_KEY_R_SHIFT) && (scanCode & PS2_SHIFT) == 0) { mapped=true; this->pcCtrl.keyCtrl |= PC9801_CTRL_SHIFT; } +// if(keyCode == PS2_KEY_KANA && (scanCode & PS2_CTRL) == 0) { mapped=true; this->pcCtrl.keyCtrl &= ~PC9801_CTRL_KANA; } + + // Any break key clears the option select flag. + this->pcCtrl.optionSelect = false; + + // Clear any feature LED blinking. + led->setLEDMode(LED::LED_MODE_OFF, LED::LED_DUTY_CYCLE_OFF, 0, 0L, 0L); + } else + { + // if((keyCode == PS2_KEY_L_SHIFT || keyCode == PS2_KEY_R_SHIFT) && (scanCode & PS2_SHIFT)) { mapped=true; this->pcCtrl.keyCtrl &= ~PC9801_CTRL_SHIFT; } +// if(keyCode == PS2_KEY_KANA && (scanCode & PS2_CTRL)) { mapped=true; this->pcCtrl.keyCtrl |= PC9801_CTRL_KANA; } + // Special mapping to allow selection of keyboard options. If the user presses CTRL+SHIFT+ESC then a flag becomes active and should a fourth key be pressed before a BREAK then the fourth key is taken as an option key and processed accordingly. + if(this->pcCtrl.optionSelect == true && keyCode != PS2_KEY_ESC) + { + mapped = true; + this->pcCtrl.optionSelect = false; + selectOption(keyCode); + } + if(keyCode == PS2_KEY_ESC && (scanCode & PS2_CTRL) && (scanCode & PS2_SHIFT) && this->pcCtrl.optionSelect == false) + { + // Prime flag ready for fourth option key and start LED blinking periodically. + mapped = true; + this->pcCtrl.optionSelect = true; + led->setLEDMode(LED::LED_MODE_BLINK, LED::LED_DUTY_CYCLE_50, 1, 500L, 500L); + } + } + + // If the key has been mapped as a special key, no further processing. + if(mapped == true) + { + ESP_LOGW(MAPKEYTAG, "Mapped special key:%02x\n", this->pcCtrl.keyCtrl); + // mappedKey = (this->pcCtrl.keyCtrl << 8) | 0x00; + } else + { + // Loop through the entire conversion table to find a match on this key, if found map to X68000 equivalent. + // switch matrix. + // + for(idx=0, mapped=false, matchExact=false; idx < pcCtrl.kmeRows && (mapped == false || (mapped == true && matchExact == false)); idx++) + { + // Match key code? Make sure the current machine and keymap match as well. + if(pcCtrl.kme[idx].ps2KeyCode == (uint8_t)(scanCode&0xFF) && ((pcCtrl.kme[idx].machine == PC9801_ALL) || ((pcCtrl.kme[idx].machine & pcConfig.params.activeMachineModel) != 0)) && ((pcCtrl.kme[idx].keyboardModel & pcConfig.params.activeKeyboardMap) != 0)) + { + // Match Raw, Shift, Function, Control, ALT or ALT-Gr? + //if( (((pcCtrl.kme[idx].ps2Ctrl & PS2CTRL_SHIFT) == 0) && ((pcCtrl.kme[idx].ps2Ctrl & PS2CTRL_CTRL) == 0) && ((pcCtrl.kme[idx].ps2Ctrl & PS2CTRL_KANA) == 0) && ((pcCtrl.kme[idx].ps2Ctrl & PS2CTRL_GRAPH) == 0) && ((pcCtrl.kme[idx].ps2Ctrl & PS2CTRL_GUI) == 0) && ((pcCtrl.kme[idx].ps2Ctrl & PS2CTRL_FUNC) == 0)) || + if( (((pcCtrl.kme[idx].ps2Ctrl & PS2CTRL_SHIFT) == 0) && ((pcCtrl.kme[idx].ps2Ctrl & PS2CTRL_CTRL) == 0) && ((pcCtrl.kme[idx].ps2Ctrl & PS2CTRL_GRAPH) == 0) && ((pcCtrl.kme[idx].ps2Ctrl & PS2CTRL_GUI) == 0) && ((pcCtrl.kme[idx].ps2Ctrl & PS2CTRL_FUNC) == 0)) || + ((scanCode & PS2_SHIFT) && (pcCtrl.kme[idx].ps2Ctrl & PS2CTRL_SHIFT) != 0) || + ((scanCode & PS2_CTRL) && (pcCtrl.kme[idx].ps2Ctrl & PS2CTRL_CTRL) != 0) || + ((scanCode & PS2_GUI) && (pcCtrl.kme[idx].ps2Ctrl & PS2CTRL_GUI) != 0) || + // ((this->pcCtrl.keyCtrl & PC9801_CTRL_KANA) && (pcCtrl.kme[idx].ps2Ctrl & PS2CTRL_KANA)!= 0) || + // ((scanCode & PS2_CAPS) && (pcCtrl.kme[idx].ps2Ctrl & PS2CTRL_CAPS) != 0) || + ((scanCode & PS2_FUNCTION) && (pcCtrl.kme[idx].ps2Ctrl & PS2CTRL_FUNC) != 0) ) + { + + // Exact entry match, data + control key? On an exact match we only process the first key. On a data only match we fall through to include additional data and control key matches to allow for un-mapped key combinations, ie. Japanese characters. + matchExact = (((scanCode & PS2_SHIFT) && (pcCtrl.kme[idx].ps2Ctrl & PS2CTRL_SHIFT) != 0) || ((scanCode & PS2_SHIFT) == 0 && (pcCtrl.kme[idx].ps2Ctrl & PS2CTRL_SHIFT) == 0)) && + (((scanCode & PS2_CTRL) && (pcCtrl.kme[idx].ps2Ctrl & PS2CTRL_CTRL) != 0) || ((scanCode & PS2_CTRL) == 0 && (pcCtrl.kme[idx].ps2Ctrl & PS2CTRL_CTRL) == 0)) && + (((scanCode & PS2_GUI) && (pcCtrl.kme[idx].ps2Ctrl & PS2CTRL_GUI) != 0) || ((scanCode & PS2_GUI) == 0 && (pcCtrl.kme[idx].ps2Ctrl & PS2CTRL_GUI) == 0)) && + // (((this->pcCtrl.keyCtrl & PC9801_CTRL_KANA) && (pcCtrl.kme[idx].ps2Ctrl & PS2CTRL_KANA)!= 0) || ((this->pcCtrl.keyCtrl & PC9801_CTRL_KANA) == 0 && (pcCtrl.kme[idx].ps2Ctrl & PS2CTRL_KANA)== 0)) && + // (((scanCode & PS2_CAPS) && (pcCtrl.kme[idx].ps2Ctrl & PS2CTRL_CAPS) != 0) || ((scanCode & PS2_GUI) == 0 && (pcCtrl.kme[idx].ps2Ctrl & PS2CTRL_CAPS) == 0)) && + (((scanCode & PS2_FUNCTION) && (pcCtrl.kme[idx].ps2Ctrl & PS2CTRL_FUNC) != 0) || ((scanCode & PS2_FUNCTION) == 0 && (pcCtrl.kme[idx].ps2Ctrl & PS2CTRL_FUNC) == 0)) + ? true : false; + + // RELEASE (PS2_BREAK == 1) or PRESS? + if((scanCode & PS2_BREAK)) + { + // Special case for the PAUSE / BREAK key. The underlying logic has been modified to send a BREAK key event immediately + // after a PAUSE make, this is necessary as the Sharp machines require SHIFT (pause) BREAK so the PS/2 CTRL+BREAK wont + // work (unless logic is added to insert a SHIFT, pause, add BREAK). The solution was to generate a BREAK event + // when SHIFT+PAUSE is pressed. + if(keyCode == PS2_KEY_PAUSE) + { + vTaskDelay(100); + } + mappedKey = 0x80 | (pcCtrl.kme[idx].pcKey & 0x7F); + mapped = true; + } else + { + // Map key actioning any control overrides. + if((pcCtrl.kme[idx].pcCtrl & PC9801_CTRL_RELEASESHIFT) != 0) + { + // RELEASESHIFT infers that the X68000 must cancel the current shift status prior to receiving the key code. This is necessary when using foreign keyboards and a character appears + // on a shifted key whereas on the original X68000 keyboard the character is the primary key. + // + mappedKey = ((0x80 | PC9801_KEY_SHIFT) << 16) | 0x00 | ((pcCtrl.kme[idx].pcKey & 0x7F) << 8) | (0x00 | PC9801_KEY_SHIFT); + } else + if((pcCtrl.kme[idx].pcCtrl & PC9801_CTRL_SHIFT) != 0) + { + // SHIFT infers that the X68000 must invoke shift status prior to receiving the key code. This is necessary when using foreign keyboards and a character appears + // as a primary key on the foreign keyboard but as a shifted key on the X68000 keyboard. + // + mappedKey = ((0x00 | PC9801_KEY_SHIFT) << 16) | 0x00 | ((pcCtrl.kme[idx].pcKey & 0x7F) << 8) | (0x80 | PC9801_KEY_SHIFT); + } + else + { + mappedKey = 0x00 | (pcCtrl.kme[idx].pcKey & 0x7F); + } + mapped = true; + } + } + } + } + } + return(mappedKey); +} + +// Primary HID thread, running on Core 0. +// This thread is responsible for receiving HID (PS/2 or BT) keyboard scan codes and mapping them to Sharp PC9801 equivalent keys, updating state flags as needed. +// The HID data is received via interrupt. The data to be sent to the PC9801 is pushed onto a FIFO queue. +// +IRAM_ATTR void PC9801::hidInterface( void * pvParameters ) +{ + // Locals. + uint16_t scanCode = 0x0000; + uint32_t pcKey = 0x00000000; + t_rcvQueueMessage rcvMsg; + + // Map the instantiating object so we can access its methods and data. + PC9801* pThis = (PC9801*)pvParameters; + + // Thread never exits, just polls the keyboard and updates the matrix. + while(1) + { + // Check stack space, report if it is getting low. + if(uxTaskGetStackHighWaterMark(NULL) < 1024) + { + ESP_LOGW(MAINTAG, "THREAD STACK SPACE(%d)\n",uxTaskGetStackHighWaterMark(NULL)); + } + + // Check for HID keyboard scan codes. + while((scanCode = pThis->hid->read()) != 0) + { + // Scan Code Breakdown: + // Define name bit description + // PS2_BREAK 15 1 = Break key code + // (MSB) 0 = Make Key code + // PS2_SHIFT 14 1 = Shift key pressed as well (either side) + // 0 = No shift key + // PS2_CTRL 13 1 = Ctrl key pressed as well (either side) + // 0 = No Ctrl key + // PS2_CAPS 12 1 = Caps Lock ON + // 0 = Caps lock OFF + // PS2_ALT 11 1 = Left Alt key pressed as well + // 0 = No Left Alt key + // PS2_ALT_GR 10 1 = Right Alt (Alt GR) key pressed as well + // 0 = No Right Alt key + // PS2_GUI 9 1 = GUI key pressed as well (either) + // 0 = No GUI key + // PS2_FUNCTION 8 1 = FUNCTION key non-printable character (plus space, tab, enter) + // 0 = standard character key + // 7-0 PS/2 Key code. + // + // BREAK code means all keys released so clear out flags and send update. + ESP_LOGW(MAPKEYTAG, "SCANCODE:%04x",scanCode); + + // Map the PS/2 key to an PC9801 CTRL + KEY + pcKey = pThis->mapKey(scanCode); + if(pcKey != 0L) { pThis->pushKeyToQueue(pcKey); } + + // Toggle LED to indicate data flow. + if((scanCode & PS2_BREAK) == 0) + pThis->led->setLEDMode(LED::LED_MODE_BLINK_ONESHOT, LED::LED_DUTY_CYCLE_10, 1, 100L, 0L); + } + + // Check for incoming host keyboard commands and execute them. + if(xQueueReceive(rcvQueue, (void *)&rcvMsg, 0) == pdTRUE) + { + ESP_LOGD(MAINTAG, "Received Host Cmd:%02x\n", rcvMsg.hostCmd); + } + + // NVS writes require both CPU cores to be free so write config out at a known junction. + if(pThis->pcCtrl.persistConfig == true) + { + // Request and wait for the interface to suspend. This ensures that the host cpu is not held in a spinlock when NVS update is requested avoiding deadlock. + pThis->suspendInterface(true); + pThis->isSuspended(true); + + if(pThis->nvs->persistData(pThis->getClassName(__PRETTY_FUNCTION__), &pThis->pcConfig, sizeof(t_pcConfig)) == false) + { + ESP_LOGW(SELOPTTAG, "Persisting PC-9801 configuration data failed, updates will not persist in future power cycles."); + pThis->led->setLEDMode(LED::LED_MODE_BLINK_ONESHOT, LED::LED_DUTY_CYCLE_10, 200, 1000L, 0L); + } else + // Few other updates so make a commit here to ensure data is flushed and written. + if(pThis->nvs->commitData() == false) + { + ESP_LOGW(SELOPTTAG, "NVS Commit writes operation failed, some previous writes may not persist in future power cycles."); + pThis->led->setLEDMode(LED::LED_MODE_BLINK_ONESHOT, LED::LED_DUTY_CYCLE_10, 200, 500L, 0L); + } + + // Release interface. + pThis->suspendInterface(false); + + // Clear flag so we dont persist in a loop. + pThis->pcCtrl.persistConfig = false; + } + + // Yield if the suspend flag is set. + pThis->yield(25); + } +} + +// A method to load the keyboard mapping table into memory for use in the interface mapping logic. If no persistence file exists or an error reading persistence occurs, the keymap +// uses the internal static default. If no persistence file exists and attempt is made to create it with a copy of the inbuilt static map so that future operations all +// work with persistence such that modifications can be made. +// +bool PC9801::loadKeyMap(void) +{ + // Locals. + // + bool result = false; + int fileRows = 0; + struct stat keyMapFileNameStat; + + // See if the file exists, if it does, get size so we can compute number of mapping rows. + if(stat(pcCtrl.keyMapFileName.c_str(), &keyMapFileNameStat) == -1) + { + ESP_LOGW(MAINTAG, "No keymap file, using inbuilt definitions."); + } else + { + // Get number of rows in the file. + fileRows = keyMapFileNameStat.st_size/sizeof(t_keyMapEntry); + + // Subsequent reloads, delete memory prior to building new map, primarily to conserve precious resources rather than trying the memory allocation trying to realloc and then having to copy. + if(pcCtrl.kme != NULL && pcCtrl.kme != PS2toPC9801.kme) + { + delete pcCtrl.kme; + pcCtrl.kme = NULL; + } + + // Allocate memory for the new keymap table. + pcCtrl.kme = new t_keyMapEntry[fileRows]; + if(pcCtrl.kme == NULL) + { + ESP_LOGW(MAINTAG, "Failed to allocate memory for keyboard map, fallback to inbuilt!"); + } else + { + // Open the keymap extension file for binary reading to add data to our map table. + std::fstream keyFileIn(pcCtrl.keyMapFileName.c_str(), std::ios::in | std::ios::binary); + + int idx=0; + while(keyFileIn.good()) + { + keyFileIn.read((char *)&pcCtrl.kme[idx], sizeof(t_keyMapEntry)); + if(keyFileIn.good()) + { + idx++; + } + } + // Any errors, we wind back and use the inbuilt mapping table. + if(keyFileIn.bad()) + { + keyFileIn.close(); + ESP_LOGW(MAINTAG, "Failed to read data from keymap extension file:%s, fallback to inbuilt!", pcCtrl.keyMapFileName.c_str()); + } else + { + // No longer need the file. + keyFileIn.close(); + + // Max rows in the KME table. + pcCtrl.kmeRows = fileRows; + + // Good to go, map ready for use with the interface. + result = true; + } + } + } + + // Any failures, free up memory and use the inbuilt mapping table. + if(result == false) + { + if(pcCtrl.kme != NULL && pcCtrl.kme != PS2toPC9801.kme) + { + delete pcCtrl.kme; + pcCtrl.kme = NULL; + } + + // No point allocating memory if no extensions exist or an error occurs, just point to the static table. + pcCtrl.kme = PS2toPC9801.kme; + pcCtrl.kmeRows = PS2TBL_PC9801_MAXROWS; + + // Persist the data so that next load comes from file. + saveKeyMap(); + } + + // Return code. Either memory map was successfully loaded, true or failed, false. + return(result); +} + +// Method to save the current keymap out to an extension file. +// +bool PC9801::saveKeyMap(void) +{ + // Locals. + // + bool result = false; + int idx = 0; + + // Has a map been defined? Cannot save unless loadKeyMap has been called which sets pcCtrl.kme to point to the internal keymap or a new memory resident map. + // + if(pcCtrl.kme == NULL) + { + ESP_LOGW(MAINTAG, "KeyMap hasnt yet been defined, need to call loadKeyMap."); + } else + { + // Open file for binary writing, trunc specified to clear out the file, we arent appending. + std::fstream keyFileOut(pcCtrl.keyMapFileName.c_str(), std::ios::out | std::ios::binary | std::ios::trunc); + + // Loop whilst no errors and data rows still not written. + while(keyFileOut.good() && idx < pcCtrl.kmeRows) + { + keyFileOut.write((char *)&pcCtrl.kme[idx], sizeof(t_keyMapEntry)); + idx++; + } + if(keyFileOut.bad()) + { + ESP_LOGW(MAINTAG, "Failed to write data from the keymap to file:%s, deleting as state is unknown!", pcCtrl.keyMapFileName.c_str()); + keyFileOut.close(); + std::remove(pcCtrl.keyMapFileName.c_str()); + } else + { + // Success. + keyFileOut.close(); + result = true; + } + } + + // Return code. Either memory map was successfully saved, true or failed, false. + return(result); +} + +// Public method to open a keymap file for data upload. +// This method opens the file and makes any validation checks as necessary. +// +bool PC9801::createKeyMapFile(std::fstream &outFile) +{ + // Locals. + // + bool result = true; + std::string fileName; + + // Attempt to open a temporary keymap file for writing. + // + fileName = pcCtrl.keyMapFileName; + replaceExt(fileName, "tmp"); + outFile.open(fileName.c_str(), std::ios::out | std::ios::binary | std::ios::trunc); + if(outFile.bad()) + { + result = false; + } + + // Send result. + return(result); +} + +// Public method to validate and store data provided by caller into an open file created by 'createKeyMapFile'. +// +bool PC9801::storeDataToKeyMapFile(std::fstream &outFile, char *data, int size) +{ + // Locals. + // + bool result = true; + + // Check that the file is still writeable then add data. + if(outFile.good()) + { + outFile.write(data, size); + } + if(outFile.bad()) + { + result = false; + } + + // Send result. + return(result); +} + +// Polymorphic alternative to take a vector of bytes for writing to the output file. +// +bool PC9801::storeDataToKeyMapFile(std::fstream & outFile, std::vector& dataArray) +{ + // Locals. + // + bool result = true; + char data[1]; + + // Check that the file is still writeable then add data. Not best for performace but ease of use and minimum memory. + if(outFile.good()) + { + for(std::size_t idx = 0; idx < dataArray.size(); idx++) + { + data[0] = (char)dataArray[idx]; + outFile.write((char *)&data, 1); + } + } + if(outFile.bad()) + { + result = false; + } + + // Send result. + return(result); +} + +// Public method to close and commit a data file, created by 'createKeyMapFile' and populated by 'storeDataToKeyMapFile'. +// This involves renaming the original keymap file, closing the new file and renaming it to the original keymap filename. +// +bool PC9801::closeAndCommitKeyMapFile(std::fstream &outFile, bool cleanupOnly) +{ + // Locals. + // + bool result = true; + std::string fileName; + + // Check the file is still accessible and close. + // + outFile.close(); + if(!cleanupOnly) + { + if(outFile.good()) + { + // Rename the original file. + fileName = pcCtrl.keyMapFileName; + replaceExt(fileName, "bak"); + // Remove old backup file. Dont worry if it is not there! + std::remove(fileName.c_str()); + replaceExt(fileName, "tmp"); + // Rename new file to active. + if(std::rename(fileName.c_str(), pcCtrl.keyMapFileName.c_str()) != 0) + { + result = false; + } + } else + { + result = false; + } + } + + // Send result. + return(result); +} + +// Method to return the keymap column names as header strings. +// +void PC9801::getKeyMapHeaders(std::vector& headerList) +{ + // Add the names. + // + headerList.push_back(PS2TBL_PS2KEYCODE_NAME); + headerList.push_back(PS2TBL_PS2CTRL_NAME); + headerList.push_back(PS2TBL_KEYBOARDMODEL_NAME); + headerList.push_back(PS2TBL_MACHINE_NAME); +// headerList.push_back(PS2TBL_X1MODE_NAME); +// headerList.push_back(PS2TBL_X1KEYCODE_NAME); +// headerList.push_back(PS2TBL_X1KEYCODE_BYTE2_NAME); +// headerList.push_back(PS2TBL_X1_CTRL_NAME); + + return; +} + +// A method to return the Type of data for a given column in the KeyMap table. +// +void PC9801::getKeyMapTypes(std::vector& typeList) +{ + // Add the types. + // + typeList.push_back(PS2TBL_PS2KEYCODE_TYPE); + typeList.push_back(PS2TBL_PS2CTRL_TYPE); + typeList.push_back(PS2TBL_KEYBOARDMODEL_TYPE); + typeList.push_back(PS2TBL_MACHINE_TYPE); +// typeList.push_back(PS2TBL_X1MODE_TYPE); +// typeList.push_back(PS2TBL_X1KEYCODE_TYPE); +// typeList.push_back(PS2TBL_X1KEYCODE_BYTE2_TYPE); +// typeList.push_back(PS2TBL_X1CTRL_TYPE); + + return; +} + +// Method to return a list of key:value entries for a given keymap column. This represents the +// feature which can be selected and the value it uses. Features can be combined by ORing the values +// together. +bool PC9801::getKeyMapSelectList(std::vector>& selectList, std::string option) +{ + // Locals. + // + bool result = true; + + // Build up a map, depending on the list required, of name to value. This list can then be used + // by a user front end to select an option based on a name and return its value. + if(option.compare(PS2TBL_PS2CTRL_TYPE) == 0) + { + selectList.push_back(std::make_pair(PS2TBL_PS2CTRL_SEL_SHIFT, PS2CTRL_SHIFT)); + selectList.push_back(std::make_pair(PS2TBL_PS2CTRL_SEL_CTRL, PS2CTRL_CTRL)); + selectList.push_back(std::make_pair(PS2TBL_PS2CTRL_SEL_CAPS, PS2CTRL_CAPS)); + selectList.push_back(std::make_pair(PS2TBL_PS2CTRL_SEL_KANA, PS2CTRL_KANA)); + selectList.push_back(std::make_pair(PS2TBL_PS2CTRL_SEL_GRAPH, PS2CTRL_GRAPH)); + selectList.push_back(std::make_pair(PS2TBL_PS2CTRL_SEL_GUI, PS2CTRL_GUI)); + selectList.push_back(std::make_pair(PS2TBL_PS2CTRL_SEL_FUNC, PS2CTRL_FUNC)); + selectList.push_back(std::make_pair(PS2TBL_PS2CTRL_SEL_EXACT, PS2CTRL_EXACT)); + } + else if(option.compare(PS2TBL_KEYBOARDMODEL_TYPE) == 0) + { + selectList.push_back(std::make_pair(KEYMAP_SEL_STANDARD, KEYMAP_STANDARD)); + selectList.push_back(std::make_pair(KEYMAP_SEL_UK_WYSE_KB3926, KEYMAP_UK_WYSE_KB3926)); + selectList.push_back(std::make_pair(KEYMAP_SEL_JAPAN_OADG109, KEYMAP_JAPAN_OADG109)); + selectList.push_back(std::make_pair(KEYMAP_SEL_JAPAN_SANWA_SKBL1, KEYMAP_JAPAN_SANWA_SKBL1)); + selectList.push_back(std::make_pair(KEYMAP_SEL_NOT_ASSIGNED_4, KEYMAP_NOT_ASSIGNED_4)); + selectList.push_back(std::make_pair(KEYMAP_SEL_NOT_ASSIGNED_5, KEYMAP_NOT_ASSIGNED_5)); + selectList.push_back(std::make_pair(KEYMAP_SEL_NOT_ASSIGNED_6, KEYMAP_NOT_ASSIGNED_6)); + selectList.push_back(std::make_pair(KEYMAP_SEL_UK_PERIBOARD_810, KEYMAP_UK_PERIBOARD_810)); + selectList.push_back(std::make_pair(KEYMAP_SEL_UK_OMOTON_K8508, KEYMAP_UK_OMOTON_K8508)); + } + else if(option.compare(PS2TBL_MACHINE_TYPE) == 0) + { + selectList.push_back(std::make_pair(PC9801_SEL_ALL, PC9801_ALL)); +// selectList.push_back(std::make_pair(X1_SEL_ORIG, X1_ORIG)); +// selectList.push_back(std::make_pair(X1_SEL_TURBO, X1_TURBO)); +// selectList.push_back(std::make_pair(X1_SEL_TURBOZ, X1_TURBOZ)); + } + else if(option.compare(PS2TBL_PC9801_CTRL_TYPE) == 0) + { + selectList.push_back(std::make_pair(PC9801_CTRL_SEL_GRAPH, PC9801_CTRL_GRAPH)); + selectList.push_back(std::make_pair(PC9801_CTRL_SEL_CAPS, PC9801_CTRL_CAPS)); + selectList.push_back(std::make_pair(PC9801_CTRL_SEL_KANA, PC9801_CTRL_KANA)); + selectList.push_back(std::make_pair(PC9801_CTRL_SEL_SHIFT, PC9801_CTRL_SHIFT)); + selectList.push_back(std::make_pair(PC9801_CTRL_SEL_CTRL, PC9801_CTRL_CTRL)); + } else + { + // Not found! + result = false; + } + + // Return result, false if the option not found, true otherwise. + // + return(result); +} + + +// Method to read the Keymap array, 1 row at a time and return it to the caller. +// +bool PC9801::getKeyMapData(std::vector& dataArray, int *row, bool start) +{ + // Locals. + // + bool result = false; + + // If start flag is set, set row to 0. + if(start == true) + { + (*row) = 0; + } + + // Bound check and if still valid, push data onto the vector. + if((*row) >= pcCtrl.kmeRows) + { + result = true; + } else + { + dataArray.push_back(pcCtrl.kme[*row].ps2KeyCode); + dataArray.push_back(pcCtrl.kme[*row].ps2Ctrl); + dataArray.push_back(pcCtrl.kme[*row].keyboardModel); + dataArray.push_back(pcCtrl.kme[*row].machine); + // dataArray.push_back(pcCtrl.kme[*row].x1Mode); + // dataArray.push_back(pcCtrl.kme[*row].x1Key); + // dataArray.push_back(pcCtrl.kme[*row].x1Key2); + // dataArray.push_back(pcCtrl.kme[*row].x1Ctrl); + (*row) = (*row) + 1; + } + + // True if no more rows, false if additional rows can be read. + return(result); +} + + +// Initialisation routine. Start two threads, one to handle the incoming PS/2 keyboard data and map it, the second to handle the host interface. +void PC9801::init(uint32_t ifMode, NVS *hdlNVS, LED *hdlLED, HID *hdlHID) +{ + // Basic initialisation. + init(hdlNVS, hdlHID); + + // Invoke the prototype init which initialises common variables and devices shared by all subclass. + KeyInterface::init(getClassName(__PRETTY_FUNCTION__), hdlNVS, hdlLED, hdlHID, ifMode); + + // Prepare the UART to be used for communications with the PC-9801. + // The PC-9801 uses Asynchronous protocol with 1 start bit, 1 stop bit, odd parity, 19200 baud. + // + uart_config_t uartConfig = { + .baud_rate = 19200, + .data_bits = UART_DATA_8_BITS, + .parity = UART_PARITY_ODD, + .stop_bits = UART_STOP_BITS_1, + .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, + .rx_flow_ctrl_thresh = 122, + .source_clk = UART_SCLK_APB, + }; + + // Install UART driver. Use RX/TX buffers without event queue. + ESP_ERROR_CHECK(uart_driver_install(pcCtrl.uartNum, pcCtrl.uartBufferSize, pcCtrl.uartBufferSize, 0, NULL, 0)); + + // Configure UART parameters and pin assignments, software flow control, not RTS/CTS. + ESP_ERROR_CHECK(uart_param_config(pcCtrl.uartNum, &uartConfig)); + ESP_ERROR_CHECK(uart_set_pin(pcCtrl.uartNum, CONFIG_HOST_KDB0, CONFIG_HOST_KDB3, -1, -1)); + + // Create queue for buffering incoming HID keys prior to transmitting to the PC-9801. + xmitQueue = xQueueCreate(MAX_PC9801_XMIT_KEY_BUF, sizeof(t_xmitQueueMessage)); + // Create queue for buffering incoming PC-9801 data for later processing. + rcvQueue = xQueueCreate(MAX_PC9801_RCV_KEY_BUF, sizeof(t_rcvQueueMessage)); + + // Create a task pinned to core 1 which will fulfill the NEC PC-9801 interface. This task has the highest priority + // and it will also hold spinlock and manipulate the watchdog to ensure a scan cycle timing can be met. This means + // all other tasks running on Core 1 will suspend as needed. The HID devices will be serviced with core 0. + // + // Core 1 - NEC PC-9801 Interface + ESP_LOGW(MAINTAG, "Starting NEC PC-9801 if thread..."); + ::xTaskCreatePinnedToCore(&this->pcInterface, "pc9801if", 4096, this, 25, &this->TaskHostIF, 1); + vTaskDelay(500); + + // Core 0 - Application + // HID Interface handler thread. + ESP_LOGW(MAINTAG, "Starting hidIf thread..."); + ::xTaskCreatePinnedToCore(&this->hidInterface, "hidIf", 8192, this, 22, &this->TaskHIDIF, 0); +} + +// Initialisation routine without hardware. +void PC9801::init(NVS *hdlNVS, HID *hdlHID) +{ + // Initialise control variables. + this->pcCtrl.keyCtrl = 0xFF; // Negative logic, 0 - active, 1 = inactive. + pcCtrl.optionSelect = false; + pcCtrl.uartNum = UART_NUM_2; + pcCtrl.uartBufferSize = 256; + pcCtrl.uartQueueSize = 10; + pcCtrl.keyMapFileName = pcCtrl.fsPath.append("/").append(PC9801IF_KEYMAP_FILE); + pcCtrl.kmeRows = 0; + pcCtrl.kme = NULL; + pcCtrl.persistConfig = false; + + // Invoke the prototype init which initialises common variables and devices shared by all subclass. + KeyInterface::init(getClassName(__PRETTY_FUNCTION__), hdlNVS, hdlHID); + + // Load the keyboard mapping table into memory. If the file doesnt exist, create it. + loadKeyMap(); + + // Retrieve configuration, if it doesnt exist, set defaults. + // + if(nvs->retrieveData(getClassName(__PRETTY_FUNCTION__), &this->pcConfig, sizeof(t_pcConfig)) == false) + { + ESP_LOGW(MAINTAG, "PC9801 configuration set to default, no valid config in NVS found."); + pcConfig.params.activeKeyboardMap = KEYMAP_STANDARD; + pcConfig.params.activeMachineModel = PC9801_ALL; + + // Persist the data for next time. + if(nvs->persistData(getClassName(__PRETTY_FUNCTION__), &this->pcConfig, sizeof(t_pcConfig)) == false) + { + ESP_LOGW(MAINTAG, "Persisting Default PC9801 configuration data failed, check NVS setup.\n"); + } + // Few other updates so make a commit here to ensure data is flushed and written. + else if(this->nvs->commitData() == false) + { + ESP_LOGW(SELOPTTAG, "NVS Commit writes operation failed, some previous writes may not persist in future power cycles."); + } + } +} + +// Constructor, basically initialise the Singleton interface and let the threads loose. +PC9801::PC9801(uint32_t ifMode, NVS *hdlNVS, LED *hdlLED, HID *hdlHID, const char* fsPath) +{ + // Setup the default path on the underlying filesystem. + this->pcCtrl.fsPath = fsPath; + + // Initialise the interface. + init(ifMode, hdlNVS, hdlLED, hdlHID); +} + +// Constructor, basic without hardware. +PC9801::PC9801(NVS *hdlNVS, HID *hdlHID, const char* fsPath) +{ + // Setup the default path on the underlying filesystem. + this->pcCtrl.fsPath = fsPath; + + // Initialise the interface. + init(hdlNVS, hdlHID); +} + +// Constructor, used for version reporting so no hardware is initialised. +PC9801::PC9801(void) +{ + return; +} + +// Destructor - only ever called when the class is used for version reporting. +PC9801::~PC9801(void) +{ + return; +} diff --git a/main/PS2KeyAdvanced.cpp b/main/PS2KeyAdvanced.cpp deleted file mode 120000 index da2eff6..0000000 --- a/main/PS2KeyAdvanced.cpp +++ /dev/null @@ -1 +0,0 @@ -../../sharpkey/main/PS2KeyAdvanced.cpp \ No newline at end of file diff --git a/main/PS2KeyAdvanced.cpp b/main/PS2KeyAdvanced.cpp new file mode 100644 index 0000000..a220349 --- /dev/null +++ b/main/PS2KeyAdvanced.cpp @@ -0,0 +1,1113 @@ +/* Version V1.0.9 + PS2KeyAdvanced.cpp - PS2KeyAdvanced library + Copyright (c) 2007 Free Software Foundation. All right reserved. + Written by Paul Carpenter, PC Services + Created September 2014 + Updated January 2016 - Paul Carpenter - add tested on Due and tidy ups for V1.5 Library Management + January 2020 Fix typos, correct keyboard reset status improve library.properties + and additional platform handling and some documentation + March 2020 Add SAMD1 as recognised support as has been tested by user + Improve different architecture handling + November 2020 Add support for STM32 from user Hiabuto-de + Tested on STM32Duino-Framework and PlatformIO on STM32F103C8T6 and an IBM Model M + July 2021 Add workaround for ESP32 issue with Silicon (hardware) from user submissions + + IMPORTANT WARNING + + If using a DUE or similar board with 3V3 I/O you MUST put a level translator + like a Texas Instruments TXS0102 or FET circuit as the signals are + Bi-directional (signals transmitted from both ends on same wire). + + Failure to do so may damage your Arduino Due or similar board. + + Test History + September 2014 Uno and Mega 2560 September 2014 using Arduino V1.6.0 + January 2016 Uno, Mega 2560 and Due using Arduino 1.6.7 and Due Board + Manager V1.6.6 + + + Assumption - Only ONE keyboard added to one Arduino + - No stream support + + This is for a LATIN style keyboard using Scan code set 2. See various + websites on what different scan code sets use. Scan Code Set 2 is the + default scan code set for PS2 keyboards on power up. + + Fully featured PS2 keyboard library to provide + All keys as a keycode (A-Z and 0-9 as ASCII equivalents) + All function and movement keys supported even multi-lingual + Parity checking of data sent/received on receive request keyboard resend + Resends data when needed handles keyboard protocol for RESEND and ECHO + Functions for get and set of + Scancode set in use READ only + LED and LOCK control + ReadID + Reset keyboard + Send ECHO + Handles NUM, _CAPS and SCROLL lock keys to LEDs + Handles NUM/SCROLL internally + + Returns an uint16_t containing + Make/Break status + CAPS status + SHIFT, CTRL, ALT, ALT GR, GUI keys + Flag for function key not a displayable/printable character + 8 bit key code + + Code Ranges(bottom byte of uint16_t) see PS2KeyAdvanced.h for details + 0 invalid/error + 1-1F Functions (_CAPS, _SHIFT, _ALT, Enter, DEL... ) + 1A-1F Functions with ASCII control code + (DEL, BS, TAB, ESC, ENTER, SPACE) + 20-60 Printable characters noting + 0-9 = 0x30 to 0x39 as ASCII + A to Z = 0x41 to 0x5A as upper case ASCII type codes + 8B Extra European key + 61-A0 Function keys and other special keys (plus F2 and F1 less 8B) + 61-78 F1 to F24 + 79-8A Multimedia + 8C-8E ACPI power + 91-A0 and F2 and F1 - Special multilingual + A8-FF Keyboard communications commands (note F2 and F1 are special + codes for special multi-lingual keyboards) + + By using these ranges it is possible to perform detection of any key and do + easy translation to ASCII/UTF-8 avoiding keys that do not have a valid code. + + Top Byte is 8 bits denoting as follows with defines for bit code + + Define name bit description + PS2_BREAK 15 1 = Break key code + (MSB) 0 = Make Key code + PS2_SHIFT 14 1 = Shift key pressed as well (either side) + 0 = NO shift key + PS2_CTRL 13 1 = Ctrl key pressed as well (either side) + 0 = NO Ctrl key + PS2_CAPS 12 1 = Caps Lock ON + 0 = Caps lock OFF + PS2_ALT 11 1 = Left Alt key pressed as well + 0 = NO Left Alt key + PS2_ALT_GR 10 1 = Right Alt (Alt GR) key pressed as well + 0 = NO Right Alt key + PS2_GUI 9 1 = GUI key pressed as well (either) + 0 = NO GUI key + PS2_FUNCTION 8 1 = FUNCTION key non-printable character (plus space, tab, enter) + 0 = standard character key + + Error Codes + Most functions return 0 or 0xFFFF as error, other codes to note and + handle appropriately value in bottom byte + 0xAA keyboard has reset and passed power up tests + will happen if keyboard plugged in after code start + 0xFC Keyboard General error or power up fail + + It is responsibility of your programme to deal with converting special cases + like <_CTRL>+ sends a special code to something else. A better method + is to use PS2KeyMap library and add your own table to that library. If you + wish to do that make a NEW library called SOMETHING different NOT a variant + or revision of this one, as you are changing base functionality + + See PS2KeyCode.h for codes from the keyboard this library uses to decode. + (may disappear in updates do not rely on this file or definitions) + + See PS2KeyAvanced.h for returned definitions of Keys and accessible + definitions + + See PS2KeyMap.h for tables currently supported + + To get the key as ASCII/UTF-8 single byte character conversion requires use + of PS2KeyMap library AS WELL. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ +#include +// Internal headers for library defines/codes/etc +#include "PS2KeyAdvanced.h" +#include "PS2KeyCode.h" +#include "PS2KeyTable.h" + + +// Private function declarations +void send_bit( void ); +void send_now( uint8_t ); +int16_t send_next( void ); +void ps2_reset( void ); +uint8_t decode_key( uint8_t ); +void pininput( uint8_t ); +void set_lock( ); + +/* Constant control functions to flags array + in translated key code value order */ +#if defined( PS2_REQUIRES_PROGMEM ) +const uint8_t PROGMEM control_flags[] = { +#else +const uint8_t control_flags[] = { +#endif + _SHIFT, _SHIFT, _CTRL, _CTRL, + _ALT, _ALT_GR, _GUI, _GUI + }; + +// Private Variables +volatile uint8_t _ps2mode; /* _ps2mode contains + _PS2_BUSY bit 7 = busy until all expected bytes RX/TX + _TX_MODE bit 6 = direction 1 = TX, 0 = RX (default) + _BREAK_KEY bit 5 = break code detected + _WAIT_RESPONSE bit 4 = expecting data response + _E0_MODE bit 3 = in E0 mode + _E1_MODE bit 2 = in E1 mode + _LAST_VALID bit 1 = last sent valid in case we receive resend + and not sent anything */ + +/* volatile RX buffers and variables accessed via interrupt functions */ +volatile uint16_t _rx_buffer[ _RX_BUFFER_SIZE ]; // buffer for data from keyboard +volatile uint8_t _head; // _head = last byte written +uint8_t _tail; // _tail = last byte read (not modified in IRQ ever) +volatile int8_t _bytes_expected; +volatile uint8_t _bitcount; // Main state variable and bit count for interrupts +volatile uint8_t _shiftdata; +volatile uint8_t _parity; + +/* TX variables */ +volatile uint8_t _tx_buff[ _TX_BUFFER_SIZE ]; // buffer for keyboard commands +volatile uint8_t _tx_head; // buffer write pointer +volatile uint8_t _tx_tail; // buffer read pointer +volatile uint8_t _last_sent; // last byte if resend requested +volatile uint8_t _now_send; // immediate byte to send +volatile uint8_t _response_count; // bytes expected in reply to next TX +volatile uint8_t _tx_ready; // TX status for type of send contains + /* _HANDSHAKE 0x80 = handshaking command (ECHO/RESEND) + _COMMAND 0x01 = other command processing */ + +/* Output key buffering */ +uint16_t _key_buffer[ _KEY_BUFF_SIZE ]; // Output Buffer for translated keys +uint8_t _key_head; // Output buffer WR pointer +uint8_t _key_tail; // Output buffer RD pointer +uint8_t _mode = 0; // Mode for output buffer contains + /* _NO_REPEATS 0x80 No repeat make codes for _CTRL, _ALT, _SHIFT, _GUI + _NO_BREAKS 0x08 No break codes */ + +// Arduino settings for pins and interrupts Needed to send data +uint8_t PS2_DataPin; +uint8_t PS2_IrqPin; + +// Key decoding variables +uint8_t PS2_led_lock = 0; // LED and Lock status +uint8_t PS2_lockstate[ 4 ]; // Save if had break on key for locks +uint8_t PS2_keystatus; // current CAPS etc status for top byte + + +/*------------------ Code starts here -------------------------*/ + +/* The ISR for the external interrupt + To receive 11 bits - start 8 data, ODD parity, stop + To send data calls send_bit( ) + Interrupt every falling incoming clock edge from keyboard */ +IRAM_ATTR void ps2interrupt( void ) +{ +// Workaround for ESP32 SILICON error see extra/Porting.md +#ifdef PS2_ONLY_CHANGE_IRQ +if( digitalRead( PS2_IrqPin ) ) + return; +#endif +if( _ps2mode & _TX_MODE ) + send_bit( ); +else + { + static uint32_t prev_ms = 0; + uint32_t now_ms; + uint8_t val, ret; + + val = digitalRead( PS2_DataPin ); + /* timeout catch for glitches reset everything */ + now_ms = millis( ); + if( now_ms - prev_ms > 250 ) + { + _bitcount = 0; + _shiftdata = 0; + } + prev_ms = now_ms; + _bitcount++; // Now point to next bit + switch( _bitcount ) + { + case 1: // Start bit + _parity = 0; + _ps2mode |= _PS2_BUSY; // set busy + break; + case 2: + case 3: + case 4: + case 5: + case 6: + case 7: + case 8: + case 9: // Data bits + _parity += val; // another one received ? + _shiftdata >>= 1; // right _SHIFT one place for next bit + _shiftdata |= ( val ) ? 0x80 : 0; // or in MSbit + break; + case 10: // Parity check + _parity &= 1; // Get LSB if 1 = odd number of 1's so parity bit should be 0 + if( _parity == val ) // Both same parity error + _parity = 0xFD; // To ensure at next bit count clear and discard + break; + case 11: // Stop bit lots of spare time now + if( _parity >= 0xFD ) // had parity error + { + send_now( PS2_KC_RESEND ); // request resend + _tx_ready |= _HANDSHAKE; + } + else // Good so save byte in _rx_buffer + { + // Check _SHIFTed data for commands and action + ret = decode_key( _shiftdata ); + if( ret & 0x2 ) // decrement expected bytes + _bytes_expected--; + if( _bytes_expected <= 0 || ret & 4 ) // Save value ?? + { + val = _head + 1; + if( val >= _RX_BUFFER_SIZE ) + val = 0; + if( val != _tail ) + { + // get last byte to save + _rx_buffer[ val ] = uint16_t( _shiftdata ); + // save extra details + _rx_buffer[ val ] |= uint16_t( _ps2mode ) << 8; + _head = val; + } + } + if( ret & 0x10 ) // Special command to send (ECHO/RESEND) + { + send_now( _now_send ); + _tx_ready |= _HANDSHAKE; + } + else + if( _bytes_expected <= 0 ) // Receive data finished + { + // Set mode and status for next receive byte + _ps2mode &= ~( _E0_MODE + _E1_MODE + _WAIT_RESPONSE + _BREAK_KEY ); + _bytes_expected = 0; + _ps2mode &= ~_PS2_BUSY; + send_next( ); // Check for more to send + } + } + _bitcount = 0; // end of byte + break; + default: // in case of weird error and end of byte reception re-sync + _bitcount = 0; + } + } +} + + +/* Decode value received to check for errors commands and responses + NOT keycode translate yet + returns bit Or'ing + 0x10 send command in _now_send (after any saves and decrements) + 0x08 error abort reception and reset status and queues + 0x04 save value ( complete after translation ) + 0x02 decrement count of bytes to expected + + Codes like EE, AA and FC ( Echo, BAT pass and fail) treated as valid codes + return code 6 +*/ +uint8_t decode_key( uint8_t value ) +{ +uint8_t state; + +state = 6; // default state save and decrement + +// Anything but resend received clear valid value to resend +if( value != PS2_KC_RESEND ) + _ps2mode &= ~( _LAST_VALID ); + +// First check not a valid response code from a host command +if( _ps2mode & _WAIT_RESPONSE ) + if( value < 0xF0 ) + return state; // Save response and decrement + +// E1 Pause mode special case just decrement +if( _ps2mode & _E1_MODE ) + return 2; + +switch( value ) + { + case 0: // Buffer overrun Errors Reset modes and buffers + case PS2_KC_OVERRUN: + ps2_reset( ); + state = 0xC; + break; + case PS2_KC_RESEND: // Resend last byte if we have sent something + if( ( _ps2mode & _LAST_VALID ) ) + { + _now_send = _last_sent; + state = 0x10; + } + else + state = 0; + break; + case PS2_KC_ERROR: // General error pass up but stop any sending or receiving + _bytes_expected = 0; + _ps2mode = 0; + _tx_ready = 0; + state = 0xE; + break; + case PS2_KC_KEYBREAK: // break Code - wait the final key byte + _bytes_expected = 1; + _ps2mode |= _BREAK_KEY; + state = 0; + break; + case PS2_KC_ECHO: // Echo if we did not originate echo back + state = 4; // always save + if( _ps2mode & _LAST_VALID && _last_sent != PS2_KC_ECHO ) + { + _now_send = PS2_KC_ECHO; + state |= 0x10; // send _command on exit + } + break; + case PS2_KC_BAT: // BAT pass + _bytes_expected = 0; // reset as if in middle of something lost now + state = 4; + break; + case PS2_KC_EXTEND1: // Major extend code (PAUSE key only) + if( !( _ps2mode & _E1_MODE ) ) // First E1 only + { + _bytes_expected = 7; // seven more bytes + _ps2mode |= _E1_MODE; + _ps2mode &= ~_BREAK_KEY; // Always a make + } + state = 0; + break; + case PS2_KC_EXTEND: // Two byte Extend code + _bytes_expected = 1; // one more byte at least to wait for + _ps2mode |= _E0_MODE; + state = 0; + break; + } +return state; +} + + +/* Send data to keyboard + Data pin direction should already be changed + Start bit would be already set so each clock setup for next clock + parity and _bitcount should be 0 already and busy should be set + + Start bit setting is due to bug in attachinterrupt not clearing pending interrupts + Also no clear pending interrupt function */ +void send_bit( void ) +{ +uint8_t val; + +_bitcount++; // Now point to next bit +switch( _bitcount ) + { + case 1: +#if defined( PS2_CLEAR_PENDING_IRQ ) + // Start bit due to Arduino bug + digitalWrite( PS2_DataPin, LOW ); + break; +#endif + case 2: + case 3: + case 4: + case 5: + case 6: + case 7: + case 8: + case 9: + // Data bits + val = _shiftdata & 0x01; // get LSB + digitalWrite( PS2_DataPin, val ); // send start bit + _parity += val; // another one received ? + _shiftdata >>= 1; // right _SHIFT one place for next bit + break; + case 10: + // Parity - Send LSB if 1 = odd number of 1's so parity should be 0 + digitalWrite( PS2_DataPin, ( ~_parity & 1 ) ); + break; + case 11: // Stop bit write change to input pull up for high stop bit + pininput( PS2_DataPin ); + break; + case 12: // Acknowledge bit low we cannot do anything if high instead of low + if( !( _now_send == PS2_KC_ECHO || _now_send == PS2_KC_RESEND ) ) + { + _last_sent = _now_send; // save in case of resend request + _ps2mode |= _LAST_VALID; + } + // clear modes to receive again + _ps2mode &= ~_TX_MODE; + if( _tx_ready & _HANDSHAKE ) // If _HANDSHAKE done + _tx_ready &= ~_HANDSHAKE; + else // else we finished a command + _tx_ready &= ~_COMMAND; + if( !( _ps2mode & _WAIT_RESPONSE ) ) // if not wait response + send_next( ); // check anything else to queue up + _bitcount = 0; // end of byte + break; + default: // in case of weird error and end of byte reception re-sync + _bitcount = 0; + } +} + + +/* Takes a byte sets up variables and starts the data sending processes + Starts the actual byte transmission + calling code must make sure line is idle and able to send + Whilst this function adds long delays the process of the delays + will STOP the interrupt source (keyboard) externally when clock held low + _tx_ready contains 2 flags checked in this order + _HANDSHAKE command sent as part of receiving e.g. ECHO, RESEND + _COMMAND other commands not part of receiving + Main difference _bytes_expected is NOT altered in _HANDSHAKE mode + in command mode we update _bytes_expected with number of response bytes +*/ +void send_now( uint8_t command ) +{ +_shiftdata = command; +_now_send = command; // copy for later to save in last sent +#if defined( PS2_CLEAR_PENDING_IRQ ) +_bitcount = 0; // AVR/SAM ignore extra interrupt +#else +_bitcount = 1; // Normal processors +#endif +_parity = 0; +_ps2mode |= _TX_MODE + _PS2_BUSY; + +// Only do this if sending a command not from Handshaking +if( !( _tx_ready & _HANDSHAKE ) && ( _tx_ready & _COMMAND ) ) + { + _bytes_expected = _response_count; // How many bytes command will generate + _ps2mode |= _WAIT_RESPONSE; + } + +// STOP interrupt handler +// Setting pin output low will cause interrupt before ready +detachInterrupt( digitalPinToInterrupt( PS2_IrqPin ) ); +// set pins to outputs and high +digitalWrite( PS2_DataPin, HIGH ); +pinMode( PS2_DataPin, OUTPUT ); +digitalWrite( PS2_IrqPin, HIGH ); +pinMode( PS2_IrqPin, OUTPUT ); +// Essential for PS2 spec compliance +delayMicroseconds( 10 ); +// set Clock LOW +digitalWrite( PS2_IrqPin, LOW ); +// Essential for PS2 spec compliance +// set clock low for 60us +delayMicroseconds( 60 ); +// Set data low - Start bit +digitalWrite( PS2_DataPin, LOW ); +// set clock to input_pullup data stays output while writing to keyboard +pininput( PS2_IrqPin ); +// Restart interrupt handler +attachInterrupt( digitalPinToInterrupt( PS2_IrqPin ), ps2interrupt, FALLING ); +// wait clock interrupt to send data +} + + +/* Send next byte/command from TX queue and start sending + Must be ready to send and idle + Assumes commands consist of 1 or more bytes and wait for response then may or + not be followed by further bytes to send with or without response + Checks + 1/ Buffer empty return empty buffer + 2/ Busy return busy (will be checked by interrupt routines later) + 3/ Read next byte (next byte to send) + 4/ Check if following byte(s) are command/data or response + + Returns 1 if started transmission or queued + -134 if already busy + -2 if buffer empty + + Note PS2_KEY_IGNORE is used to denote a byte(s) expected in response */ +int16_t send_next( void ) +{ +uint8_t i; +int16_t val; + +val = -1; +// Check buffer not empty +i = _tx_tail; +if( i == _tx_head ) + return -2; + +// set command bit in _tx_ready as another command to do +_tx_ready |= _COMMAND; + +// Already item waiting to be sent or sending interrupt routines will call back +if( _tx_ready & _HANDSHAKE ) + return -134; + +// if busy let interrupt catch and call us again +if( _ps2mode & _PS2_BUSY ) + return -134; + +// Following only accessed when not receiving or sending protocol bytes +// Scan for command response and expected bytes to follow +_response_count = 0; +do + { + i++; + if( i >= _TX_BUFFER_SIZE ) + i = 0; + if( val == -1 ) + val = _tx_buff[ i ]; + else + if( _tx_buff[ i ] != PS2_KEY_IGNORE ) + break; + else + _response_count++; + _tx_tail = i; + } +while( i != _tx_head ); +// Now know what to send and expect start the actual wire sending +send_now( val ); +return 1; +} + + +/* Send a byte to the TX buffer + Value in buffer of PS2_KEY_IGNORE signifies wait for response, + use one for each byte expected + + Returns -4 - if buffer full (buffer overrun not written) + Returns 1 byte written when done */ +int send_byte( uint8_t val ) +{ +uint8_t ret; + +ret = _tx_head + 1; +if( ret >= _TX_BUFFER_SIZE ) + ret = 0; +if( ret != _tx_tail ) + { + _tx_buff[ ret ] = val; + _tx_head = ret; + return 1; + } +return -4; +} + + +// initialize a data pin for input +void pininput( uint8_t pin ) +{ +#ifdef INPUT_PULLUP +pinMode( pin, INPUT_PULLUP ); +#else +digitalWrite( pin, HIGH ); +pinMode( pin, INPUT ); +#endif +} + + +void ps2_reset( void ) +{ +/* reset buffers and states */ +_tx_head = 0; +_tx_tail = 0; +_tx_ready = 0; +_response_count = 0; +_head = 0; +_tail = 0; +_bitcount = 0; +PS2_keystatus = 0; +PS2_led_lock = 0; +_ps2mode = 0; +} + + + +/* Translate PS2 keyboard code sequence into our key code data + PAUSE key (_E1_MODE) is de_ALT with as special case, and + command responses not translated + + Called from read function as too long for in interrupt + + Returns 0 for no valid key or processed internally ignored or similar + 0 for empty buffer + */ +uint16_t translate( void ) +{ + uint8_t index, length, data, status; + uint16_t retdata; + + // get next character + // Check first something to fetch + index = _tail; + + // check for empty buffer + if( index == _head ) + return 0; + index++; + if( index >= _RX_BUFFER_SIZE ) + index = 0; + + // Special handling for PAUSE/BREAK, PAUSE key doesnt send a BREAK code yet MZ machines need SHIFT (hold) -> BREAK to recognise a BREAK, CTRL+BREAK will not work. + // In this case we inject a BREAK code by setting the flag on the received code. + status = (( _rx_buffer[ index ] & 0xFF00 ) >> 8); + if( (status & _E1_MODE) && (status & _BREAK)) + { + _rx_buffer[index] &= ~PS2_BREAK; + } else + { + _tail = index; + } + + // Get the flags byte break modes etc in this order + data = _rx_buffer[ index ] & 0xFF; + index = ( _rx_buffer[ index ] & 0xFF00 ) >> 8; + + // Catch special case of PAUSE key + if( index & _E1_MODE ) + { + return ( (uint16_t)PS2_keystatus << 8 ) | PS2_KEY_PAUSE | PS2_FUNCTION | (status & _BREAK ? 0 : PS2_BREAK); + } + + // Ignore anything not actual keycode but command/response + // Return untranslated as valid + if( ( data >= PS2_KC_BAT && data != PS2_KC_LANG1 && data != PS2_KC_LANG2 ) || ( index & _WAIT_RESPONSE ) ) + { + return ( uint16_t )data; + } + + // Gather the break of key status + if( index & _BREAK_KEY ) + { + PS2_keystatus |= _BREAK; + } else + { + PS2_keystatus &= ~_BREAK; + } + + retdata = 0; // error code by default + + // Scan appropriate table + if( index & _E0_MODE ) + { + length = sizeof( extended_key ) / sizeof( extended_key[ 0 ] ); + for( index = 0; index < length; index++ ) + { + #if defined( PS2_REQUIRES_PROGMEM ) + if( data == pgm_read_byte( &extended_key[ index ][ 0 ] ) ) + { + retdata = pgm_read_byte( &extended_key[ index ][ 1 ] ); + #else + if( data == extended_key[ index ][ 0 ] ) + { + retdata = extended_key[ index ][ 1 ]; + #endif + break; + } + } + } else + { + length = sizeof( single_key ) / sizeof( single_key[ 0 ] ); + for( index = 0; index < length; index++ ) + { + #if defined( PS2_REQUIRES_PROGMEM ) + if( data == pgm_read_byte( &single_key[ index ][ 0 ] ) ) + { + retdata = pgm_read_byte( &single_key[ index ][ 1 ] ); + #else + if( data == single_key[ index ][ 0 ] ) + { + retdata = single_key[ index ][ 1 ]; + #endif + break; + } + } + } + + // trap not found key + if( index == length ) + { + retdata = 0; + } + + /* valid found values only */ + if( retdata > 0 ) + { + if( retdata <= PS2_KEY_CAPS ) + { // process lock keys need second make to turn off + if( PS2_keystatus & _BREAK ) + { + PS2_lockstate[ retdata ] = 0; // Set received a break so next make toggles LOCK status + // MZ-2500 uses one make-break cycle to enable and one make-break to disable, so the BREAK is needed. + if(retdata != PS2_KEY_CAPS) + { + retdata = PS2_KEY_IGNORE; // ignore key + } + } else { + + if( PS2_lockstate[ retdata ] == 1 ) + { + retdata = PS2_KEY_IGNORE; // ignore key if make and not received break + // As per above, MZ-2500 needs both events, so this code changed from original authors. + } else + { + PS2_lockstate[ retdata ] = 1; + switch( retdata ) + { + case PS2_KEY_CAPS: + index = PS2_LOCK_CAPS; + + // Set CAPS lock if not set before + if( PS2_keystatus & _CAPS ) + { + PS2_keystatus &= ~_CAPS; + } else + { + PS2_keystatus |= _CAPS; + } + break; + case PS2_KEY_SCROLL: + index = PS2_LOCK_SCROLL; + break; + case PS2_KEY_NUM: + index = PS2_LOCK_NUM; + break; + } + // Now update PS2_led_lock status to match + if( PS2_led_lock & index ) + { + PS2_led_lock &= ~index; + if(index != PS2_LOCK_CAPS) + { + PS2_keystatus |= _BREAK; // send as break + } + } else + { + PS2_led_lock |= index; + } + set_lock( ); + } + } + } else + { + + if( retdata >= PS2_KEY_L_SHIFT && retdata <= PS2_KEY_R_GUI ) + { // Update bits for _SHIFT, _CTRL, _ALT, _ALT GR, _GUI in status + #if defined( PS2_REQUIRES_PROGMEM ) + index = pgm_read_byte( &control_flags[ retdata - PS2_KEY_L_SHIFT ] ); + #else + index = control_flags[ retdata - PS2_KEY_L_SHIFT ]; + #endif + if( PS2_keystatus & _BREAK ) + { + PS2_keystatus &= ~index; + } + + // if already set ignore repeats if flag set + else if( ( PS2_keystatus & index ) && ( _mode & _NO_REPEATS ) ) + { + retdata = PS2_KEY_IGNORE; // ignore repeat _SHIFT, _CTRL, _ALT, _GUI + } else + { + PS2_keystatus |= index; + } + } else + { + // Numeric keypad ONLY works in numlock state or when _SHIFT status + if( retdata >= PS2_KEY_KP0 && retdata <= PS2_KEY_KP_DOT ) + { + if( !( PS2_led_lock & PS2_LOCK_NUM ) || ( PS2_keystatus & _SHIFT ) ) + { + #if defined( PS2_REQUIRES_PROGMEM ) + retdata = pgm_read_byte( &scroll_remap[ retdata - PS2_KEY_KP0 ] ); + #else + retdata = scroll_remap[ retdata - PS2_KEY_KP0 ]; + #endif + } + } + } + + // Sort break code handling or ignore for all having processed the _SHIFT etc status + if( ( PS2_keystatus & _BREAK ) && ( _mode & _NO_BREAKS ) ) + { + return ( uint16_t )PS2_KEY_IGNORE; + } + + // Assign Function keys _mode + if( ( retdata <= PS2_KEY_SPACE || retdata >= PS2_KEY_F1) && retdata != PS2_KEY_BTICK && retdata != PS2_KEY_HASH && retdata != PS2_KEY_EUROPE2 ) + { + PS2_keystatus |= _FUNCTION; + } else + { + PS2_keystatus &= ~_FUNCTION; + } + } + } + return ( retdata | ( (uint16_t)PS2_keystatus << 8 ) ); +} + + +/* Build command to send lock status + Assumes data is within range */ +void set_lock( ) +{ +// ESP32: Heltec 32 Wifi - Bug found with a DELL KB-3926 keyboard. Basically sending more than one byte using this class +// mechanism of waiting for a response in the send/interrupt procedures results in a lockup. +// Workaround: Send one byte, wait for one response with a delay loop, send next byte. +send_byte( PS2_KC_LOCK ); // send command +if( ( send_byte( PS2_KEY_IGNORE ) ) ) // wait ACK + send_next( ); // if idle start transmission +// A delay of less than 10mS will cause lockup or abort!!! +delayMicroseconds( 10000 ); +send_byte( PS2_led_lock ); // send data from internal variable +if( ( send_byte( PS2_KEY_IGNORE ) ) ) // wait ACK + send_next( ); // if idle start transmission +} + + +/* Send echo command to keyboard + returned data in keyboard buffer read as keys */ +void PS2KeyAdvanced::echo( void ) +{ +send_byte( PS2_KC_ECHO ); // send command +if( ( send_byte( PS2_KEY_IGNORE ) ) ) // wait data PS2_KC_ECHO + send_next( ); // if idle start transmission +} + + +/* Get the ID used in keyboard + returned data in keyboard buffer read as keys */ +void PS2KeyAdvanced::readID( void ) +{ +send_byte( PS2_KC_READID ); // send command +send_byte( PS2_KEY_IGNORE ); // wait ACK +send_byte( PS2_KEY_IGNORE ); // wait data +if( ( send_byte( PS2_KEY_IGNORE ) ) ) // wait data + send_next( ); // if idle start transmission +} + + +/* Get the current Scancode Set used in keyboard + returned data in keyboard buffer read as keys */ +void PS2KeyAdvanced::getScanCodeSet( void ) +{ +send_byte( PS2_KC_SCANCODE ); // send command +if( ( send_byte( PS2_KEY_IGNORE ) ) ) // wait data + send_next( ); // if idle start transmission +delayMicroseconds( 10000 ); +send_byte( 0 ); // send data 0 = read +if( ( send_byte( PS2_KEY_IGNORE ) ) ) // wait data + send_next( ); // if idle start transmission +} + + +/* Returns the current status of Locks */ +uint8_t PS2KeyAdvanced::getLock( ) +{ +return( PS2_led_lock ); +} + + +/* Sets the current status of Locks and LEDs */ +void PS2KeyAdvanced::setLock( uint8_t code ) +{ +code &= 0xF; // To allow for rare keyboards with extra LED +PS2_led_lock = code; // update our lock copy +PS2_keystatus &= ~_CAPS; // Update copy of _CAPS lock as well +PS2_keystatus |= ( code & PS2_LOCK_CAPS ) ? _CAPS : 0; +set_lock( ); +} + + +/* Set library to not send break key codes + 1 = no break codes + 0 = send break codes */ +void PS2KeyAdvanced::setNoBreak( uint8_t data ) +{ +_mode &= ~_NO_BREAKS; +_mode |= data ? _NO_BREAKS : 0; +} + + /* Set library to not repeat make codes for _CTRL, _ALT, _GUI, _SHIFT + 1 = no repeat codes + 0 = send repeat codes */ +void PS2KeyAdvanced::setNoRepeat( uint8_t data ) +{ +_mode &= ~_NO_REPEATS; +_mode |= data ? _NO_REPEATS : 0; +} + + +/* Resets keyboard when reset has completed + keyboard sends AA - Pass or FC for fail */ +void PS2KeyAdvanced::resetKey( ) +{ +send_byte( PS2_KC_RESET ); // send command +send_byte( PS2_KEY_IGNORE ); // wait ACK +if( ( send_byte( PS2_KEY_IGNORE ) ) ) // wait data PS2_KC_BAT or PS2_KC_ERROR + send_next( ); // if idle start transmission +// LEDs and KeyStatus Reset too... to match keyboard +PS2_led_lock = 0; +PS2_keystatus = 0; +} + + +/* Send Typematic rate/delay command to keyboard + First Parameter rate is 0 - 0x1F (31) + 0 = 30 CPS + 0x1F = 2 CPS + default in keyboard is 0xB (10.9 CPS) + Second Parameter delay is 0 - 3 for 0.25s to 1s in 0.25 increments + default in keyboard is 1 = 0.5 second delay + Returned data in keyboard buffer read as keys + + Error returns 0 OK + -5 parameter error + */ +int PS2KeyAdvanced::typematic( uint8_t rate, uint8_t delay ) +{ +if( rate > 31 || delay > 3 ) + return -5; +send_byte( PS2_KC_RATE ); // send command +if( ( send_byte( PS2_KEY_IGNORE ) ) ) // wait ACK + send_next( ); // if idle start transmission +delayMicroseconds( 10000 ); +send_byte( ( delay << 5 ) + rate ); // Send values +if( ( send_byte( PS2_KEY_IGNORE ) ) ) // wait ACK + send_next( ); // if idle start transmission +return 0; +} + +uint8_t PS2KeyAdvanced::keyAvailable( ) +{ +int8_t i; + +i = _head - _tail; +if( i < 0 ) + i += _RX_BUFFER_SIZE; +return uint8_t( i ); +} + + +/* Returns count of available processed key codes + + If processed key buffer (_key_buffer) buffer returns max count + else processes input key code buffer until + either input buffer empty + or output buffer full + returns actual count + + Returns 0 buffer empty + 1 to buffer size less 1 as 1 to full buffer + + As with other ring buffers here when pointers match + buffer empty so cannot actually hold buffer size values */ +uint8_t PS2KeyAdvanced::available( ) +{ +int8_t i, idx; +uint16_t data; + +// check output queue +i = _key_head - _key_tail; +if( i < 0 ) + i += _KEY_BUFF_SIZE; +while( i < ( _KEY_BUFF_SIZE - 1 ) ) // process if not full +{ + if( keyAvailable( ) ) // not check for more keys to process + { + data = translate( ); // get next translated key + + if( data == 0 ) // unless in buffer is empty + break; + if( (data & 0xFF) != PS2_KEY_IGNORE && (data & 0xFF) > 0) + { + idx = _key_head + 1; // point to next space + if( idx >= _KEY_BUFF_SIZE ) // loop to front if necessary + idx = 0; + _key_buffer[ idx ] = data; // save the data to out buffer + _key_head = idx; + i++; // update count + } + } + else + break; // exit nothing coming in +} +return uint8_t( i ); +} + + +/* read a decoded key from the keyboard buffer + returns 0 for empty buffer */ +uint16_t PS2KeyAdvanced::read( ) +{ +uint16_t result; +uint8_t idx; + +while( ( result = available( ) ) ) + { + idx = _key_tail; + idx++; + if( idx >= _KEY_BUFF_SIZE ) // loop to front if necessary + idx = 0; + _key_tail = idx; + result = _key_buffer[ idx ]; + + // Filter out unwanted control data. + if((result & 0xFF) != PS2_KC_ACK && (result & 0xFF) != PS2_KC_RESEND && (result & 0xFF) != 0) + break; + } +return result; +} + +// Method to suspend the keyboard handler and disable the interrupts whilst other non-mutually inclusive tasks make +// use of system resources. +// +void PS2KeyAdvanced::suspend(bool suspend) +{ + // Suspend detaches the interrupt, ie. this module becomes inactive, attach re-enables the interrupt. + if(suspend) + { + detachInterrupt( digitalPinToInterrupt( PS2_IrqPin ) ); + } else + { + attachInterrupt( digitalPinToInterrupt( PS2_IrqPin ), ps2interrupt, FALLING ); + } + return; +} + +PS2KeyAdvanced::PS2KeyAdvanced( ) +{ +// nothing to do here, begin( ) does it all +} + +// Destructor - detach interrupts and free up resources. +PS2KeyAdvanced::~PS2KeyAdvanced(void) +{ + // Detach interrupts. + detachInterrupt( digitalPinToInterrupt( PS2_IrqPin ) ); +} + +/* instantiate class for keyboard */ +void PS2KeyAdvanced::begin( uint8_t data_pin, uint8_t irq_pin ) +{ +/* PS2 variables reset */ +ps2_reset( ); + +PS2_DataPin = data_pin; +PS2_IrqPin = irq_pin; + +// initialize the pins +pininput( PS2_IrqPin ); /* Setup Clock pin */ +pininput( PS2_DataPin ); /* Setup Data pin */ + +// Start interrupt handler +attachInterrupt( digitalPinToInterrupt( irq_pin ), ps2interrupt, FALLING ); +} diff --git a/main/PS2Mouse.cpp b/main/PS2Mouse.cpp deleted file mode 120000 index dfb287c..0000000 --- a/main/PS2Mouse.cpp +++ /dev/null @@ -1 +0,0 @@ -../../sharpkey/main/PS2Mouse.cpp \ No newline at end of file diff --git a/main/PS2Mouse.cpp b/main/PS2Mouse.cpp new file mode 100644 index 0000000..ec0f470 --- /dev/null +++ b/main/PS2Mouse.cpp @@ -0,0 +1,682 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// Name: PS2Mouse.cpp +// Created: Jan 2022 +// Version: v1.0 +// Author(s): Philip Smart +// Description: PS/2 Mouse Class. +// This source file contains the class to encapsulate a PS/2 mouse. Given two GPIO +// pins, datapin and clkpin, it is able to communicate, configure and return mouse +// data via a rich set of methods. +// +// This class borrows ideas from the interrupt concept of the PS2KeyAdvanced class +// for communicating via the PS/2 protocol. +// https://github.com/techpaul/PS2KeyAdvanced class from Paul Carpenter. +// +// The application uses the Espressif Development environment with Arduino components. +// This is necessary as the class uses the Arduino methods for GPIO manipulation. I +// was considering using pure Espressif IDF methods but considered the potential +// of also using this class on an Arduino project. +// +// Credits: +// Copyright: (c) 2022 Philip Smart +// +// History: Mar 2022 - Initial write. +// +// 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 "PS2Mouse.h" + +// Global handle to allow the static interrupt routine to access the instantiated object. This does limit this class to being Singleton +// but it is unusual to have more than 1 PS/2 mouse on a project so less of a problem. +PS2Mouse *pThis; + +// Constructor. Simple assign the hardware data and clock pins to internal variables and setup +// variables. Actual real initialisation is performed by a public method so re-initialisation can +// be made if required. +// +PS2Mouse::PS2Mouse(int clockPin, int dataPin) +{ + ps2Ctrl.clkPin = clockPin; + ps2Ctrl.dataPin = dataPin; + ps2Ctrl.supportsIntelliMouseExtensions = false; + ps2Ctrl.mouseDataCallback = NULL; +} + +// Destructor - Detach interrupts and free resources. +// +PS2Mouse::~PS2Mouse() +{ + // Disable interrupts. + detachInterrupt( digitalPinToInterrupt( ps2Ctrl.clkPin ) ); +} + +// The interrupt handler triggered on each falling edge of the clock pin. +// Rx Mode: 11 bits - <8 data bits> +// Tx Mode: 11 bits - <8 data bits> +IRAM_ATTR void PS2Mouse::ps2interrupt( void ) +{ + // Locals. + // + static uint32_t timeLast = 0; + uint32_t timeCurrent; + uint8_t dataBit; + + // Workaround for ESP32 SILICON error see extra/Porting.md + #ifdef PS2_ONLY_CHANGE_IRQ + if( digitalRead( ps2Ctrl.clkPin ) ) + return; + #endif + + // TRANSMIT MODE. + if( pThis->ps2Ctrl.mode & _TX_MODE ) + { + // Received data not valid when transmitting. + pThis->ps2Ctrl.rxPos = 0; + + // Now point to next bit + pThis->ps2Ctrl.bitCount++; + + // BIT 1 - START BIT + if(pThis->ps2Ctrl.bitCount == 1) + { + #if defined( PS2_CLEAR_PENDING_IRQ ) + // Start bit due to Arduino bug + digitalWrite(pThis->ps2Ctrl.dataPin, LOW); + break; + #endif + } else + // BIT 2->9 - DATA BIT MSB->LSB + if(pThis->ps2Ctrl.bitCount >= 2 && pThis->ps2Ctrl.bitCount <= 9) + { + // Data bits + dataBit = pThis->ps2Ctrl.shiftReg & 0x01; // get LSB + digitalWrite(pThis->ps2Ctrl.dataPin, dataBit); // send start bit + pThis->ps2Ctrl.parity += dataBit; // another one received ? + pThis->ps2Ctrl.shiftReg >>= 1; // right _SHIFT one place for next bit + } else + // BIT 10 - PARITY BIT + if(pThis->ps2Ctrl.bitCount == 10) + { + // Parity - Send LSB if 1 = odd number of 1's so ps2Ctrl.parity should be 0 + digitalWrite( pThis->ps2Ctrl.dataPin, ( ~pThis->ps2Ctrl.parity & 1 ) ); + } else + // BIT 11 - STOP BIT + if(pThis->ps2Ctrl.bitCount == 11) + { + // Stop bit write change to input pull up for high stop bit + digitalWrite( pThis->ps2Ctrl.dataPin, HIGH ); + pinMode( pThis->ps2Ctrl.dataPin, INPUT ); + } else + // BIT 12 - ACK BIT + if(pThis->ps2Ctrl.bitCount == 12) + { + // Acknowledge bit low we cannot do anything if high instead of low + // clear modes to receive again + pThis->ps2Ctrl.mode &= ~_TX_MODE; + pThis->ps2Ctrl.bitCount = 0; // end of byte + } else + { + // in case of weird error and end of byte reception re-sync + pThis->ps2Ctrl.bitCount = 0; + } + } + + // RECEIVE MODE. + else + { + // Read latest bit. + dataBit = digitalRead( pThis->ps2Ctrl.dataPin ); + + // Get current time. + timeCurrent = millis( ); + + // Reset the receive byte buffer pointer if the gap from the last received byte to the current time is greater than a packet interbyte delay. + if(timeCurrent - timeLast > 100) + { + pThis->ps2Ctrl.rxPos = 0; + } + + // Catch glitches, any clock taking longer than 250ms is either a glitch, an error or start of a new packet. + if( timeCurrent - timeLast > 250 ) + { + pThis->ps2Ctrl.bitCount = 0; + pThis->ps2Ctrl.shiftReg = 0; + } + + // Store current time for next loop to detect timing issues. + timeLast = timeCurrent; + + // Now point to next bit + pThis->ps2Ctrl.bitCount++; + + // BIT 1 - START BIT + if(pThis->ps2Ctrl.bitCount == 1) + { + // Start bit + pThis->ps2Ctrl.parity = 0; + pThis->ps2Ctrl.mode |= _PS2_BUSY; // set busy + } else + // BIT 2->9 - DATA BIT MSB->LSB + if(pThis->ps2Ctrl.bitCount >= 2 && pThis->ps2Ctrl.bitCount <= 9) + { + // Data bits + pThis->ps2Ctrl.parity += dataBit; // another one received ? + pThis->ps2Ctrl.shiftReg >>= 1; // right _SHIFT one place for next bit + pThis->ps2Ctrl.shiftReg |= ( dataBit ) ? 0x80 : 0; // or in MSbit + } else + // BIT 10 - PARITY BIT + if(pThis->ps2Ctrl.bitCount == 10) + { + // Parity check + pThis->ps2Ctrl.parity &= 1; // Get LSB if 1 = odd number of 1's so ps2Ctrl.parity bit should be 0 + if( pThis->ps2Ctrl.parity == dataBit ) // Both same ps2Ctrl.parity error + pThis->ps2Ctrl.parity = 0xFD; // To ensure at next bit count clear and discard + } else + // BIT 11 - STOP BIT + if(pThis->ps2Ctrl.bitCount == 11) + { + // Streaming mode, assemble the data into the buffer. + if(pThis->ps2Ctrl.streamingEnabled) + { + if(pThis->ps2Ctrl.rxPos == 0 && pThis->streaming.newData == true) pThis->streaming.overrun = true; + if(pThis->ps2Ctrl.rxPos == 0) pThis->streaming.mouseData.status = pThis->ps2Ctrl.shiftReg; + if(pThis->ps2Ctrl.rxPos == 1) pThis->streaming.mouseData.position.x = pThis->ps2Ctrl.shiftReg; + if(pThis->ps2Ctrl.rxPos == 2) pThis->streaming.mouseData.position.y = pThis->ps2Ctrl.shiftReg; + if(pThis->ps2Ctrl.rxPos == 3) pThis->streaming.mouseData.wheel = pThis->ps2Ctrl.shiftReg; + if( (pThis->ps2Ctrl.supportsIntelliMouseExtensions == false && pThis->ps2Ctrl.rxPos == 2) || (pThis->ps2Ctrl.supportsIntelliMouseExtensions == true && pThis->ps2Ctrl.rxPos == 3)) + { + pThis->streaming.newData = true; + pThis->streaming.overrun = false; + pThis->ps2Ctrl.rxPos = 0; + } else + { + pThis->ps2Ctrl.rxPos++; + } + } else + { + // Save the received byte and parity, let consumer decide on it's validity. + pThis->ps2Ctrl.rxBuf[pThis->ps2Ctrl.rxPos++] = (pThis->ps2Ctrl.parity << 8 | pThis->ps2Ctrl.shiftReg); + } + // Set mode and status for next receive byte + pThis->ps2Ctrl.mode &= ~( _WAIT_RESPONSE ); + pThis->ps2Ctrl.mode &= ~_PS2_BUSY; + pThis->ps2Ctrl.bitCount = 0; // end of byte + } else + { + // in case of weird error and end of byte reception re-sync + pThis->ps2Ctrl.bitCount = 0; + } + } +} + +// Method to write a byte (control or parameter) to the Mouse. This method encapsulates the protocol necessary +// to invoke Host -> PS/2 Mouse transmission and the interrupts, on falling clock edge, process the byte to send +// and bitbang accordingly. +// +void PS2Mouse::writeByte(uint8_t command) +{ + // Locals. + // + uint32_t currentTime = millis(); + + // Test to see if a transmission is underway, block until the xmit buffer becomes available or timeout expires (no mouse). + // + while((ps2Ctrl.mode & _TX_MODE) && currentTime+100 > millis()); + + // If TX_MODE has been reset, interrupt processing has occurred so line up next byte, + // + if((ps2Ctrl.mode & _TX_MODE) == 0) + { + // Initialise the ps2 control variables. + ps2Ctrl.shiftReg = command; + ps2Ctrl.bitCount = 1; + ps2Ctrl.parity = 0; + ps2Ctrl.mode |= _TX_MODE + _PS2_BUSY; + ps2Ctrl.rxPos = 0; + + // Initialise the streaming buffer. + streaming.mouseData.valid = false; + streaming.mouseData.status = 0; + streaming.mouseData.position.x = 0; + streaming.mouseData.position.y = 0; + streaming.mouseData.wheel = 0; + streaming.newData = false; + streaming.overrun = false; + + // STOP the interrupt handler - Setting pin output low will cause interrupt before ready + detachInterrupt( digitalPinToInterrupt( ps2Ctrl.clkPin ) ); + + // Set data and clock pins to output and high + digitalWrite(ps2Ctrl.dataPin, HIGH); + pinMode(ps2Ctrl.dataPin, OUTPUT); + digitalWrite(ps2Ctrl.clkPin, HIGH); + pinMode(ps2Ctrl.clkPin, OUTPUT); + + // Essential for PS2 spec compliance + delayMicroseconds(10); + + // Set Clock LOW - trigger Host -> Mouse transmission. Mouse controls the clock but dragging clock low is used by the mouse to detect a host write and clock + // data in accordingly. + digitalWrite( ps2Ctrl.clkPin, LOW ); + + // Essential for PS2 spec compliance, set clock low for 60us + delayMicroseconds(60); + + // Set data low - Start bit + digitalWrite( ps2Ctrl.dataPin, LOW ); + + // Set clock to input_pullup data stays output while writing to keyboard + digitalWrite(ps2Ctrl.clkPin, HIGH); + pinMode(ps2Ctrl.clkPin, INPUT); + + // Restart interrupt handler + attachInterrupt( digitalPinToInterrupt( ps2Ctrl.clkPin ), ps2interrupt, FALLING ); + } + + // Everything is now processed in the interrupt handler. + return; +} + +// Setup and initialise the running object and Mouse hardware. This method must be called at startup and anytime a full reset is required. +// +void PS2Mouse::initialize() +{ + // Setup variables. + ps2Ctrl.mode = 0; + ps2Ctrl.supportsIntelliMouseExtensions = false; + ps2Ctrl.streamingEnabled = false; + ps2Ctrl.bitCount = 0; + ps2Ctrl.shiftReg = 0; + ps2Ctrl.parity = 0; + ps2Ctrl.rxPos = 0; + // Clear the receive buffer. + for(int idx=0; idx < 16; idx++) ps2Ctrl.rxBuf[idx] = 0x00; + + // Set data and clock pins to input. + digitalWrite(ps2Ctrl.dataPin, HIGH); + pinMode(ps2Ctrl.dataPin, INPUT); + digitalWrite(ps2Ctrl.clkPin, HIGH); + pinMode(ps2Ctrl.clkPin, INPUT); + + // Initialise the control structure. + ps2Ctrl.bitCount = 0; + ps2Ctrl.mode = 0; + ps2Ctrl.rxPos = 0; + + // As the interrupt handler is static it wont have reference to the instantiated object methods so we need to store the object in a pointer + // which is then used by the interrupt handler. + pThis = this; + + // Attach the clock line to a falling low interrupt trigger and handler. The Mouse toggles the clock line for each bit to be sent/received + // so we interrupt on each falling clock edge. + attachInterrupt( digitalPinToInterrupt( ps2Ctrl.clkPin ), ps2interrupt, FALLING ); + + // Setup the mouse, make a reset, check and set Intellimouse extensions, set the resolution, scaling, sample rate to defaults and switch to remote (polled) mode. + reset(); + checkIntelliMouseExtensions(); + setResolution(PS2_MOUSE_RESOLUTION_1_8); + setScaling(PS2_MOUSE_SCALING_1_1); + setSampleRate(PS2_MOUSE_SAMPLE_RATE_40); + setRemoteMode(); + + // All done. + return; +} + +// Public method to force a mouse reset. Used on startup and anytime the client believes the mouse has hungup. +// +bool PS2Mouse::reset(void) +{ + // Locals. + // + uint8_t respBuf[5]; + bool result = false; + + // Send command to reset the mouse, if it returns an ACK then reset succeeded. + // + if(sendCmd(MOUSE_CMD_RESET, 0, respBuf, DEFAULT_MOUSE_TIMEOUT)) + { + result = true; + } + + // Return result. + return(result); +} + +// Private method to check and see if the mouse suports Microsoft Intellimouse extensions. It sets an internal state flag accordingly. +// +bool PS2Mouse::checkIntelliMouseExtensions(void) +{ + // Locals. + // + char deviceId; + + // IntelliMouse detection sequence, error checking isnt used. + setSampleRate(PS2_MOUSE_SAMPLE_RATE_200); + setSampleRate(PS2_MOUSE_SAMPLE_RATE_100); + setSampleRate(PS2_MOUSE_SAMPLE_RATE_80); + + // Get device Id and if the mouse supports Intellimouse extensions, it will reveal itself as an INTELLI_MOUSE. + deviceId = getDeviceId(); + ps2Ctrl.supportsIntelliMouseExtensions = (deviceId == INTELLI_MOUSE); + + // Return flag to indicate support (true) or no support (false). + return(ps2Ctrl.supportsIntelliMouseExtensions); +} + +// Public method to set the automatic sample rate. +// +bool PS2Mouse::setSampleRate(enum PS2_SAMPLING rate) +{ + // Locals. + // + uint8_t respBuf[5]; + bool result = false; + + // Sanity check. + if(rate == PS2_MOUSE_SAMPLE_RATE_10 || rate == PS2_MOUSE_SAMPLE_RATE_20 || rate == PS2_MOUSE_SAMPLE_RATE_40 || rate == PS2_MOUSE_SAMPLE_RATE_60 || rate == PS2_MOUSE_SAMPLE_RATE_80 || rate == PS2_MOUSE_SAMPLE_RATE_100 || rate == PS2_MOUSE_SAMPLE_RATE_200) + { + // Send command to set the mouse resolution. + // + if(sendCmd(MOUSE_CMD_SET_SAMPLE_RATE, 0, respBuf, DEFAULT_MOUSE_TIMEOUT)) + { + // Send the rate, if ACK is returned, then resolution set otherwise error. + if(sendCmd((uint8_t)rate, 0, respBuf, DEFAULT_MOUSE_TIMEOUT)) + { + result = true; + } + } + } + + // Return result. + return(result); +} + +// Public method to request the mouse Id which can be used to identify the mouse capabilities. +// +char PS2Mouse::getDeviceId(void) +{ + // Locals. + // + uint8_t respBuf[5]; + + // Send command to set the mouse scaling, either 2:1 or 1:1. + // + if(sendCmd(MOUSE_CMD_GET_DEVICE_ID, 1, respBuf, DEFAULT_MOUSE_TIMEOUT) == false) + { + respBuf[0] = 0xFF; + } + + // Return result. + return(respBuf[0]); +} + +// Public method to set the mouse scaling, either Normal 1:1 (scaling = 0) or non-linear 2:1 (scaling = 1). +// +bool PS2Mouse::setScaling(enum PS2_SCALING scaling) +{ + // Locals. + // + uint8_t respBuf[5]; + bool result = false; + + // Sanity check. + if(scaling >= PS2_MOUSE_SCALING_1_1 && scaling < PS2_MOUSE_SCALING_2_1) + { + // Send command to set the mouse scaling, either 2:1 or 1:1. + // + if(sendCmd((uint8_t)scaling, 0, respBuf, DEFAULT_MOUSE_TIMEOUT)) + { + result = true; + } + } + + // Return result. + return(result); +} + +// Public method to request the mouse enters remote mode. +// +bool PS2Mouse::setRemoteMode(void) +{ + // Locals. + // + uint8_t respBuf[5]; + + // Simply pass on the request to the mouse to enter remote mode. + return(sendCmd(MOUSE_CMD_SET_REMOTE_MODE, 1, respBuf, DEFAULT_MOUSE_TIMEOUT)); +} + +// Public method to request the mouse enters stream mode. This mode reports mouse movements as they change, albeit the streaming must also be enabled +// once set to Stream Mode via the enableStreaming method. +// +bool PS2Mouse::setStreamMode(void) +{ + // Locals. + // + uint8_t respBuf[5]; + + // Simply pass on the request to the mouse to enter stream mode. + return(sendCmd(MOUSE_CMD_SET_STREAM_MODE, 1, respBuf, DEFAULT_MOUSE_TIMEOUT)); +} + +// Public methods to enable and disable streaming (constant rate packet transmission from mouse to host). +// This module accepts the data and updates an in object set which the caller queries. No buffering takes place +// so should the caller fail to read the data then the arrival of the next packet from the mouse will override +// the in object values. +// +bool PS2Mouse::enableStreaming(void) +{ + // Locals. + // + uint8_t respBuf[5]; + + // Sanity check. + if(ps2Ctrl.streamingEnabled == false) + { + if(sendCmd(MOUSE_CMD_ENABLE_STREAMING, 0, respBuf, DEFAULT_MOUSE_TIMEOUT)) + { + // Initialise the streaming buffer. + streaming.mouseData.valid = false; + streaming.mouseData.status = 0; + streaming.mouseData.position.x = 0; + streaming.mouseData.position.y = 0; + streaming.mouseData.wheel = 0; + streaming.newData = false; + streaming.overrun = false; + ps2Ctrl.streamingEnabled = true; + } + } + + // Return the enabled flag to indicate success. + return(ps2Ctrl.streamingEnabled); +} +bool PS2Mouse::disableStreaming(void) +{ + // Locals. + // + uint8_t respBuf[5]; + + // Sanity check. + if(ps2Ctrl.streamingEnabled == true) + { + if(sendCmd(MOUSE_CMD_DISABLE_STREAMING, 0, respBuf, DEFAULT_MOUSE_TIMEOUT)) + { + ps2Ctrl.streamingEnabled = false; + } + } + + // Return the enabled flag to indicate success. + return(ps2Ctrl.streamingEnabled); +} + +// Public method to set the mouse resolution in pixels per millimeter, valid values are o..3. +// +bool PS2Mouse::setResolution(enum PS2_RESOLUTION resolution) +{ + // Locals. + // + uint8_t respBuf[5]; + bool result = false; + + // Sanity check. + if(resolution >= PS2_MOUSE_RESOLUTION_1_1 && resolution < PS2_MOUSE_RESOLUTION_1_8) + { + // Send command to set the mouse resolution. + // + if(sendCmd(MOUSE_CMD_SET_RESOLUTION, 0, respBuf, DEFAULT_MOUSE_TIMEOUT)) + { + // Send the resolution, if ACK is returned, then resolution set otherwise error. + if(sendCmd((uint8_t)resolution, 0, respBuf, DEFAULT_MOUSE_TIMEOUT)) + { + result = true; + } + } + } + + // Return result. + return(result); +} + +// Public method to get the current mouse status. The status code is 3 bytes wide and has the following format: +// +// 7 6 5 4 3 2 1 0 +// Byte 1: 0 mode enable scaling 0 left btn middle right btn +// Byte 2: resolution +// Byte 3: sample rate +// +bool PS2Mouse::getStatus(uint8_t *respBuf) +{ + // Locals. + // + bool result = false; + + // Sanity check. + if(respBuf != NULL) + { + // Send command to set the mouse resolution. + // + if(sendCmd(MOUSE_CMD_GET_STATUS, 3, respBuf, DEFAULT_MOUSE_TIMEOUT)) + { + result = true; + } + } + + // Return result. + return(result); +} + +// Public method to obtain current mouse state data. +// +PS2Mouse::MouseData PS2Mouse::readData(void) +{ + // Locals. + MouseData data; + uint8_t dataBuf[8] = {0,0,0,0,0,0,0,0}; + + // If streaming mode enabled then set values according to data state. Data only valid if a new update has occurred since last call otherwise old data is returned and valid flag + // is cleared. + if(ps2Ctrl.streamingEnabled) + { + data.valid = streaming.newData; + data.overrun = streaming.overrun; + data.status = streaming.mouseData.status; + data.position.x = streaming.mouseData.position.x; + data.position.y = streaming.mouseData.position.y; + data.wheel = ps2Ctrl.supportsIntelliMouseExtensions ? streaming.mouseData.wheel : 0; + streaming.newData = false; + streaming.overrun = false; + + // If a data callback has been setup execute it otherwise data is read by caller. + // + if(ps2Ctrl.mouseDataCallback != NULL && data.valid) + ps2Ctrl.mouseDataCallback(data); + } else + // Single on-request data set from mouse. + { + // Request data from mouse via issuing get single data packet command. + if(requestData(ps2Ctrl.supportsIntelliMouseExtensions ? 3 : 3, dataBuf, DEFAULT_MOUSE_TIMEOUT)) + { + data.valid = true; + data.overrun = false; + data.status = dataBuf[0]; + data.position.x = dataBuf[1]; + data.position.y = dataBuf[2]; + data.wheel = ps2Ctrl.supportsIntelliMouseExtensions ? dataBuf[3] : 0; + } else + { + data.valid = false; + data.overrun = false; + } + } + + return data; +}; + +// Method to request the latest mouse movement, wheel and key data. The method blocks until data is available or the timeout is reached. A timeout of 0 +// will only return when the data has been received. +bool PS2Mouse::requestData(uint8_t expectedBytes, uint8_t *respBuf, uint32_t timeout) +{ + // Locals. + // + + // Simply pass on the request for the mouse to send data and await reply. + return(sendCmd(MOUSE_CMD_REQUEST_DATA, expectedBytes, respBuf, timeout)); +} + +// Method to send a command to the Mouse and await it's reply. If an ACK isnt returned then a resend request is made otherwise wait until all bytes +// arrive or we timeout. +// +bool PS2Mouse::sendCmd(uint8_t cmd, uint8_t expectedBytes, uint8_t *respBuf, uint32_t timeout) +{ + // Locals. + // + uint32_t currentTime = millis(); + uint32_t endTime = millis() + timeout; + uint8_t *pBuf = respBuf; + bool result = false; + + // Send command. + writeByte(cmd); + + // Wait for the expected number of bytes to arrive. + while(((timeout == 0) || (currentTime < endTime)) && ps2Ctrl.rxPos <= expectedBytes) + { + // If an ACK isnt received, request a resend. + if(ps2Ctrl.rxPos >= 1 && ps2Ctrl.rxBuf[0] != MOUSE_RESP_ACK) { writeByte(MOUSE_CMD_RESEND); } + + // Get latest time. + currentTime = millis(); + } + + // Store the response in callers buffer. + for(int idx=0; idx < expectedBytes; idx++) + { + (*pBuf) = ps2Ctrl.rxBuf[idx+1]; + pBuf++; + } + + // Set return code, true if a valid packet was received. + if(((timeout == 0) || (currentTime < endTime)) && ps2Ctrl.rxPos >= expectedBytes && ps2Ctrl.rxBuf[0] == MOUSE_RESP_ACK) result = true; + + // Debug print. + //printf("%d:%d:%02x,%02x,%02x,%02x, %02x, %d, result=%d, %d, %d, %d\n", result, ps2Ctrl.rxPos, ps2Ctrl.rxBuf[0], ps2Ctrl.rxBuf[1], ps2Ctrl.rxBuf[2], ps2Ctrl.rxBuf[3],ps2Ctrl.rxBuf[4], ps2Ctrl.bitCount, result, timeout, currentTime, endTime); + + // And complete with result! + return(result); +} diff --git a/main/SWITCH.cpp b/main/SWITCH.cpp deleted file mode 120000 index 4ff989e..0000000 --- a/main/SWITCH.cpp +++ /dev/null @@ -1 +0,0 @@ -../../sharpkey/main/SWITCH.cpp \ No newline at end of file diff --git a/main/SWITCH.cpp b/main/SWITCH.cpp new file mode 100644 index 0000000..d5fc80c --- /dev/null +++ b/main/SWITCH.cpp @@ -0,0 +1,216 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// Name: SWITCH.cpp +// Created: May 2022 +// Version: v1.0 +// Author(s): Philip Smart +// Description: Base class for encapsulating the SharpKey WiFi/Config switch. +// Credits: +// Copyright: (c) 2019-2022 Philip Smart +// +// History: May 2022 - Initial write. +// v1.00 Jun 2022 - Updates to add additional callbacks for RESET and CLEARNVS +// +// 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 "freertos/semphr.h" +#include "esp_log.h" +#include "esp_system.h" +#include "nvs_flash.h" +#include "nvs.h" +#include "driver/gpio.h" +#include "soc/timer_group_struct.h" +#include "soc/timer_group_reg.h" +#include "driver/timer.h" +#include "sdkconfig.h" +#include "SWITCH.h" + +// Primary SWITCH thread, running on Core 0. +// This thread is responsible for scanning the config/WiFi key on the SharpKey and generating callbacks according to state. +// +IRAM_ATTR void SWITCH::swInterface( void * pvParameters ) +{ + // Locals. + // + uint32_t keyDebCtr = 0; + uint32_t WIFIEN_MASK = (1 << (CONFIG_IF_WIFI_EN_KEY - 32)); + uint32_t resetTimer = 0; + #define WIFIIFTAG "swInterface" + + // Map the instantiating object so we can access its methods and data. + SWITCH* pThis = (SWITCH*)pvParameters; + + // Loop indefinitely. + while(true) + { + // Check the switch, has it gone to zero, ie. pressed? + // + if((REG_READ(GPIO_IN1_REG) & WIFIEN_MASK) == 0) + { + // First press detection turn LED off. + if(keyDebCtr == 0) + { + pThis->led->setLEDMode(LED::LED_MODE_OFF, LED::LED_DUTY_CYCLE_OFF, 0, 0L, 0L); + } + // Entering WiFi enable mode, blink LED + if(keyDebCtr == 10) + { + pThis->led->setLEDMode(LED::LED_MODE_BLINK, LED::LED_DUTY_CYCLE_50, 1, 50000L, 500L); + } + // Enter default AP mode. + if(keyDebCtr == 50) + { + pThis->led->setLEDMode(LED::LED_MODE_BLINK, LED::LED_DUTY_CYCLE_30, 1, 25000L, 250L); + } + // Enter BT pairing mode. + if(keyDebCtr == 100) + { + pThis->led->setLEDMode(LED::LED_MODE_BLINK, LED::LED_DUTY_CYCLE_10, 1, 10000L, 100L); + } + // Enter Clear NVS settings mode. + if(keyDebCtr == 150) + { + pThis->led->setLEDMode(LED::LED_MODE_BLINK, LED::LED_DUTY_CYCLE_80, 5, 10000L, 1000L); + } + // Increment counter so we know how long it has been held. + keyDebCtr++; + } else + if((REG_READ(GPIO_IN1_REG) & WIFIEN_MASK) != 0 && keyDebCtr > 1) + { + // On first 1/2 second press, if WiFi active, disable and reboot. + if(keyDebCtr > 1 && keyDebCtr < 10) + { + // If a cancel callback has been setup, invoke it. + // + if(pThis->swCtrl.cancelEventCallback != NULL) + pThis->swCtrl.cancelEventCallback(); + + // If the reset timer is running then a previous button press occurred. If it is less than 1 second then a RESET event + // is required. + if(resetTimer != 0 && (pThis->milliSeconds() - resetTimer) < 1000L) + { + // If a handler is installed call it. If the return value is true then a restart is possible. No handler then we just restart. + if(pThis->swCtrl.resetEventCallback != NULL) + { + if(pThis->swCtrl.resetEventCallback()) + esp_restart(); + } else + esp_restart(); + } else + { + resetTimer = pThis->milliSeconds(); + } + } + // If counter is in range 1 to 4 seconds then assume a WiFi on (so long as the client parameters have been configured). + else if(keyDebCtr > 10 && keyDebCtr < 40) + { + // If a wifi enable callback has been setup, invoke it. + // + if(pThis->swCtrl.wifiEnEventCallback != NULL) + pThis->swCtrl.wifiEnEventCallback(); + } + // If the key is held for 5 or more seconds, then enter Wifi Config Default AP mode. + else if(keyDebCtr > 50 && keyDebCtr < 100) + { + // If a wifi default enable callback has been setup, invoke it. + // + if(pThis->swCtrl.wifiDefEventCallback != NULL) + pThis->swCtrl.wifiDefEventCallback(); + } + // If the key is held for 10 seconds or more, invoke Bluetooth pairing mode. + else if(keyDebCtr >= 100 && keyDebCtr < 150) + { + // If a bluetooth start pairing callback has been setup, invoke it. + // + if(pThis->swCtrl.btPairingEventCallback != NULL) + pThis->swCtrl.btPairingEventCallback(); + } + // If the key is held for 15 seconds or more, invoke the clear NVS settings (factory) mode. + else if(keyDebCtr >= 150) + { + // If a clear NVS handler has been installed, call it. + // + if(pThis->swCtrl.clearNVSEventCallback != NULL) + pThis->swCtrl.clearNVSEventCallback(); + } + + // LED off, no longer needed. + pThis->led->setLEDMode(LED::LED_MODE_OFF, LED::LED_DUTY_CYCLE_OFF, 0, 0L, 0L); + + // Re-init switch variables for next activation. + keyDebCtr = 0; + } + + // Reset the reset timer if not activated. + if(resetTimer != 0 && (pThis->milliSeconds() - resetTimer) > 2000L) { resetTimer = 0; } + + // Let other tasks run. NB. This value affects the debounce counter, update as necessary. + vTaskDelay(100); + } + return; +} + +// Initialisation routine. Setup variables and spawn a task to monitor the config switch. +// +void SWITCH::init(void) +{ + // Initialise control variables. + #define SWINITTAG "SWINIT" + + // Core 0 - Application + // SWITCH handler thread. + ESP_LOGW(SWINITTAG, "Starting SWITCH thread..."); + ::xTaskCreatePinnedToCore(&this->swInterface, "switch", 4096, this, 0, &this->swCtrl.TaskSWIF, 0); + vTaskDelay(1500); +} + +// Basic constructor, init variables! +SWITCH::SWITCH(LED *hdlLED) +{ + swCtrl.cancelEventCallback = NULL; + swCtrl.wifiEnEventCallback = NULL; + swCtrl.wifiDefEventCallback = NULL; + swCtrl.btPairingEventCallback = NULL; + + // Store the class name for later use. + this->swCtrl.swClassName = getClassName(__PRETTY_FUNCTION__); + + // Save the LED object so it can be used to warn the user. + this->led = hdlLED; + + // Initialse the SWITCH object. + init(); +} + +// Basic consructor, do nothing! +SWITCH::SWITCH(void) +{ + // Store the class name for later use. + this->swCtrl.swClassName = getClassName(__PRETTY_FUNCTION__); +} + +// Basic destructor. +SWITCH::~SWITCH(void) +{ +} diff --git a/main/SharpKey.cpp b/main/SharpKey.cpp deleted file mode 120000 index f65eab1..0000000 --- a/main/SharpKey.cpp +++ /dev/null @@ -1 +0,0 @@ -../../sharpkey/main/SharpKey.cpp \ No newline at end of file diff --git a/main/SharpKey.cpp b/main/SharpKey.cpp new file mode 100644 index 0000000..6df6cf7 --- /dev/null +++ b/main/SharpKey.cpp @@ -0,0 +1,1074 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// Name: SharpKey.cpp +// Created: Jan 2022 +// Version: v1.0 +// Author(s): Philip Smart +// Description: HID (PS/2, Bluetooth) to Sharp Keyboard and Mouse Interface. +// This source file contains the application logic to interface several Sharp MZ/X +// machines to a HID (PS/2 keyboard, PS/2 Mouse, Bluetooth Keyboard/Mouse). The type +// of host is determined by the host i/o lines and the necessary control threads +// instantiated accordingly. +// +// Please see the individual classes (singleton obiects) for a specific host logic. +// +// The application uses the Espressif Development environment with Arduino components. +// This is necessary for the PS2KeyAdvanced class, which may in future be converted to +// use esp-idf library calls rather than Arduino. I wrote the PS2Mouse class using +// Arduino as well, so both will need conversion eventually. +// +// The Espressif environment is necessary in order to have more control over the build. +// It is important, for timing, that Core 1 is dedicated to the MZ 2599/2800 Interface +// logic due to timing constraints and Core 0 is used for all RTOS/Interrupts tasks. +// Other host interface classes freely use both cores. +// +// The application is configured via the Kconfig system. Use 'idf.py menuconfig' to +// configure. +// Credits: +// Copyright: (c) 2022 Philip Smart +// +// History: Jan 2022 - Initial write. +// Feb 2022 - Updates and fixes. Added logic to detect PS/2 disconnect and reconnect, +// added 3 alternative maps selected by ALT+F1 (MZ2500), ALT+F2(MZ2000) +// ALT+F3(MZ80B) due to slight differences in the key layout. +// Added framework for wifi so that the interface can enter AP mode to +// acquire local net parameters then connect to local net. Needs the web +// interface writing. +// Mar 2022 - Split from mz25key and modularised to allow multiple host targets. +// Apr 2022 - Rewrote the application as C++ classes, the host interfaces are based +// on a Base class (KeyInterface) which virtualises the interface and +// provides some base methods and variables, each host inherits the Base class +// to form individual singleton objects. +// Apr 2022 - Added X1, X68000 Keyboard functionality and Mouse functionality. +// Apr 2022 - Started on tests for using Bluetooth, moved all input devices (Human +// Input Device) into its own class to encapsulate PS/2 Keyboard, Mouse and +// Bluetooth Keyboard/Mouse. +// v1.01 May 2022 - More objectifying the fundamental components to make it easier to upgrade. +// v1.02 May 2022 - Initial release version. +// v1.03 May 2022 - Added feature security and efuse build data. +// v1.04 Jun 2022 - Reworked the Wifi so that when requested, the SharpKey reboots and +// immediately starts up in WiFi mode without enabling BT or hardware I/F. +// This is necessary due to shared antenna in the ESP32 and also clashes +// in the IDF library stack. +// +// 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 +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_log.h" +#include "esp_system.h" +#include "esp_efuse.h" +#include "esp_efuse_table.h" +#include "esp_efuse_custom_table.h" +#include "nvs_flash.h" +#include "nvs.h" +#include "Arduino.h" +#include "driver/gpio.h" +#include "soc/timer_group_struct.h" +#include "soc/timer_group_reg.h" +#include "soc/soc.h" +#include "soc/rtc_cntl_reg.h" +#include "sdkconfig.h" +#include "MZ2528.h" +#include "X1.h" +#include "X68K.h" +#include "MZ5665.h" +#include "PC9801.h" +#include "Mouse.h" +#include "HID.h" +#include "NVS.h" +#include "WiFi.h" + +////////////////////////////////////////////////////////////////////////// +// Important: +// +// All configuration is performed via the 'idf.py menuconfig' command. +// The file 'sdkconfig' contains the configured parameter defines. +////////////////////////////////////////////////////////////////////////// + +// Constants. +#define SHARPKEY_NAME "SharpKey" +#define SHARPKEY_VERSION 1.04 +#define SHARPKEY_MODULES "SharpKey MZ2528 X1 X68K MZ5665 PC9801 Mouse KeyInterface HID NVS LED SWITCH WiFi FilePack" + +// Tag for ESP main application logging. +#define MAINTAG "sharpkey" + +// LittleFS default parameters. +#define LITTLEFS_DEFAULT_PATH "/littlefs" +#define LITTLEFS_DEFAULT_PARTITION "filesys" + +// Structure for configuration information stored in NVS. +struct SharpKeyConfig { + struct { + uint8_t bootMode; // Flag to indicate the mode SharpKey should boot into. + // 0 = Interface, 1 = WiFi (configured), 2 = WiFi (default). + } params; +} sharpKeyConfig; + +// Overloads for the EFUSE Custom MAC definitions. Limited Efuse space and Custom MAC not needed in eFuse in this design +// so we overload with custom flags. +// 0-7 Reserved +// 8-15 defined as Base configuration and enhanced set 1. +static const esp_efuse_desc_t ENABLE_BT[] = { + {EFUSE_BLK3, 8, 1}, +}; +static const esp_efuse_desc_t ENABLE_MZ5665[] = { + {EFUSE_BLK3, 9, 1}, +}; +static const esp_efuse_desc_t ENABLE_PC9801[] = { + {EFUSE_BLK3, 10, 1}, +}; +static const esp_efuse_desc_t ENABLE_MOUSE[] = { + {EFUSE_BLK3, 11, 1}, +}; +static const esp_efuse_desc_t ENABLE_X68000[] = { + {EFUSE_BLK3, 12, 1}, +}; +static const esp_efuse_desc_t ENABLE_X1[] = { + {EFUSE_BLK3, 13, 1}, +}; +static const esp_efuse_desc_t ENABLE_MZ2800[] = { + {EFUSE_BLK3, 14, 1}, +}; +static const esp_efuse_desc_t ENABLE_MZ2500[] = { + {EFUSE_BLK3, 15, 1}, +}; +const esp_efuse_desc_t* ESP_EFUSE_ENABLE_BT[] = { + &ENABLE_BT[0], + NULL +}; +const esp_efuse_desc_t* ESP_EFUSE_ENABLE_MZ5665[] = { + &ENABLE_MZ5665[0], + NULL +}; +const esp_efuse_desc_t* ESP_EFUSE_ENABLE_PC9801[] = { + &ENABLE_PC9801[0], + NULL +}; +const esp_efuse_desc_t* ESP_EFUSE_ENABLE_X68000[] = { + &ENABLE_X68000[0], + NULL +}; +const esp_efuse_desc_t* ESP_EFUSE_ENABLE_X1[] = { + &ENABLE_X1[0], + NULL +}; +const esp_efuse_desc_t* ESP_EFUSE_ENABLE_MZ2800[] = { + &ENABLE_MZ2800[0], + NULL +}; +const esp_efuse_desc_t* ESP_EFUSE_ENABLE_MZ2500[] = { + &ENABLE_MZ2500[0], + NULL +}; +const esp_efuse_desc_t* ESP_EFUSE_ENABLE_MOUSE[] = { + &ENABLE_MOUSE[0], + NULL +}; + +// Revision control, stored in EFUSE block. +typedef struct { + uint16_t hardwareRevision; // Hardware revision, stored as x/1000 - gives range 0.000 - 64.999 + uint16_t serialNo; // Serial Number of the device. + uint8_t buildDate[3]; // Build date of the hardware. + bool disableRestrictions; // Flag to indicate all firmware restrictions should be disabled. + bool enableMZ2500; // Flag to indicate MZ-2500 functionality should be enabled. + bool enableMZ2800; // Flag to indicate MZ-2800 functionality should be enabled. + bool enableX1; // Flag to indicate X1 functionality should be enabled. + bool enableX68000; // Flag to indicate X68000 functionality should be enabled. + bool enableMouse; // Flag to indicate Mouse functionality should be enabled. + bool enableBluetooth; // Flag to indicate Bluetooth functionality should be enabled. + bool enableMZ5665; // Flag to indicate MZ-6500 functionality should be enabled. + bool enablePC9801; // Flag to indicate NEC PC-9801 functionality should be enabled. +} t_EFUSE; + +// Method to check the efuse coding scheme is disabled. For this project it should be disabled. +bool checkEFUSE(void) +{ + // Locals. + bool result = false; + size_t secureVersion = 0; + + // Check the efuse coding scheme, should be NONE and the security version should be 0 for this project. + esp_efuse_coding_scheme_t coding_scheme = esp_efuse_get_coding_scheme(EFUSE_BLK3); + if(coding_scheme == EFUSE_CODING_SCHEME_NONE) + { + ESP_ERROR_CHECK(esp_efuse_read_field_cnt(ESP_EFUSE_SECURE_VERSION, &secureVersion)); + if(secureVersion == 0) + { + result = true; + } + } + + // True = efuse present and correct, false = not recognised. + return(result); +} + +// Method to read out the stored configuration from EFUSE into the configuration structure +// for later appraisal. +bool readEFUSE(t_EFUSE &sharpkeyEfuses) +{ + // Locals. + bool result = true; + + // Manually read each fuse value into the given structure, any failures treat as a complete failure. + result = esp_efuse_read_field_blob(ESP_EFUSE_HARDWARE_REVISION, &sharpkeyEfuses.hardwareRevision, 16) == ESP_OK ? result : false; sharpkeyEfuses.hardwareRevision = __builtin_bswap16(sharpkeyEfuses.hardwareRevision); + result = esp_efuse_read_field_blob(ESP_EFUSE_SERIAL_NO, &sharpkeyEfuses.serialNo, 16) == ESP_OK ? result : false; sharpkeyEfuses.serialNo = __builtin_bswap16(sharpkeyEfuses.serialNo); + result = esp_efuse_read_field_blob(ESP_EFUSE_BUILD_DATE, &sharpkeyEfuses.buildDate, 24) == ESP_OK ? result : false; + result = esp_efuse_read_field_blob(ESP_EFUSE_DISABLE_RESTRICTIONS, &sharpkeyEfuses.disableRestrictions, 1) == ESP_OK ? result : false; + result = esp_efuse_read_field_blob(ESP_EFUSE_ENABLE_BT, &sharpkeyEfuses.enableBluetooth, 1) == ESP_OK ? result : false; + result = esp_efuse_read_field_blob(ESP_EFUSE_ENABLE_MZ2500, &sharpkeyEfuses.enableMZ2500, 1) == ESP_OK ? result : false; + result = esp_efuse_read_field_blob(ESP_EFUSE_ENABLE_MZ2800, &sharpkeyEfuses.enableMZ2800, 1) == ESP_OK ? result : false; + result = esp_efuse_read_field_blob(ESP_EFUSE_ENABLE_X1, &sharpkeyEfuses.enableX1, 1) == ESP_OK ? result : false; + result = esp_efuse_read_field_blob(ESP_EFUSE_ENABLE_X68000, &sharpkeyEfuses.enableX68000, 1) == ESP_OK ? result : false; + result = esp_efuse_read_field_blob(ESP_EFUSE_ENABLE_MZ5665, &sharpkeyEfuses.enableMZ5665, 1) == ESP_OK ? result : false; + result = esp_efuse_read_field_blob(ESP_EFUSE_ENABLE_PC9801, &sharpkeyEfuses.enablePC9801, 1) == ESP_OK ? result : false; + result = esp_efuse_read_field_blob(ESP_EFUSE_ENABLE_MOUSE, &sharpkeyEfuses.enableMouse, 1) == ESP_OK ? result : false; + + // Return true = successful read, false = failed to read efuse or values. + return(result); +} + +// Method to write the configuration to one-time programmable FlashRAM EFuses. This setting persists for the life of the SharpKey +// and so minimal information is stored which cant be wiped, everything else uses reprogrammable FlashRAM via NVS. +bool writeEFUSE(t_EFUSE &sharpkeyEfuses) +{ + // Locals. + bool result = true; +#ifdef CONFIG_EFUSE_VIRTUAL + + // Write out the configuration structure member at a time. + result = esp_efuse_write_field_blob(ESP_EFUSE_HARDWARE_REVISION, &sharpkeyEfuses.hardwareRevision, 16) == ESP_OK ? result : false; + result = esp_efuse_write_field_blob(ESP_EFUSE_SERIAL_NO, &sharpkeyEfuses.serialNo, 16) == ESP_OK ? result : false; + result = esp_efuse_write_field_blob(ESP_EFUSE_BUILD_DATE, &sharpkeyEfuses.buildDate, 24) == ESP_OK ? result : false; + result = esp_efuse_write_field_blob(ESP_EFUSE_DISABLE_RESTRICTIONS, &sharpkeyEfuses.disableRestrictions, 1) == ESP_OK ? result : false; + result = esp_efuse_write_field_blob(ESP_EFUSE_ENABLE_BT, &sharpkeyEfuses.enableBluetooth, 1) == ESP_OK ? result : false; + result = esp_efuse_write_field_blob(ESP_EFUSE_ENABLE_MZ2500, &sharpkeyEfuses.enableMZ2500, 1) == ESP_OK ? result : false; + result = esp_efuse_write_field_blob(ESP_EFUSE_ENABLE_MZ2800, &sharpkeyEfuses.enableMZ2800, 1) == ESP_OK ? result : false; + result = esp_efuse_write_field_blob(ESP_EFUSE_ENABLE_X1, &sharpkeyEfuses.enableX1, 1) == ESP_OK ? result : false; + result = esp_efuse_write_field_blob(ESP_EFUSE_ENABLE_X68000, &sharpkeyEfuses.enableX68000, 1) == ESP_OK ? result : false; + result = esp_efuse_write_field_blob(ESP_EFUSE_ENABLE_MZ5665, &sharpkeyEfuses.enableMZ5665, 1) == ESP_OK ? result : false; + result = esp_efuse_write_field_blob(ESP_EFUSE_ENABLE_PC9801, &sharpkeyEfuses.enablePC9801, 1) == ESP_OK ? result : false; + result = esp_efuse_write_field_blob(ESP_EFUSE_ENABLE_MOUSE, &sharpkeyEfuses.enableMouse, 1) == ESP_OK ? result : false; + +#endif // CONFIG_EFUSE_VIRTUAL + // Return true for success, false for 1 or more failures. + return(result); +} + +// Method to return the application version number. +float version(void) +{ + return(SHARPKEY_VERSION); +} + +// Method to startup the WiFi interface. +// Starting the WiFi method requires no Bluetooth or running host interface threads. It is started after a fresh boot. This is necessary due to the ESP IDF +// and hardware antenna constraints. +// +#if defined(CONFIG_IF_WIFI_ENABLED) +void startWiFi(NVS &nvs, LED *led, bool defaultMode, uint32_t ifMode) +{ + // Locals. + // + KeyInterface *keyIf = NULL; + KeyInterface *mouseIf = NULL; + HID *hid = NULL; + SWITCH *sw = NULL; + WiFi *wifi = NULL; + + // The WiFi interface needs to report version numbers so an end user can view which version of an object is built-in, used for error tracking and firmware upgrades. + // In order to do this, build up a structure of object and version numbers which is passed into the WiFi interface object. The structure is defined within wifi.h as technically + // it belongs to that object but needs to be evaluated in main as it has access to all class/objects forming the SharpKey. + WiFi::t_versionList *versionList = new WiFi::t_versionList; + std::istringstream list(SHARPKEY_MODULES); + std::vector modules{std::istream_iterator{list}, std::istream_iterator{}}; + for(int idx = 0; idx < modules.size() && idx < OBJECT_VERSION_LIST_MAX; idx++, versionList->elements=idx) + { + versionList->item[idx] = new WiFi::t_versionItem; + versionList->item[idx]->object = modules[idx]; + if(modules[idx].compare("SharpKey") == 0) + { + versionList->item[idx]->version = version(); + } + else if(modules[idx].compare("HID") == 0) + { + hid = new HID(); + versionList->item[idx]->version = hid->version(); + delete hid; + } + else if(modules[idx].compare("NVS") == 0) + { + versionList->item[idx]->version = nvs.version(); + } + else if(modules[idx].compare("LED") == 0) + { + versionList->item[idx]->version = led->version(); + } + else if(modules[idx].compare("SWITCH") == 0) + { + sw = new SWITCH; + versionList->item[idx]->version = sw->version(); + delete sw; + } + else if(modules[idx].compare("MZ2528") == 0) + { + keyIf = new MZ2528(); + versionList->item[idx]->version = keyIf->version(); + delete keyIf; + } + else if(modules[idx].compare("X1") == 0) + { + keyIf = new X1(); + versionList->item[idx]->version = keyIf->version(); + delete keyIf; + } + else if(modules[idx].compare("X68K") == 0) + { + keyIf = new X68K(); + versionList->item[idx]->version = keyIf->version(); + delete keyIf; + } + else if(modules[idx].compare("MZ5665") == 0) + { + keyIf = new MZ5665(); + versionList->item[idx]->version = keyIf->version(); + delete keyIf; + } + else if(modules[idx].compare("PC9801") == 0) + { + keyIf = new PC9801(); + versionList->item[idx]->version = keyIf->version(); + delete keyIf; + } + else if(modules[idx].compare("Mouse") == 0) + { + keyIf = new Mouse(); + versionList->item[idx]->version = keyIf->version(); + delete keyIf; + } + else if(modules[idx].compare("WiFi") == 0) + { + WiFi *wifiIf = new WiFi(); + versionList->item[idx]->version = wifiIf->version(); + delete wifiIf; + } + else if(modules[idx].compare("KeyInterface") == 0) + { + keyIf = new KeyInterface(); + versionList->item[idx]->version = keyIf->version(); + delete keyIf; + } + else if(modules[idx].compare("FilePack") == 0) + { + // Look on the filesystem for the version file and read the first line contents as the version number. + std::string version = "0.00"; + std::stringstream fqfn; fqfn << LITTLEFS_DEFAULT_PATH << "/" << FILEPACK_VERSION_FILE; + std::ifstream inFile; inFile.open(fqfn.str()); + if(inFile.is_open()) + { + std::getline(inFile, version); + } + inFile.close(); + versionList->item[idx]->version = std::stof(version); + } + else + { + ESP_LOGE(MAINTAG, "Unknown class name in module configuration list:%s", modules[idx].c_str()); + } + } + keyIf = NULL; + + // Create a basic hid object for config persistence and retrieval. + hid = new HID(&nvs); + + // Create basic host interface objects without hardware configuration. This is needed as the WiFi object probes them for configuration parameters and to update + // the parameters. + switch(ifMode) + { + // MZ-2500 or MZ-2800 Host. + case 2500: + case 2800: + { + keyIf = new MZ2528(&nvs, hid, LITTLEFS_DEFAULT_PATH); + break; + } + + // Sharp X1 Host. + case 1: + { + keyIf = new X1(&nvs, hid, LITTLEFS_DEFAULT_PATH); + break; + } + + // Sharp X68000 Host. + case 68000: + { + keyIf = new X68K(&nvs, hid, LITTLEFS_DEFAULT_PATH); + mouseIf = new Mouse(&nvs, hid); + break; + } + + // MZ-5600/MZ-6500 Host. + case 5600: + case 6500: + { + keyIf = new MZ5665(&nvs, hid, LITTLEFS_DEFAULT_PATH); + break; + } + + // PC-9801 + case 9801: + { + keyIf = new PC9801(&nvs, hid, LITTLEFS_DEFAULT_PATH); + break; + } + + // Mouse + case 2: + { + mouseIf = new Mouse(&nvs, hid); + break; + } + + // Unknown host, so just bring up a basic WiFi configuration without interface object configuration. + default: + { + keyIf = new KeyInterface(&nvs, hid); + break; + } + } + + // There are a number of issues with the ESP32 WiFi and code base which you have to work around, some are hardware issues but others will no doubt be + // resolved in later IDF releases. On the MZ-2500/MZ-2800 there is an ESP32 issue regarding WiFi Client mode and ADC2. If the pins are input and wrong + // value the WiFi Client mode wont connect, it goes into a state where it wont connect and errors out - the same kind of error is seen if the voltage/current + // supplied to the ESP32 is out of parameter. + if(keyIf != NULL && (ifMode == 2500 || ifMode == 2800 || ifMode == 68000 || ifMode == 5600 || ifMode == 6500 || ifMode == 9801)) + { + keyIf->reconfigADC2Ports(true); + } + + // Create a new WiFi object. + wifi = new WiFi(keyIf, mouseIf, defaultMode, &nvs, led, LITTLEFS_DEFAULT_PATH, versionList); + + // Pass control, only returns if a reboot is needed. + wifi->run(); +} +#endif + +// Method to determine which host the SharpKey is connected to. This is done by examining the host I/O for tell tale signals +// or inputs wired in a fixed combination. +// +uint32_t getHostType(bool eFuseInvalid, t_EFUSE sharpkeyEfuses) +{ + // Locals. + // + uint32_t RTSNI_MASK = (1 << (CONFIG_HOST_RTSNI - 32)); + uint32_t MPXI_MASK = (1 << CONFIG_HOST_MPXI); + uint32_t KDB0_MASK = (1 << CONFIG_HOST_KDB0); + //uint32_t KDB1_MASK = (1 << CONFIG_HOST_KDB1); + //uint32_t KDB2_MASK = (1 << CONFIG_HOST_KDB2); + //uint32_t KDB3_MASK = (1 << CONFIG_HOST_KDB3); + + // Build selectable target. This software can be built to run on the SharpKey or mz25key interfaces. If a resistor is connected from MPX input to the ESP32 IO12 pin 14 then + // the SharpKey build can be used even though the mz25key only supports one target at a time. If no resistor is connected then you will need to build for a specific target + // as the detection logic will not be able to determine if it is connected to an MZ-2500 or MZ-2800. Use menuconfig to select the target. + #ifdef CONFIG_SHARPKEY + + // Connected host detection. + // + // MZ-2800 - RTSN Goes High and Low, MPX goes High and Low. RTSN, depending upon the machine mode may only oscillate a few times per second, so need to count + // MPX pulses to determine MZ-2800 mode. + // MZ-2500 - RTSN Goes High and Low. MPX goes High and Low. On the falling edge of RTSN sample MPX 50ns in, if it is high then + // select MZ-2500 mode. + // X1 - Output 1010 onto KDO[3:0] and read KDB[3:0] - if match then X1 mode. + // X68000 = KD4 = low, MPX = low, RTSN = high + // Mouse = KD4 = high, MPX = low, RTSN = high + // + // NB: The above tests ASSUME the interface is plugged into the host, only powered by the host and the host is switched on. Development cycles where the interface + // is powered by the UART adapter and/or the host is switched off will not detect the correct host. + // + // First up, sample RTSN and MPX to see if they are alternating. The ratio of RTSN to MPX will yield the type of host. + uint32_t cntRTSN = 0; + uint32_t cntMPX = 0; + uint32_t ifMode = 0; + volatile uint32_t gpioIN, gpioINLast; + gpioIN = gpioINLast = REG_READ(GPIO_IN_REG); + for(uint32_t idx=0; idx < 400; idx++) + { + gpioIN = REG_READ(GPIO_IN_REG); + if((gpioIN & MPXI_MASK) && (gpioIN & MPXI_MASK) != (gpioINLast & MPXI_MASK)) cntMPX++; + gpioINLast = gpioIN; + } + gpioIN = gpioINLast = REG_READ(GPIO_IN1_REG); + for(uint32_t idx=0; idx < 400; idx++) + { + gpioIN = REG_READ(GPIO_IN1_REG); + if((gpioIN & RTSNI_MASK) && (gpioIN & RTSNI_MASK) != (gpioINLast & RTSNI_MASK)) cntRTSN++; + gpioINLast = gpioIN; + } + + // If RTSN and MPX are alternating then the number identifies the host, the MZ-2500 has 1 RTSN per 1 MPX repeating every 1.2uS whereas + // the MZ-2800 has 1 RTSN per ~14 or more MPX cycles albeit there are periods of 1ms where no activity can be seen. + if(cntMPX > 1) + { + if(cntRTSN > 20 && cntMPX > 20 && eFuseInvalid == false && (sharpkeyEfuses.disableRestrictions == true || sharpkeyEfuses.enableMZ2500 == true)) + ifMode = 2500; + + // RTSN may not oscillate in the small capture window depending on run mode, so check MPX and if it is oscillating, at a lower rate than the MZ-2500, select MZ-2800 mode. + else if(cntMPX > 5 && eFuseInvalid == false && (sharpkeyEfuses.disableRestrictions == true || sharpkeyEfuses.enableMZ2800 == true)) + ifMode = 2800; + + if(ifMode > 0) + ESP_LOGW(MAINTAG, "Detected MZ-%d host, counts:RTSN=%d, MPX=%d.", ifMode, cntRTSN, cntMPX); + } else + { + // Check for X1 - this is accomplished by writing a value to KDO and reading it back on KDB. This works because RTSN is tied low on the X1 cable as the X1 protocol is output only. + // Clear all KDO bits - clear state = '0' + GPIO.out_w1tc = (1 << CONFIG_HOST_KDO7) | (1 << CONFIG_HOST_KDO6) | (1 << CONFIG_HOST_KDO5) | (1 << CONFIG_HOST_KDO4) | + (1 << CONFIG_HOST_KDO3) | (1 << CONFIG_HOST_KDO2) | (1 << CONFIG_HOST_KDO1) | (1 << CONFIG_HOST_KDO0); + vTaskDelay(1); + // Set the test pattern. KDO[3:0] = 1010. + GPIO.out_w1ts = (1 << CONFIG_HOST_KDO7) | (1 << CONFIG_HOST_KDO6) | (1 << CONFIG_HOST_KDO5) | (1 << CONFIG_HOST_KDO4) | + (1 << CONFIG_HOST_KDO3) | (0 << CONFIG_HOST_KDO2) | (1 << CONFIG_HOST_KDO1) | (0 << CONFIG_HOST_KDO0); + vTaskDelay(1); + // Now read back KDB. + gpioIN = REG_READ(GPIO_IN_REG); + if((gpioIN & (1 << CONFIG_HOST_KDB3)) && (gpioIN & (1 << CONFIG_HOST_KDB2)) == 0 && (gpioIN & (1 << CONFIG_HOST_KDB1)) && (gpioIN & (1 << CONFIG_HOST_KDB0)) == 0 && + eFuseInvalid == false && (sharpkeyEfuses.disableRestrictions == true || sharpkeyEfuses.enableX1 == true)) + { + ifMode = 1; + } + else + { + // Need to reconfigure the Mouse CTRL pin so we can detect counts. + gpio_config_t io_conf; + io_conf.intr_type = GPIO_INTR_DISABLE; + io_conf.mode = GPIO_MODE_INPUT; + io_conf.pin_bit_mask = (1ULL< 1 && (gpioIN & (1 << CONFIG_HOST_KDI4)) != 0 && (gpioIN & (1 << CONFIG_HOST_MPXI)) == 0 && (REG_READ(GPIO_IN1_REG) & RTSNI_MASK) != 0 && + eFuseInvalid == false && (sharpkeyEfuses.disableRestrictions == true || sharpkeyEfuses.enableMouse == true)) + { + ifMode = 2; + } + } + } + } + //ESP_LOGW(MAINTAG, "RTSN(%d) and MPX(%d) counts.", cntRTSN, cntMPX); + #endif + + // Target build for an MZ-2500 using the mz25key hardware. + #ifdef CONFIG_MZ25KEY_MZ2500 + uint32_t ifMode = 2500; + #endif + + // Target build for an MZ-2500 using the mz25key hardware. + #ifdef CONFIG_MZ25KEY_MZ2800 + uint32_t ifMode = 2800; + #endif +ifMode = 9801; + // Return a value which represents the detected host type. + return(ifMode); +} + +// Method triggered on a WiFi Enable switch event. Set the boot mode and restart to enter WiFi handler and webserver. +void wifiEnableCallback(void) +{ + ESP_LOGW(MAINTAG, "Setting WiFi Enable mode."); + sharpKeyConfig.params.bootMode = 1; +} + +// Method triggered on a WiFi Default Mode Enable switch event. Set the boot mode and restart to enter WiFi handler and webserver. +void wifiDefaultCallback(void) +{ + ESP_LOGW(MAINTAG, "Setting WiFi Default Enable mode."); + sharpKeyConfig.params.bootMode = 2; +} + +// Method triggered on a Clear NVS switch event. Close the NVS and erase its contents setting the SharpKey back to factory default. +void clearNVSCallback(void) +{ + ESP_LOGW(MAINTAG, "Clearing NVS..."); + sharpKeyConfig.params.bootMode = 255; +} + +// Setup method to configure ports, devices and threads prior to application run. +// Configuration: +// PS/2 Keyboard over 2 wire interface +// PS/2 Mouse over 2 wire interface +// Power/Status LED +// Bluetooth, in-built. +// 4 bit input - MZ-2500/2800 Row Number +// 8 bit output - MZ-2500/2800 Scan data +// 1 bit input - RTSN strobe line, low indicating a new Row Number available. +// 1 bit input - KD4, High = Key scan data required, Low = AND of all key matrix rows required. +// +void setup(NVS &nvs) +{ + // Locals. + uint32_t ifMode; + bool eFuseInvalid = false; + KeyInterface *keyIf = NULL; + KeyInterface *mouseIf = NULL; + HID *hid = NULL; + LED *led = NULL; + SWITCH *sw = NULL; + t_EFUSE sharpkeyEfuses; + esp_vfs_littlefs_conf_t lfsConf; + esp_err_t lfsStatus; + gpio_config_t io_conf; + #define SETUPTAG "setup" + + + // Check the efuse and retrieve configured values for later appraisal. + if(checkEFUSE() == false) { eFuseInvalid = true; } + memset((void *)&sharpkeyEfuses, 0x00, sizeof(t_EFUSE)); + if(readEFUSE(sharpkeyEfuses) == true) + { + // If the hw revision, build date and/or serial number havent been set, ie. an unconfigured ESP32 eFuse, obsfucate it. + if(sharpkeyEfuses.hardwareRevision == 0) { sharpkeyEfuses.hardwareRevision = 1300; } + if(sharpkeyEfuses.buildDate[0] == 0) { sharpkeyEfuses.buildDate[0] = 1; sharpkeyEfuses.buildDate[1] = 6; sharpkeyEfuses.buildDate[2] = 22; } + if(sharpkeyEfuses.serialNo == 0) { sharpkeyEfuses.serialNo = (uint16_t)((rand() * 65534) + 1); } + // Bug in Efuse programming workaround. + if(sharpkeyEfuses.buildDate[0] == 31 && sharpkeyEfuses.buildDate[1] == 6) { sharpkeyEfuses.buildDate[0] = 1; } + ESP_LOGW(SETUPTAG, "EFUSE:Hardware Rev=%f, Build Date:%d/%d/%d, Serial Number:%05d %s%s%s%s%s%s%s%s%s", + ((float)sharpkeyEfuses.hardwareRevision)/1000, + sharpkeyEfuses.buildDate[0],sharpkeyEfuses.buildDate[1],sharpkeyEfuses.buildDate[2], + sharpkeyEfuses.serialNo, + sharpkeyEfuses.disableRestrictions == true ? "disableRestrictions" : " ", + sharpkeyEfuses.enableMZ2500 == true ? "enableMZ2500" : " ", + sharpkeyEfuses.enableMZ2800 == true ? "enableMZ2800" : " ", + sharpkeyEfuses.enableX1 == true ? "enableX1" : " ", + sharpkeyEfuses.enableX68000 == true ? "enableX68000" : " ", + sharpkeyEfuses.enableMouse == true ? "enableMouse" : " ", + sharpkeyEfuses.enableBluetooth == true ? "enableBluetooth" : " ", + sharpkeyEfuses.enableMZ5665 == true ? "enableMZ5665" : " ", + sharpkeyEfuses.enablePC9801 == true ? "enablePC9801" : ""); + } else + { + eFuseInvalid = true; + ESP_LOGW(SETUPTAG, "EFUSE not programmed/readable."); + } + #if defined(CONFIG_DISABLE_FEATURE_SECURITY) + sharpkeyEfuses.disableRestrictions = true; + #endif + + // Configure 4 inputs to be the Strobe Row Number which is used to index the virtual key matrix and the strobe data returned. + #if !defined(CONFIG_DEBUG_DISABLE_KDB) + ESP_LOGW(SETUPTAG, "Configuring 4 bit (KDB[3:0] Row Number Inputs."); + io_conf.intr_type = GPIO_INTR_DISABLE; + io_conf.mode = GPIO_MODE_INPUT; + io_conf.pin_bit_mask = (1ULL<setWifiEnEventCallback(&wifiEnableCallback); + sw->setWifiDefEventCallback(&wifiDefaultCallback); + sw->setClearNVSEventCallback(&clearNVSCallback); + + // Initialise the HID and find out what input device is connected. Bluetooth can support two devices, keyboard and mouse, so this + // can be used by selected hosts to provide both keyboard/mouse simulateously. + if(ifMode == 2) + { + // When the detected host is a Mouse port then only one service, a mouse service, can be provided. + hid = new HID(HID::HID_DEVICE_TYPE_MOUSE, &nvs, led, sw); + } else + { + // When the detected host is a Keyboard port then it is possible, if using Bluetooth, to simultaneously offer a Mouse service at the same time, host dependent. + hid = new HID(HID::HID_DEVICE_TYPE_KEYBOARD, &nvs, led, sw); + } + + // Setup host interface according to the detected host. We run the interface regardless of optional extras such as LittleFS/WiFi as + // keyboard protocol conversion is this devices priority. + switch(ifMode) + { + // MZ-2500 Host. + case 2500: + { + keyIf = new MZ2528(ifMode, &nvs, led, hid, LITTLEFS_DEFAULT_PATH); + break; + } + + // MZ-8500 Host. + case 2800: + { + keyIf = new MZ2528(ifMode, &nvs, led, hid, LITTLEFS_DEFAULT_PATH); + break; + } + + // Sharp X1 Host. + case 1: + { + ESP_LOGW(SETUPTAG, "Detected Sharp X1 host."); + keyIf = new X1(ifMode, &nvs, led, hid, LITTLEFS_DEFAULT_PATH); + break; + } + + // Sharp X68000 Host. + case 68000: + { + ESP_LOGW(SETUPTAG, "Detected Sharp X68000 host."); + keyIf = new X68K(ifMode, &nvs, led, hid, LITTLEFS_DEFAULT_PATH); + + // See if Bluetooth is available, if yes then we can offer mouse services simultaneously. + if(hid->isBluetooth()) + { + mouseIf = new Mouse(ifMode, &nvs, led, hid, true); + } + break; + } + + // MZ-5600/MZ-6500 Host. + case 5600: + case 6500: + { + ESP_LOGW(SETUPTAG, "Detected Sharp MZ-5600/MZ-6500 host."); + keyIf = new MZ5665(ifMode, &nvs, led, hid, LITTLEFS_DEFAULT_PATH); + break; + } + + // PC-9801 + case 9801: + { + ESP_LOGW(SETUPTAG, "Detected NEC PC-9801 host."); + keyIf = new PC9801(ifMode, &nvs, led, hid, LITTLEFS_DEFAULT_PATH); + break; + } + + // Mouse + case 2: + { + ESP_LOGW(SETUPTAG, "Detected Mouse."); + mouseIf = new Mouse(ifMode, &nvs, led, hid, false); + break; + } + + // Unknown host or detected interface feature not enabled. Log the details, flash the LED and if WiFi is built-in,enable it and then just wait for RESET or user interaction with the WiFi. + default: + { + ESP_LOGW(SETUPTAG, "Connected host is unknown."); + ESP_LOGW(SETUPTAG, "GPIO:%08x, %08x", REG_READ(GPIO_IN_REG), REG_READ(GPIO_IN_REG) & (1 << CONFIG_HOST_MPXI)); + ESP_LOGW(SETUPTAG, "GPIO1:%08x,%08x", REG_READ(GPIO_IN1_REG), REG_READ(GPIO_IN1_REG) & (1 << CONFIG_HOST_RTSNI)); + + // Initialise a base object so we have access to NVS and the LED, these are needed for the WiFi. + keyIf = new KeyInterface(ifMode, &nvs, led, hid); + break; + } + } + + // Disable the brownout detector, when WiFi starts up it randomly triggers the brownout even though the voltage at the WROOM input is 3.3V. It is posisbly a hardware bug + // as adding larger capacitors doesnt solve it. + // + WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); + } + + // All running, wont reach here if WiFi is enabled. + // + ESP_LOGW(SETUPTAG, "Free Heap (%d)", xPortGetFreeHeapSize()); +} + +// ESP-IDF Application entry point. +// +extern "C" void app_main() +{ + // Locals. + NVS nvs; + + // Setup hardware and start primary control threads, + setup(nvs); + + // Loop waiting on callback events and action accordingly. + while(true) + { + // Change in boot mode requires persisting and reboot. + // + if(sharpKeyConfig.params.bootMode == 1 || sharpKeyConfig.params.bootMode == 2) + { + // Set boot mode to wifi, save and restart. + // + ESP_LOGW(MAINTAG, "Persisting WiFi mode."); + if(nvs.persistData(SHARPKEY_NAME, &sharpKeyConfig, sizeof(struct SharpKeyConfig)) == false) + { + ESP_LOGW(SETUPTAG, "Persisting SharpKey configuration data failed, updates will not persist in future power cycles."); + } else + // Few other updates so make a commit here to ensure data is flushed and written. + if(nvs.commitData() == false) + { + ESP_LOGW(SETUPTAG, "NVS Commit writes operation failed, some previous writes may not persist in future power cycles."); + } + + // Restart and the SharpKey will come up in Wifi mode. + esp_restart(); + } + + // Piggy backing off the bootMode is a flag to indicate NVS flash erase and reboot. + // + if(sharpKeyConfig.params.bootMode == 255) + { + // Close out NVS and erase. + nvs.eraseAll(); + + // Need to reboot as the in-memory config still holds the old settings. + esp_restart(); + } + + // Sleep, not much to be done other than look at event flags. + vTaskDelay(500); + } + + // Lost in space.... this thread is no longer required! +} diff --git a/main/WiFi.cpp b/main/WiFi.cpp deleted file mode 120000 index 7cc1075..0000000 --- a/main/WiFi.cpp +++ /dev/null @@ -1 +0,0 @@ -../../sharpkey/main/WiFi.cpp \ No newline at end of file diff --git a/main/WiFi.cpp b/main/WiFi.cpp new file mode 100644 index 0000000..cf54535 --- /dev/null +++ b/main/WiFi.cpp @@ -0,0 +1,2872 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// Name: WiFi.cpp +// Created: Jan 2022 +// Version: v1.0 +// Author(s): Philip Smart +// Description: The WiFi AP/Client Interface. +// This source file contains the application logic to provide WiFi connectivity in +// order to allow remote query and configuration of the sharpkey interface. +// +// The module provides Access Point (AP) functionality to allow initial connection +// in order to configure local WiFi credentials. +// +// The module provides Client functionality, using the configured credentials, +// to connect to a local Wifi net and present a browser session for querying and +// mapping configuration of the sharpkey interface. +// Credits: +// Copyright: (c) 2022 Philip Smart +// +// History: Jan 2022 - Initial write. +// Mar 2022 - Split out from main.cpp. +// v1.01 May 2022 - Initial release version. +// v1.02 Jun 2022 - Seperated out the WiFi Enable switch and made the WiFi module active +// via a reboot process. This is necessary now that Bluetooth is inbuilt +// as the ESP32 shares an antenna and both operating together electrically +// is difficult but also the IDF stack conflicts as well. +// +// 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 . +///////////////////////////////////////////////////////////////////////////////////////////////////////// + +// This is an optional compile time module, only compile if configured. +#include "sdkconfig.h" +#if defined(CONFIG_IF_WIFI_ENABLED) + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_log.h" +#include "freertos/event_groups.h" +#include "esp_system.h" +#include "esp_wifi.h" +#include "esp_event.h" +#include "esp_ota_ops.h" +#include "lwip/err.h" +#include "lwip/sys.h" +#include "Arduino.h" +#include "driver/gpio.h" +#include "soc/timer_group_struct.h" +#include "soc/timer_group_reg.h" +#include +#include "esp_tls_crypto.h" +#include +#include "esp_littlefs.h" +#include "WiFi.h" + +// FreeRTOS event group to signal when we are connected +static EventGroupHandle_t s_wifi_event_group; + +// Template to convert a given type into a std::string. +// +template std::string to_str(const Type & t, int precision, int base) +{ + // Locals. + // + std::ostringstream os; + + if(precision != 0) + { + os << std::fixed << std::setw(precision) << std::setprecision(precision) << std::setfill('0') << t; + } else + { + if(base == 16) + { + os << "0x" << std::hex << t; + } else + { + os << t; + } + } + return os.str(); +} + +// Method to convert the idf internal partition type to a readable string for output to the browser. +// +std::string WiFi::esp32PartitionType(esp_partition_type_t type) +{ + // Locals. + // + std::string result = "Unknown"; + + switch(static_cast(type)) + { + case ESP_PARTITION_TYPE_APP: + result = "App"; + break; + + case ESP_PARTITION_TYPE_DATA: + result = "Data"; + break; + + default: + result = "n/a"; + break; + } + + // Return the string version of the enum. + return(result); +} + +// Method to convert the idf internal partition subtype to a readable string for output to the browser. +// +std::string WiFi::esp32PartitionSubType(esp_partition_subtype_t subtype) +{ + // Locals. + // + std::string result = "Unknown"; + + switch(static_cast(subtype)) + { + case ESP_PARTITION_SUBTYPE_APP_FACTORY: + result = "Factory"; + break; + + case ESP_PARTITION_SUBTYPE_DATA_PHY: + result = "phy"; + break; + + case ESP_PARTITION_SUBTYPE_DATA_NVS: + result = "nvs"; + break; + + case ESP_PARTITION_SUBTYPE_DATA_COREDUMP: + result = "core"; + break; + + case ESP_PARTITION_SUBTYPE_DATA_NVS_KEYS: + result = "nvs_keys"; + break; + + case ESP_PARTITION_SUBTYPE_DATA_EFUSE_EM: + result = "efuse"; + break; + + case ESP_PARTITION_SUBTYPE_DATA_ESPHTTPD: + result = "httpd"; + break; + + case ESP_PARTITION_SUBTYPE_DATA_FAT: + result = "fat"; + break; + + case ESP_PARTITION_SUBTYPE_DATA_SPIFFS: + result = "spiffs"; + break; + + case ESP_PARTITION_SUBTYPE_APP_OTA_0: + case ESP_PARTITION_SUBTYPE_APP_OTA_1: + case ESP_PARTITION_SUBTYPE_APP_OTA_2: + case ESP_PARTITION_SUBTYPE_APP_OTA_3: + case ESP_PARTITION_SUBTYPE_APP_OTA_4: + case ESP_PARTITION_SUBTYPE_APP_OTA_5: + case ESP_PARTITION_SUBTYPE_APP_OTA_6: + case ESP_PARTITION_SUBTYPE_APP_OTA_7: + case ESP_PARTITION_SUBTYPE_APP_OTA_8: + case ESP_PARTITION_SUBTYPE_APP_OTA_9: + case ESP_PARTITION_SUBTYPE_APP_OTA_10: + case ESP_PARTITION_SUBTYPE_APP_OTA_11: + case ESP_PARTITION_SUBTYPE_APP_OTA_12: + case ESP_PARTITION_SUBTYPE_APP_OTA_13: + case ESP_PARTITION_SUBTYPE_APP_OTA_14: + case ESP_PARTITION_SUBTYPE_APP_OTA_15: + case ESP_PARTITION_SUBTYPE_APP_OTA_MAX: + result = "ota_" + to_str(subtype - ESP_PARTITION_SUBTYPE_APP_OTA_MIN, 0, 10); + break; + + default: + result = to_str(subtype, 0, 10); + break; + } + + // Return the string version of the enum. + return(result); +} + +// Method to return the version number of a given module. +float WiFi::getVersionNumber(std::string name) +{ + // Locals. + // + int idx = 0; + + // Loop through the version list looking for FilePack. + while(idx < wifiCtrl.run.versionList->elements && wifiCtrl.run.versionList->item[idx]->object.compare(name) != 0) + { + idx++; + } + + // Return the version number if found. + return(idx == wifiCtrl.run.versionList->elements ? 0.00 : wifiCtrl.run.versionList->item[idx]->version); +} + +// Method to request KeyMap table headers from the underlying interface and send as a Javascript array. +// +esp_err_t WiFi::sendKeyMapHeaders(httpd_req_t *req) +{ + // Locals. + // + esp_err_t result = ESP_OK; + std::string jsArray; + std::vector headerList; + + // Call the underlying interface to fill the vector with a list of header names. + keyIf->getKeyMapHeaders(headerList); + + // Build up a javascript array and send to the browser direct. + // + jsArray = "["; + for(std::size_t idx = 0; idx < headerList.size(); idx++) + { + jsArray.append("\"").append(headerList[idx]).append("\""); + if(idx == headerList.size()-1) + { + jsArray.append(",\"\"]"); + } else + { + jsArray.append(","); + } + } + + // Send array and return result. + result=httpd_resp_send_chunk(req, jsArray.c_str(), jsArray.size()); + + // Send result, ESP_OK = all successful, anything else a transmission or data error occurred. + return(result); +} + +// Method to request KeyMap table data types from the underlying interface and send as a Javascript array. +esp_err_t WiFi::sendKeyMapTypes(httpd_req_t *req) +{ + // Locals. + // + int startPos; + esp_err_t result = ESP_OK; + std::string jsArray; + std::vector typeList; + + // Call the underlying interface to fill a vector with the type of each keymap column. + keyIf->getKeyMapTypes(typeList); + + // Build up a javascript array containing the column types mapping if needed to an EditTable value. + // + jsArray = "["; + for(std::size_t idx = 0; idx < typeList.size(); idx++) + { + // Strip out the custom tag, (custom_ttp_ where tt = type, ie. rd = Radio, cb = Checkbox, p = polarity, p = positive, = negative) not needed, use as an internal marker to identify custom fields. + if((startPos = typeList[idx].find("custom_")) >= 0) + { + jsArray.append("\"").append(typeList[idx].substr(startPos+11, std::string::npos)).append("\""); + } else + { + jsArray.append("\"").append(typeList[idx]).append("\""); + } + + if(idx == typeList.size()-1) + { + jsArray.append(",\"checkbox\"]"); + } else + { + jsArray.append(","); + } + } + + // Send array and return result. + result=httpd_resp_send_chunk(req, jsArray.c_str(), jsArray.size()); + + // Send result, ESP_OK = all successful, anything else a transmission or data error occurred. + return(result); +} + +// Method to expand the custom type fields in the interface to custom fields in the EditTable configuration code. +// This code is injected into the javascript setup and will invoke a custom popover or select UI. +esp_err_t WiFi::sendKeyMapCustomTypeFields(httpd_req_t *req) +{ + // Locals. + // + int startPos; + esp_err_t result = ESP_OK; + std::string typeStr = ""; + std::vector typeList; + + // Call the underlying interface to fill a vector with the type of each keymap column. + keyIf->getKeyMapTypes(typeList); + + for(std::size_t idx = 0; idx < typeList.size(); idx++) + { + // Custom field? + if((startPos = typeList[idx].find("custom")) >= 0) + { + // Find any duplicate by searching the vector just processed. + bool duplicate = false; + for(std::size_t idx2 = 0; idx2 < idx; idx2++) { if(typeList[idx].compare(typeList[idx2]) == 0) { duplicate = true; break; } } + if(duplicate) continue; + + // Negative or positive value? + bool negate = (typeList[idx].substr(startPos+9, 1)[0] == 'p' ? false : true); + + // Build the custom type definition which is injected into the javascript setup of EditTable. + typeStr += " '" + typeList[idx].substr(startPos+11, std::string::npos) + "' : { \n" + + " html: '', \n"; + + // As some of the interface parameters are negative active, if the 'custom' label is followed by a minus '-' this means the value sent or received needs to be negated. + // This is because the UI works in positive values. If the 'custom' label is followed by an underscore '_' then no data change is made. + typeStr.append(" getValue: function (input) { \n"); + typeStr.append(" var $thisVal = $(input).val(); \n"); + if(negate) + typeStr.append(" return hexConvert($thisVal, true);\n"); + else + typeStr.append(" return hexConvert($thisVal, false);\n"); + typeStr.append(" }, \n"); + typeStr.append(" setValue: function (input, inVal) { \n"); + if(negate) + typeStr.append(" var $thisVal = $(input).attr(\"value\", hexConvert(inVal, true));\n"); + else + typeStr.append(" var $thisVal = $(input).attr(\"value\", hexConvert(inVal, false));\n"); + typeStr.append(" return $thisVal; \n"); + typeStr.append(" } \n"); + typeStr.append(" },\n"); + } + } + + // Send array and return result. + result=httpd_resp_send_chunk(req, typeStr.c_str(), typeStr.size()); + + // Send result, ESP_OK = all successful, anything else a transmission or data error occurred. + return(result); +} + +// Method to request KeyMap table entries, row at a time, from the underlying interface and send as a Javascript array. +// This method could involve large amounts of data which may overflow the heap so data is requested and sent row by row. +esp_err_t WiFi::sendKeyMapData(httpd_req_t *req) +{ + // Locals. + // + esp_err_t result = ESP_OK; + bool startMode = true; + bool firstRow = true; + int row = 0; + std::string jsArray = ""; + std::vector data; + + // Initiate a loop, calling the underlying interface to return data row by row until the end of the keymap data. + while(result == ESP_OK && keyIf->getKeyMapData(data, &row, startMode) == false) + { + // At start, we initialise the data retrieval and also setup the Javascript array designator. + if(startMode == true) + { + startMode = false; + jsArray = "["; + } + if(firstRow == false) + { + jsArray = ","; + } else + { + firstRow = false; + } + jsArray.append("["); + for(std::size_t idx = 0; idx < data.size(); idx++) + { + jsArray.append("\"").append(to_str(data[idx], 0, 16)).append("\""); + if(idx == data.size()-1) + { + jsArray.append(",false]"); + } else + { + jsArray.append(","); + } + } + data.clear(); + + // Send array and return result. + result=httpd_resp_send_chunk(req, jsArray.c_str(), jsArray.size()); + } + + // At the end we need to close the javascript array designator. No way to do this in the loop as the data get method doesnt provide next state information. + if(result == ESP_OK) + { + jsArray = "]"; + // Send array and return result. + result=httpd_resp_send_chunk(req, jsArray.c_str(), jsArray.size()); + } + + // Send result, ESP_OK = all successful, anything else a transmission or data error occurred. + return(result); +} + +// Method for building up the popover modals which are used to enable a user to select values by tick rather than work out a hex value. +// +esp_err_t WiFi::sendKeyMapPopovers(httpd_req_t *req) +{ + // Locals. + // + int startPos; + esp_err_t result = ESP_OK; + std::string jsArray; + std::string jsClass; + std::vector headerList; + std::vector typeList; + std::vector> selectList; + + // Get list of column headers, these are used as the popover title. + keyIf->getKeyMapHeaders(headerList); + + // Get list of types, these are needed to setup a popup for each custom field. + keyIf->getKeyMapTypes(typeList); + + // Loop through the types, process any custom field into a popover modal. + for(std::size_t idx = 0; result == ESP_OK && idx < typeList.size(); idx++) + { + // Custom field? Skip if not custom type. + if((startPos = typeList[idx].find("custom_")) >= 0) + { + // Find any duplicate by searching the vector just processed. + bool duplicate = false; + for(std::size_t idx2 = 0; idx2 < idx; idx2++) { if(typeList[idx].compare(typeList[idx2]) == 0) { duplicate = true; break; } } + if(duplicate) continue; + + jsClass = typeList[idx].substr(startPos+11, std::string::npos); + jsArray = "
\n" + + "
" + headerList[idx] + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n"; + + // Get the select list of values for the current custom type. + keyIf->getKeyMapSelectList(selectList, typeList[idx]); + + // Add in all the check boxes. + if(typeList[idx].find("custom_cb") != std::string::npos) + { + for(auto iter = std::begin(selectList); iter != std::end(selectList); iter++) + { + jsArray.append("
\n") + .append(" \n") + .append("
\n"); + } + } + else if(typeList[idx].find("custom_rd") != std::string::npos) + { + for(auto iter = std::begin(selectList); iter != std::end(selectList); iter++) + { + jsArray.append("
\n") + .append(" \n") + .append("
\n"); + } + } + + // Finish off by closing up the opened DIV blocks. + jsArray.append("
\n") + .append("
\n") + .append("
\n") + .append("
\n") + .append("
\n"); + + // Send array and return result. + result=httpd_resp_send_chunk(req, jsArray.c_str(), jsArray.size()); + + // Free up memory for next iteration. + selectList.clear(); + } + } + + // Send result, ESP_OK = all successful, anything else a transmission or data error occurred. + return(result); +} + +// Method to render the radio select for Mouse Host Scaling. +// +esp_err_t WiFi::sendMouseRadioChoice(httpd_req_t *req, const char *option) +{ + // Locals. + // + int startPos; + esp_err_t result = ESP_OK; + std::string typeStr = ""; + std::string typeHead = ""; + std::string typeBody = ""; + std::vector typeList; + std::vector> selectList; + KeyInterface *activeMouseIf = (mouseIf == NULL ? keyIf : mouseIf); + + // Call the underlying interface to fill a vector with the type of config parameters. + activeMouseIf->getMouseConfigTypes(typeList); + + for(std::size_t idx = 0; idx < typeList.size(); idx++) + { + // Custom field? + if((startPos = typeList[idx].find(option)) >= 0) + { + // Find any duplicate by searching the vector just processed. + bool duplicate = false; + for(std::size_t idx2 = 0; idx2 < idx; idx2++) { if(typeList[idx].compare(typeList[idx2]) == 0) { duplicate = true; break; } } + if(duplicate) continue; + + // Get the select list of values for the current config type. + activeMouseIf->getMouseSelectList(selectList, typeList[idx]); + typeStr.append("
\n"); + for(auto iter = std::begin(selectList); iter != std::end(selectList); iter++) + { + // Skip current value item. + if(iter == std::begin(selectList)) continue; + + typeStr.append(" first).append("\" type=\"radio\" name=\"").append(typeList[idx]).append("\" value=\"").append(to_str(iter->second, 0, 10)).append("\"").append(selectList[0].second == iter->second ? "checked" : "").append("/>") + .append(" \n"); + } + typeStr.append("

"); + } + } + + // Send array and return result. + result=httpd_resp_send_chunk(req, typeStr.c_str(), typeStr.size()); + + // Send result, ESP_OK = all successful, anything else a transmission or data error occurred. + return(result); +} + +// Method to expand variable macros into variable values within a string buffer. The buffer will contain HTML/CSS text prior to despatch to a browser. +// +esp_err_t WiFi::expandVarsAndSend(httpd_req_t *req, std::string str) +{ + // Locals. + // + bool largeMacroDetected = false; + int startPos; + t_kvPair keyValue; + esp_err_t result = ESP_OK; + std::vector pairs; + + // Build up the list of pairs, place holder to value, this is used to expand the given string with latest runtime values. + // Certain macros return a lot of data so they cannot be added into the mapping vector due to RAM constraints, these are handled in-situ. + // + keyValue.name = "%SK_WIFIMODEAP%"; keyValue.value = (wifiCtrl.run.wifiMode == WIFI_CONFIG_AP ? "checked" : ""); pairs.push_back(keyValue); + keyValue.name = "%SK_WIFIMODECLIENT%"; keyValue.value = (wifiCtrl.run.wifiMode == WIFI_CONFIG_CLIENT ? "checked" : ""); pairs.push_back(keyValue); + keyValue.name = "%SK_CLIENTSSID%"; keyValue.value = wifiConfig.clientParams.ssid; pairs.push_back(keyValue); + keyValue.name = "%SK_CLIENTPWD%"; keyValue.value = wifiConfig.clientParams.pwd; pairs.push_back(keyValue); + keyValue.name = "%SK_CLIENTDHCPON%"; keyValue.value = (wifiConfig.clientParams.useDHCP == true ? "checked" : ""); pairs.push_back(keyValue); + keyValue.name = "%SK_CLIENTDHCPOFF%"; keyValue.value = (wifiConfig.clientParams.useDHCP == false ? "checked" : ""); pairs.push_back(keyValue); + keyValue.name = "%SK_CLIENTIP%"; keyValue.value = wifiConfig.clientParams.ip; pairs.push_back(keyValue); + keyValue.name = "%SK_CLIENTNM%"; keyValue.value = wifiConfig.clientParams.netmask; pairs.push_back(keyValue); + keyValue.name = "%SK_CLIENTGW%"; keyValue.value = wifiConfig.clientParams.gateway; pairs.push_back(keyValue); + keyValue.name = "%SK_APSSID%"; keyValue.value = wifiConfig.apParams.ssid; pairs.push_back(keyValue); + keyValue.name = "%SK_APPWD%"; keyValue.value = wifiConfig.apParams.pwd; pairs.push_back(keyValue); + keyValue.name = "%SK_APIP%"; keyValue.value = wifiConfig.apParams.ip; pairs.push_back(keyValue); + keyValue.name = "%SK_APNM%"; keyValue.value = wifiConfig.apParams.netmask; pairs.push_back(keyValue); + keyValue.name = "%SK_APGW%"; keyValue.value = wifiConfig.apParams.gateway; pairs.push_back(keyValue); + keyValue.name = "%SK_CURRENTSSID%"; keyValue.value = (wifiCtrl.run.wifiMode == WIFI_CONFIG_AP ? wifiCtrl.ap.ssid : wifiCtrl.client.ssid); pairs.push_back(keyValue); + keyValue.name = "%SK_CURRENTPWD%"; keyValue.value = (wifiCtrl.run.wifiMode == WIFI_CONFIG_AP ? wifiCtrl.ap.pwd : wifiCtrl.client.pwd); pairs.push_back(keyValue); + keyValue.name = "%SK_CURRENTIP%"; keyValue.value = (wifiCtrl.run.wifiMode == WIFI_CONFIG_AP ? wifiCtrl.ap.ip : wifiCtrl.client.ip); pairs.push_back(keyValue); + keyValue.name = "%SK_CURRENTNM%"; keyValue.value = (wifiCtrl.run.wifiMode == WIFI_CONFIG_AP ? wifiCtrl.ap.netmask : wifiCtrl.client.netmask); pairs.push_back(keyValue); + keyValue.name = "%SK_CURRENTGW%"; keyValue.value = (wifiCtrl.run.wifiMode == WIFI_CONFIG_AP ? wifiCtrl.ap.gateway : wifiCtrl.client.gateway); pairs.push_back(keyValue); + keyValue.name = "%SK_CURRENTIF%"; keyValue.value = keyIf->ifName().append(" "); pairs.push_back(keyValue); + keyValue.name = "%SK_SECONDIF%"; keyValue.value = (mouseIf != NULL ? mouseIf->ifName().append(" ") : ""); pairs.push_back(keyValue); + keyValue.name = "%SK_REBOOTBUTTON%"; keyValue.value = (wifiCtrl.run.rebootButton == true ? "block" : "none"); pairs.push_back(keyValue); + keyValue.name = "%SK_ERRMSG%"; keyValue.value = wifiCtrl.run.errorMsg; pairs.push_back(keyValue); + keyValue.name = "%SK_PRODNAME%"; keyValue.value = (wifiCtrl.run.versionList->elements > 1 ? wifiCtrl.run.versionList->item[0]->object : "Unknown"); pairs.push_back(keyValue); + keyValue.name = "%SK_PRODVERSION%"; keyValue.value = (wifiCtrl.run.versionList->elements > 1 ? to_str(wifiCtrl.run.versionList->item[0]->version, 2, 10) : "Unknown"); pairs.push_back(keyValue); + keyValue.name = "%SK_MODULES%"; if(wifiCtrl.run.versionList->elements > 1) + { + std::ostringstream list; + list << ""; + for(int idx=0, cols=0; idx < wifiCtrl.run.versionList->elements; idx++) + { + // Ignore SharpKey/FilePack, they are part of our version list but not relevant as a module. + if(wifiCtrl.run.versionList->item[idx]->object.compare("SharpKey") == 0 || wifiCtrl.run.versionList->item[idx]->object.compare("FilePack") == 0) + continue; + + if((cols++ % 6) == 0) + { + list << ""; + if(idx < wifiCtrl.run.versionList->elements) { list << ""; } + } + list << " "; + } + list << "
" << wifiCtrl.run.versionList->item[idx]->object << " (v" << to_str(wifiCtrl.run.versionList->item[idx]->version, 2, 10) << ")   
"; + keyValue.value = list.str(); + } else { keyValue.value = "Unknown"; }; pairs.push_back(keyValue); + keyValue.name = "%SK_FILEPACK%"; { + std::ostringstream list; + list << ""; + list << " "; + list << "
FilePack (v" << to_str(getVersionNumber("FilePack"), 2, 10) << ")
"; + keyValue.value = list.str(); + } pairs.push_back(keyValue); + keyValue.name = "%SK_PARTITIONS%"; { + std::ostringstream list; + const esp_partition_t *runPart = esp_ota_get_running_partition(); + esp_partition_iterator_t it; + it = esp_partition_find(ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, NULL); + esp_err_t err; + esp_app_desc_t appDesc; + for (; it != NULL; it = esp_partition_next(it)) { + const esp_partition_t *part = esp_partition_get(it); + err = esp_ota_get_partition_description(part, &appDesc); + list << "" + << "" << part->label << "" + << "" << esp32PartitionType(part->type) << "" + << "" << esp32PartitionSubType(part->subtype) << "" + << "" << to_str(part->address, 0, 16) << "" + << "" << to_str(part->size, 0, 16) << "" + << "" << (err == ESP_OK ? appDesc.version : part->subtype == ESP_PARTITION_SUBTYPE_DATA_SPIFFS ? to_str(getVersionNumber("FilePack"), 2, 10) : "") << "" + << "" << (err == ESP_OK ? appDesc.date : "") << " " + << (err == ESP_OK ? appDesc.time : "") << "" + << "" << (runPart->address == part->address ? "Yes" : "") << "" + << ""; + } + esp_partition_iterator_release(it); + + keyValue.value = list.str(); + } pairs.push_back(keyValue); + keyValue.name = "%SK_KEYMAPHEADER%"; keyValue.value = ""; pairs.push_back(keyValue); + keyValue.name = "%SK_KEYMAPTYPES%"; keyValue.value = ""; pairs.push_back(keyValue); + keyValue.name = "%SK_KEYMAPJSFIELDS%"; keyValue.value = ""; pairs.push_back(keyValue); + keyValue.name = "%SK_KEYMAPDATA%"; keyValue.value = ""; pairs.push_back(keyValue); + keyValue.name = "%SK_KEYMAPPOPOVER%"; keyValue.value = ""; pairs.push_back(keyValue); + keyValue.name = "%SK_MOUSEHOSTSCALING%"; keyValue.value = ""; pairs.push_back(keyValue); + keyValue.name = "%SK_MOUSEPS2SCALING%"; keyValue.value = ""; pairs.push_back(keyValue); + keyValue.name = "%SK_MOUSEPS2RESOLUTION%"; keyValue.value = ""; pairs.push_back(keyValue); + keyValue.name = "%SK_MOUSEPS2SAMPLERATE%"; keyValue.value = ""; pairs.push_back(keyValue); + + // Go through list of place holders to expand and replace. + // + for(auto pair : pairs) + { + // If the varname exists, replace with value. + if((startPos = str.find(pair.name)) >= 0) + { + // Dont expand large data macros yet, they can potentially generate too much data for the limited ESP32 RAM. + if(pair.name.compare("%SK_KEYMAPHEADER%") != 0 && pair.name.compare("%SK_KEYMAPTYPES%") != 0 && pair.name.compare("%SK_KEYMAPDATA%") != 0 && + pair.name.compare("%SK_KEYMAPJSFIELDS%") != 0 && pair.name.compare("%SK_KEYMAPPOPOVER%") != 0 && pair.name.compare("%SK_MOUSEHOSTSCALING%") != 0 && + pair.name.compare("%SK_MOUSEPS2SCALING%") != 0 && pair.name.compare("%SK_MOUSEPS2RESOLUTION%") != 0 && pair.name.compare("%SK_MOUSEPS2SAMPLERATE%") != 0 + ) + { + str.replace(startPos, pair.name.length(), pair.value); + } else + { + largeMacroDetected = true; + } + } + } + // Complete line ready for transmission. + str.append("\n"); + + // Normal macros have been expanded, if no large macros were detected, send the expanded string and return. + // + if(largeMacroDetected == false) + { + // Send as a chunk to the browser. + if(str.size() > 0) + { + result=httpd_resp_send_chunk(req, str.c_str(), str.size()); + } + } else + { + // Repeat the key:value search, locating the large macro, only 1 is allowed per line. + for(auto pair : pairs) + { + // If the macro name exists, process, it will be the large macro, only 1 allowed per line. + if((startPos = str.find(pair.name)) >= 0) + { + // Ease of reading. + int endMacroPos = startPos+pair.name.size(); + int sizeMacro = pair.name.size(); + int sizeEndStr = str.size() - startPos - sizeMacro; + if(sizeEndStr < 0) sizeEndStr = 0; + + // Send the first part of the string upto but excluding the macro. + if(startPos > 0) + result=httpd_resp_send_chunk(req, str.substr(0, startPos).c_str(), startPos); + if(result == ESP_OK) + { + // Keymap Table header. The underlying interface converts its keyboard mapping table into a javascript format list of column headers. + if(pair.name.compare("%SK_KEYMAPHEADER%") == 0) + { + result = sendKeyMapHeaders(req); + } + // Keymap Table types. The underlying interface converts its keyboard mapping table into a javascript format list of column types. + if(pair.name.compare("%SK_KEYMAPTYPES%") == 0) + { + result = sendKeyMapTypes(req); + } + // Keymap field definition for custom fields. + if(pair.name.compare("%SK_KEYMAPJSFIELDS%") == 0) + { + result = sendKeyMapCustomTypeFields(req); + } + // Keymap Table data. This is the big one where the underlying interface converts its keyboard mapping table into a javascript format list of column types. + if(pair.name.compare("%SK_KEYMAPDATA%") == 0) + { + result = sendKeyMapData(req); + } + // Popover boxes, aid data input in a more user friendly manner. + if(pair.name.compare("%SK_KEYMAPPOPOVER%") == 0) + { + result = sendKeyMapPopovers(req); + } + // Mouse host scaling - Radio selection of the scaling required for adaption of the PS/2 mouse data to host. + if(pair.name.compare("%SK_MOUSEHOSTSCALING%") == 0) + { + result = sendMouseRadioChoice(req, "host_scaling"); + } + if(pair.name.compare("%SK_MOUSEPS2SCALING%") == 0) + { + result = sendMouseRadioChoice(req, "mouse_scaling"); + } + if(pair.name.compare("%SK_MOUSEPS2RESOLUTION%") == 0) + { + result = sendMouseRadioChoice(req, "mouse_resolution"); + } + if(pair.name.compare("%SK_MOUSEPS2SAMPLERATE%") == 0) + { + result = sendMouseRadioChoice(req, "mouse_sampling"); + } + + // If the input string had any data after the macro then send it to complete transmission. + if(result == ESP_OK && sizeEndStr > 0) + { + result=httpd_resp_send_chunk(req, str.substr(endMacroPos, std::string::npos).c_str(), sizeEndStr); + } + } + break; + } + } + } + + // Debug, track heap size. + ESP_LOGD(WIFITAG, "After expansion Free Heap (%d)", xPortGetFreeHeapSize()); + + // Return result of expansion/transmission. + return(result); +} + +// A method to open and read a file line by line, expanding any macros therein and sending the result to the open socket connection. +// +esp_err_t WiFi::expandAndSendFile(httpd_req_t *req, const char *basePath, std::string fileName) +{ + // Locals. + // + std::string line; + std::ifstream inFile; + esp_err_t result = ESP_OK; + + // Build the FQFN for reading. + std::string fqfn = basePath; fqfn += "/"; fqfn += fileName; + + // Ensure the content type is set correctly. + setContentTypeFromFileType(req, fileName); + + // Read the file into an input stream, read a line, expand it and r and then store into a string buffer to be returned to caller. + inFile.open(fqfn.c_str()); + while(result == ESP_OK && std::getline(inFile, line)) + { + // Call method to output line after expanding, in-situ, any macros into variable values. + if((result=expandVarsAndSend(req, line)) != ESP_OK) + { + // Abort sending file. + httpd_resp_sendstr_chunk(req, NULL); + + // Respond with 500 Internal Server Error. + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to send file"); + break; + } + } + + // Successful, end the response with a NULL string. + if(result == ESP_OK) + { + result = httpd_resp_send_chunk(req, NULL, 0); + } + + // Tidy up for exit. + inFile.close(); + + // Return result code. + return(result); +} + +bool WiFi::isFileExt(std::string fileName, std::string extension) +{ + // Locals. + // + bool result = false; + + // Match the extension. + if(strcasecmp(fileName.substr(fileName.find_last_of(".")).c_str(), extension.substr(extension.find_last_of(".")).c_str()) == 0) + { + // If this is a multi part extension, match the whole extension too. + // + result = true; + if(extension.find_first_of(".") != extension.find_last_of(".") && strcasecmp(fileName.substr(fileName.find_first_of(".")).c_str(), extension.c_str()) != 0) + { + result = false; + } + } + + // Extension match? + return(result); +} + +// Method to set the HTTP response content type according to file extension. +// +esp_err_t WiFi::setContentTypeFromFileType(httpd_req_t *req, std::string fileName) +{ + if (isFileExt(fileName, ".pdf")) + { + return httpd_resp_set_type(req, "application/pdf"); + } + else if (isFileExt(fileName, ".html")) + { + return httpd_resp_set_type(req, "text/html"); + } + else if (isFileExt(fileName, ".css")) + { + return httpd_resp_set_type(req, "text/css"); + } + else if (isFileExt(fileName, ".js")) + { + return httpd_resp_set_type(req, "application/javascript"); + } + else if (isFileExt(fileName, ".ico")) + { + return httpd_resp_set_type(req, "image/x-icon"); + } + else if (isFileExt(fileName, ".jpeg") || isFileExt(fileName, ".jpg")) + { + return httpd_resp_set_type(req, "image/jpeg"); + } + else if (isFileExt(fileName, ".bin")) + { + return httpd_resp_set_type(req, "application/octet-stream"); + } + else if (isFileExt(fileName, ".bmp")) + { + return httpd_resp_set_type(req, "image/bmp"); + } + else if (isFileExt(fileName, ".gif")) + { + return httpd_resp_set_type(req, "image/gif"); + } + else if (isFileExt(fileName, ".jar")) + { + return httpd_resp_set_type(req, "application/java-archive"); + } + else if (isFileExt(fileName, ".js")) + { + return httpd_resp_set_type(req, "text/javascript"); + } + else if (isFileExt(fileName, ".json")) + { + return httpd_resp_set_type(req, "application/json"); + } + else if (isFileExt(fileName, ".png")) + { + return httpd_resp_set_type(req, "image/png"); + } + else if (isFileExt(fileName, ".php")) + { + return httpd_resp_set_type(req, "application/x-httod-php"); + } + else if (isFileExt(fileName, ".rtf")) + { + return httpd_resp_set_type(req, "application/rtf"); + } + else if (isFileExt(fileName, ".tif") || isFileExt(fileName, ".tiff")) + { + return httpd_resp_set_type(req, "image/tiff"); + } + else if (isFileExt(fileName, ".txt")) + { + return httpd_resp_set_type(req, "text/plain"); + } + else if (isFileExt(fileName, ".xml")) + { + return httpd_resp_set_type(req, "application/xml"); + } + else if (isFileExt(fileName, ".ico")) { + return httpd_resp_set_type(req, "image/x-icon"); + } + // Default to plain text. + return httpd_resp_set_type(req, "text/plain"); +} + +// Locates the path within URI anc copies it into a string. +// +esp_err_t WiFi::getPathFromURI(std::string& destPath, std::string& destFile, const char *basePath, const char *uri) +{ + // Locals. + // + size_t pathlen = strlen(uri); + const char *question = strchr(uri, '?'); + const char *hash = strchr(uri, '#'); + + // Question in the URI - skip. + if(question) + { + pathlen = MIN(pathlen, question - uri); + } + // Hash in the URI - skip. + if(hash) + { + pathlen = MIN(pathlen, hash - uri); + } + + // Construct full path (base + path) + destPath = basePath; + destPath.append(uri, pathlen); + + // Extract filename. + destFile = ""; + destFile.append(uri, 1, pathlen-1); + + // Result, fail if no path extracted. + return(destFile.size() == 0 ? ESP_FAIL : ESP_OK); +} + +// Overloaded method to get the remaining URI from the triggering base path. +// +esp_err_t WiFi::getPathFromURI(std::string& destPath, const char *basePath, const char *uri) +{ + // Locals. + // + esp_err_t result = ESP_OK; + size_t pathlen = strlen(uri); + const char *question = strchr(uri, '?'); + const char *hash = strchr(uri, '#'); + + // Question in the URI - skip. + if(question) + { + pathlen = MIN(pathlen, question - uri); + } + // Hash in the URI - skip. + if(hash) + { + pathlen = MIN(pathlen, hash - uri); + } + + // Extract the path without starting base path and without trailing variables. + destPath = ""; + destPath.append(uri, pathlen); + result = (destPath.find(basePath) != std::string::npos ? ESP_OK : ESP_FAIL); + if(result == ESP_OK) + { + destPath.erase(0, strlen(basePath)); + } + + // Result, fail if no base path was found. + return(result); +} + +// Handler to read and send static files. HTML/CSS are expanded with embedded vars. +// +esp_err_t WiFi::defaultFileHandler(httpd_req_t *req) +{ + // Locals. + // + FILE *fd = NULL; + struct stat file_stat; + char *buf; + int bufLen; + std::string gzipFile = ""; + std::string disposition = ""; + + // Retrieve pointer to object in order to access data. + WiFi* pThis = (WiFi*)req->user_ctx; + + // Get required Header values for processing. + bufLen = httpd_req_get_hdr_value_len(req, "Host") + 1; + if(bufLen > 1) + { + buf = new char[bufLen]; + // Copy null terminated value string into buffer + if(httpd_req_get_hdr_value_str(req, "Host", buf, bufLen) == ESP_OK) + { + // Assign to control structure for later use. + pThis->wifiCtrl.session.host = buf; + } + + // Free up memory to complete. + delete buf; + } + // Get encoding methods. + bufLen = httpd_req_get_hdr_value_len(req, "Accept-Encoding") + 1; + if(bufLen > 1) + { + buf = new char[bufLen]; + // Set flags to indicate allowed encoding methods. + if(httpd_req_get_hdr_value_str(req, "Accept-Encoding", buf, bufLen) == ESP_OK) + { + pThis->wifiCtrl.session.gzip = (strstr(buf, "gzip") != NULL ? true : false); + pThis->wifiCtrl.session.deflate = (strstr(buf, "deflate") != NULL ? true : false); + } + + // Free up memory to complete. + delete buf; + } + + // Get and store the URL query string. + bufLen = httpd_req_get_url_query_len(req) + 1; + if (bufLen > 1) + { + buf = new char[bufLen]; + if (httpd_req_get_url_query_str(req, buf, bufLen) == ESP_OK) + { + pThis->wifiCtrl.session.queryStr = buf; + ESP_LOGI(WIFITAG, "Found URL query => %s", pThis->wifiCtrl.session.queryStr.c_str()); + } + + // Free up memory to complete. + delete buf; + } + + // Look for a filename in the URI and construct the file path returning both. If filename isnt valid, respond with 500 Internal Server Error and exit. + if(pThis->getPathFromURI(pThis->wifiCtrl.session.filePath, pThis->wifiCtrl.session.fileName, pThis->wifiCtrl.run.basePath, req->uri) == ESP_FAIL) + { + // Check for root URL. + if(strlen(req->uri) == 1 && req->uri[0] == '/') + { + pThis->wifiCtrl.session.fileName = "/"; + } else + { + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Filename invalid"); + return(ESP_FAIL); + } + } + + // See if the provided name matches a static handler, such as root, if so execute response directly. + if(pThis->wifiCtrl.session.fileName.compare("/") == 0 || pThis->wifiCtrl.session.fileName.compare("index.html") == 0 || pThis->wifiCtrl.session.fileName.compare("index.htm") == 0) + { + // Open the given file, read and expand macros and send to open connection. + return pThis->expandAndSendFile(req, pThis->wifiCtrl.run.basePath, "index.html"); + } + + // Is this a macro to specify keymap file? Keymap file name changes depending on runmode so adjust filename accordingly. + if(pThis->wifiCtrl.session.fileName.compare("keymap") == 0) + { + pThis->wifiCtrl.session.fileName = std::regex_replace(pThis->wifiCtrl.session.fileName, std::regex("keymap"), pThis->keyIf->getKeyMapFileName()); + pThis->wifiCtrl.session.filePath = std::regex_replace(pThis->wifiCtrl.session.filePath, std::regex("keymap"), pThis->keyIf->getKeyMapFileName()); + disposition = "attachment; filename=" + pThis->keyIf->getKeyMapFileName(); + if(httpd_resp_set_hdr(req, "Content-Disposition", disposition.c_str()) != ESP_OK) + { + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Set content disposition filename failed"); + return(ESP_FAIL); + } + } + + // Get details of the file, throw error 404 - File Not Found on error. + if(stat(pThis->wifiCtrl.session.filePath.c_str(), &file_stat) == -1) + { + // Prepare gzip version, size remains 0 if normal file is found. + if(pThis->wifiCtrl.session.gzip) + gzipFile = pThis->wifiCtrl.session.filePath + ".gz"; + + // Check to see if the file is compressed. Tag on .gz and retry, if success then set encoding content and carry on as normal. + // + if(pThis->wifiCtrl.session.gzip == true && stat(gzipFile.c_str(), &file_stat) == -1) + { + // Respond with 404 Not Found. + httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "File does not exist"); + return(ESP_FAIL); + } + + // Set the content encoding to gzip to comply with specs. + // WARNING: Do not gzip html or library css files as they get parsed and expanded. + if(httpd_resp_set_hdr(req, "Content-Encoding", "gzip") != ESP_OK) + { + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Set content encoding to gzip failed"); + return(ESP_FAIL); + } + } + + // If the file is HTML, JS or CSS then process externally as we need to subsitute embedded variables as required. Note the guard around static evaluation, ie. gzipFile.size. This is to cater for gzipped html, js or css as we cannot + // parse and expand, it is served 'as is'. + if((pThis->isFileExt(pThis->wifiCtrl.session.fileName, ".html") || (pThis->isFileExt(pThis->wifiCtrl.session.fileName, ".js") && !pThis->isFileExt(pThis->wifiCtrl.session.fileName, ".min.js")) || (pThis->isFileExt(pThis->wifiCtrl.session.fileName, ".css") && !pThis->isFileExt(pThis->wifiCtrl.session.fileName, ".min.css"))) && gzipFile.size() == 0) + { + // Open the given file, read and expand macros and send to open connection. + pThis->expandAndSendFile(req, pThis->wifiCtrl.run.basePath, pThis->wifiCtrl.session.fileName); + } else + { + // Try to open the file, we performed a stat so is does exist but perhaps a FAT corruption occurred?. + fd = fopen(gzipFile.size() > 0 ? gzipFile.c_str() : pThis->wifiCtrl.session.filePath.c_str(), "r"); + if(!fd) + { + // Respond with 500 Internal Server Error + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to read existing file"); + return(ESP_FAIL); + } + + ESP_LOGI(WIFITAG, "Sending %sfile : %s (%ld bytes)...", gzipFile.size() > 0 ? "gzip " : " ", pThis->wifiCtrl.session.fileName.c_str(), file_stat.st_size); + pThis->setContentTypeFromFileType(req, pThis->wifiCtrl.session.fileName); + + // Allocate a buffer for chunking the file. The file could be binary, so unlike the HTML/CSS handler, strings cant be used + // thus we read chunks according to our buffer size and send accordingly. + char *chunk = new char[MAX_CHUNK_SIZE]; + size_t chunksize; + do { + // Read file in chunks into the temporary buffer. + chunksize = fread(chunk, 1, MAX_CHUNK_SIZE, fd); + + if (chunksize > 0) + { + // Send the buffer contents as HTTP response chunk. + if(httpd_resp_send_chunk(req, chunk, chunksize) != ESP_OK) + { + + // Release memory and close files, error!! + fclose(fd); + delete chunk; + + // Abort sending file. + httpd_resp_sendstr_chunk(req, NULL); + + // Respond with 500 Internal Server Error. + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to send file"); + return(ESP_FAIL); + } + } + + // Keep looping till the whole file is sent. + } while (chunksize != 0); + + // Release memory to complete. + delete chunk; + + // Close file after sending complete. + fclose(fd); + ESP_LOGI(WIFITAG, "File sending complete"); + } + + // Respond with an empty chunk to signal HTTP response completion. + httpd_resp_send_chunk(req, NULL, 0); + return ESP_OK; +} + +// Handler to send data sets. The handler is triggered on the /data URI and subpaths define the data to be sent. +// +esp_err_t WiFi::defaultDataGETHandler(httpd_req_t *req) +{ + // Locals. + // + char *buf; + int bufLen; + esp_err_t result = ESP_OK; + std::string uriStr; + + // Retrieve pointer to object in order to access data. + WiFi* pThis = (WiFi*)req->user_ctx; + + // Get required Header values for processing. + bufLen = httpd_req_get_hdr_value_len(req, "Host") + 1; + if(bufLen > 1) + { + buf = new char[bufLen]; + // Copy null terminated value string into buffer + if(httpd_req_get_hdr_value_str(req, "Host", buf, bufLen) == ESP_OK) + { + // Assign to control structure for later use. + pThis->wifiCtrl.session.host = buf; + } + + // Free up memory to complete. + delete buf; + } + + // Get and store the URL query string. + bufLen = httpd_req_get_url_query_len(req) + 1; + if (bufLen > 1) + { + buf = new char[bufLen]; + if (httpd_req_get_url_query_str(req, buf, bufLen) == ESP_OK) + { + pThis->wifiCtrl.session.queryStr = buf; + ESP_LOGI(WIFITAG, "Found URL query => %s", pThis->wifiCtrl.session.queryStr.c_str()); + } + + // Free up memory to complete. + delete buf; + } + + // Get the subpath from the URI. + if(pThis->getPathFromURI(uriStr, "/data/", req->uri) == ESP_FAIL) + { + // Respond with 500 Internal Server Error. + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to extract URI"); + return(ESP_FAIL); + } + + // Match URI and execute required data retrieval and return. + if(uriStr.compare("keymap/table/headers") == 0) + { + result = pThis->sendKeyMapHeaders(req); + } else + if(uriStr.compare("keymap/table/types") == 0) + { + result = pThis->sendKeyMapTypes(req); + } else + if(uriStr.compare("keymap/table/data") == 0) + { + result = pThis->sendKeyMapData(req); + } else + { + result = ESP_FAIL; + } + + // Check the result, if the data send failed we need to tidy up and send an error code. + if(result != ESP_OK) + { + // Abort sending file. + httpd_resp_sendstr_chunk(req, NULL); + + // Respond with 500 Internal Server Error. + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to send data."); + } else + { + // Successful, end the response with a NULL string. + result = httpd_resp_send_chunk(req, NULL, 0); + } + + // Return result, successful data send == ESP_OK. + return(result); +} + +// A method, activated on a client side POST using AJAX file upload, to accept incoming data and write it into the next free OTA partition. If successful +// set the active partition to the newly loaded one. +// +IRAM_ATTR esp_err_t WiFi::otaFirmwareUpdatePOSTHandler(httpd_req_t *req) +{ + // Locals. + // + esp_err_t ret = ESP_OK; + std::string resp = ""; + bool checkImageHeader = true; + esp_ota_handle_t updateHandle = 0 ; + esp_app_desc_t newAppInfo; + esp_app_desc_t runningAppInfo; + esp_app_desc_t invalidAppInfo; + const esp_partition_t *lastInvalidApp; + const esp_partition_t *runningApp; + const esp_partition_t *updatePartition; + + // Retrieve pointer to object in order to access data. + //WiFi* pThis = (WiFi*)req->user_ctx; + + // Get current configuration and next available partition in round-robin style. + lastInvalidApp = esp_ota_get_last_invalid_partition(); + runningApp = esp_ota_get_running_partition(); + updatePartition = esp_ota_get_next_update_partition(NULL); + if(runningApp == NULL || updatePartition == NULL) + { + // Respond with 500 Internal Server Error as we couldnt get primary information on running partition or next available partition for update. + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to resolve current/next NVS OTA partition information."); + return(ESP_FAIL); + } + + // Allocate heap space for our receive buffer. + // + char *chunk = new char[MAX_CHUNK_SIZE]; + size_t chunkSize; + + // Use the Content length as the size of the file to be uploaded. + int remaining = req->content_len; + + // Loop while data is still expected. + while(remaining > 0) + { + ESP_LOGI(WIFITAG, "Remaining size : %d", remaining); + + // The file is received in chunks according to the free memory available for a buffer. It has to be at least the size of the firmware application header + // so that it can be read and evaluated in one chunk. + if((chunkSize = httpd_req_recv(req, chunk, MIN(remaining, MAX_CHUNK_SIZE))) <= 0) + { + // Retry if timeout occurred. + if (chunkSize == HTTPD_SOCK_ERR_TIMEOUT) + continue; + + // Release memory, error!! + delete chunk; + + // Respond with 500 Internal Server Error when a file error occurs. + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to receive file"); + return(ESP_FAIL); + } + + // If this is the first read, check the header information and make sure we are not uploading a bad file or one the same as the current image. + // + if(checkImageHeader == true) + { + // The size should be at least that of the application header structures. + // + if (chunkSize > sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t) + sizeof(esp_app_desc_t)) + { + // Check current version being downloaded. + memcpy(&newAppInfo, &chunk[sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t)], sizeof(esp_app_desc_t)); + if(newAppInfo.magic_word != ESP_APP_DESC_MAGIC_WORD) + { + // Release memory, error!! + delete chunk; + + /* Respond with 500 Internal Server Error */ + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "File image is not a valid firmware file."); + return(ESP_FAIL); + } + + // Get information on the running application image. + if(esp_ota_get_partition_description(runningApp, &runningAppInfo) == ESP_OK) + { + ESP_LOGI(WIFITAG, "Running firmware version: %s, new: %s", runningAppInfo.version, newAppInfo.version); + } + + // Compare and make sure we are not trying to upload the same image as the running image. + if(strcmp(newAppInfo.version, runningAppInfo.version) == 0) + { + // Release memory, error!! + delete chunk; + + // Respond with 500 Internal Server Error - Same firmware version as running image being uploaded. + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Firmware version is same as current version."); + return(ESP_FAIL); + } + + // This part is crucial. If a previous image upload failed to boot, it is marked as bad and the previous working image is rebooted. + // Checks need to be made to ensure we are not trying to upload the same bad image as we may not be so lucky next time detecting it as bad! + // + if (esp_ota_get_partition_description(lastInvalidApp, &invalidAppInfo) == ESP_OK) + { + ESP_LOGI(WIFITAG, "Last invalid firmware version: %s", invalidAppInfo.version); + } + + // Check current version with last invalid partition version. On first factory load there wont be a last partition. + if(lastInvalidApp != NULL) + { + if (memcmp(invalidAppInfo.version, newAppInfo.version, sizeof(newAppInfo.version)) == 0) + { + // Release memory, error!! + delete chunk; + + // Respond with 500 Internal Server Error - New image is a previous bad image, cannot upload. + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Firmware version is known as bad, it previously failed to boot."); + return(ESP_FAIL); + } + } + + // Dont repeat the check! + checkImageHeader = false; + + // Start the update procedure, data is written as it arrives from the client in chunks. + ret = esp_ota_begin(updatePartition, OTA_WITH_SEQUENTIAL_WRITES, &updateHandle); + if(ret != ESP_OK) + { + esp_ota_abort(updateHandle); + + // Release memory, error!! + delete chunk; + + // Respond with 500 Internal Server Error - Failed to initialise NVS for writing. + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to initialise NVS OTA partition for writing."); + return(ESP_FAIL); + } + } else + { + esp_ota_abort(updateHandle); + + // Release memory, error!! + delete chunk; + + // Respond with 500 Internal Server Error - Failed to receive sufficient bytes from file to identify header. + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to receive sufficient bytes from file to identify image."); + return(ESP_FAIL); + } + } + // Write out this chunk of data. + ret = esp_ota_write( updateHandle, (const void *)chunk, chunkSize); + if(ret != ESP_OK) + { + esp_ota_abort(updateHandle); + + // Release memory, error!! + delete chunk; + + // Respond with 500 Internal Server Error - Failed to write packet to NVS OTA partition. + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to write data to NVS partition."); + return(ESP_FAIL); + } + + // Keep track of remaining size to download. + remaining -= chunkSize; + } + // Release memory, all done! + delete chunk; + + // Complete the NVS write transaction. + ret = esp_ota_end(updateHandle); + if(ret != ESP_OK) + { + if(ret == ESP_ERR_OTA_VALIDATE_FAILED) + { + // Respond with 500 Internal Server Error - Image validation failed. + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Image validation failed, image is corrupt."); + return(ESP_FAIL); + } else + { + // Respond with 500 Internal Server Error - Image completion failed. + std::string errMsg = "Image completion failed:"; errMsg += esp_err_to_name(ret); + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, errMsg.c_str()); + return(ESP_FAIL); + } + } + + // The image has been successfully downloaded, it is not duplicate or a known dud and validation has passed so set it up as the next boot partition. + ret = esp_ota_set_boot_partition(updatePartition); + if(ret != ESP_OK) + { + std::string errMsg = "Set boot parition to new image failed:"; errMsg += esp_err_to_name(ret); + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, errMsg.c_str()); + return(ESP_FAIL); + } + + // Done, send positive status. + vTaskDelay(500); + httpd_resp_set_status(req, "200 OK"); + httpd_resp_sendstr(req, ""); + + return(ret); +} + +// Method to update the LittleFS filesystem OTA. The image file is pushed via an AJAX xhttp POST, received in chunks and written direct +// to the parition. There is no rollback, any failure will see the filesystem corrupted. On a bigger ESP32 Flash chip, rollback may be +// possible but not with the 4MB standard IC. +IRAM_ATTR esp_err_t WiFi::otaFilepackUpdatePOSTHandler(httpd_req_t *req) +{ + // Locals. + // + esp_err_t ret = ESP_OK; + std::string resp = ""; + bool checkImageHeader = true; + uint32_t partStartAddr; + uint32_t partOffsetAddr = 0; + uint32_t partSize = 0; + + // Retrieve pointer to object in order to access data. + WiFi* pThis = (WiFi*)req->user_ctx; + + // Find the filesystem partition. + esp_partition_iterator_t it; + it = esp_partition_find(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS, NULL); + if(it == NULL) + { + // Respond with 500 Internal Server Error - Couldnt find the filesystem partition. + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Couldnt find a filesysten partition, is this ESP32 configured?"); + return(ESP_FAIL); + } + + // Get the partition information from the iterator. + const esp_partition_t *part = esp_partition_get(it); + + // Setup the addresses of the partition. + partStartAddr = part->address; + partSize = part->size; + + // Check to ensure the file to upload is not larger than the partition. + // + if(req->content_len > partSize) + { + // Respond with 500 Internal Server Error - File is too large. + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Upload file size too large."); + return(ESP_FAIL); + } + + // Erase the partition. + ret = esp_partition_erase_range(part, partOffsetAddr, partSize); + if(ret != ESP_OK) + { + // Respond with 500 Internal Server Error - Partition erase failure. + std::string errMsg = "Failed to erase partition:"; errMsg += esp_err_to_name(ret); errMsg += ", you may need to connect external programmer."; + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, errMsg.c_str()); + return(ESP_FAIL); + } + + // Allocate heap space for our receive buffer. + // + char *chunk = new char[MAX_CHUNK_SIZE]; + size_t chunkSize; + + // Use the Content length as the size of the file to be uploaded. + int remaining = req->content_len; + + // Loop while data is still expected. + while(remaining > 0) + { + ESP_LOGI(WIFITAG, "Remaining size : %d", remaining); + + // The file is received in chunks according to the free memory available for a buffer. It has to be at least the size of the firmware application header + // so that it can be read and evaluated in one chunk. + if((chunkSize = httpd_req_recv(req, chunk, MIN(remaining, MAX_CHUNK_SIZE))) <= 0) + { + // Retry if timeout occurred. + if (chunkSize == HTTPD_SOCK_ERR_TIMEOUT) + continue; + + // Release memory, error!! + delete chunk; + + // Respond with 500 Internal Server Error when a file error occurs. + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to receive file"); + return(ESP_FAIL); + } + + // If this is the first read, check the header information and make sure we are not uploading a bad file or one the same as the current image. + // + if(checkImageHeader == true) + { + // Simple check, look for the base path name in the image. Also the max size was checked earlier, smaller sizes are fine as the littlefs + // filestructure is valid but larger files will overwrite NVS data. + if(strncmp(pThis->wifiCtrl.run.fsPath+1, &chunk[8], strlen(pThis->wifiCtrl.run.fsPath)-1) != 0) + { + // Release memory, error!! + delete chunk; + + /* Respond with 500 Internal Server Error */ + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Filepack image is not a valid file."); + return(ESP_FAIL); + } + + // Dont repeat the check! + checkImageHeader = false; + } + + // Write out the chunk we received, any errors we abort - probably means the user needs to use an external programmer to correct to error. + ret = esp_partition_write_raw(part, partOffsetAddr, (const void *)chunk, chunkSize); + if(ret != ESP_OK) + { + // Release memory, error!! + delete chunk; + + // Respond with 500 Internal Server Error - Write failure. + std::string errMsg = "Write failure: "; errMsg += esp_err_to_name(ret); errMsg += " @ "; errMsg += to_str(partStartAddr+partOffsetAddr, 0, 16); + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, errMsg.c_str()); + return(ESP_FAIL); + } + + // Update counters. + partOffsetAddr += chunkSize; + remaining -= chunkSize; + } + // Release memory, all done! + delete chunk; + + // Done, send positive status. + vTaskDelay(500); + httpd_resp_set_status(req, "200 OK"); + httpd_resp_sendstr(req, ""); + + // Send result. + return(ESP_OK); +} + +// Method to upload a file and store it onto the filesystem tagged according to the current running I/F. +// This method could be merged with the Firmware/Filepack methods but at the moment kept seperate to allow for any +// unseen requirements. +// The data should be passed to the I/F object so that it can verify +// +esp_err_t WiFi::keymapUploadPOSTHandler(httpd_req_t *req) +{ + // Locals. + // + std::string resp = ""; + std::fstream keyFileOut; + + // Retrieve pointer to object in order to access data. + WiFi* pThis = (WiFi*)req->user_ctx; + + // Attempt to open the keymap file for writing. + // + if(pThis->keyIf->createKeyMapFile(keyFileOut) == false) + { + // Respond with 500 Internal Server Error - File creation error. + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to create temporary keymap file"); + return(ESP_FAIL); + } + + // Allocate heap space for our receive buffer. + // + char *chunk = new char[MAX_CHUNK_SIZE]; + size_t chunkSize; + + // Use the Content length as the size of the file to be uploaded. + int remaining = req->content_len; + + // Loop while data is still expected. + while(remaining > 0) + { + ESP_LOGI(WIFITAG, "Remaining size : %d", remaining); + + // The file is received in chunks according to the free memory available for a buffer. + if((chunkSize = httpd_req_recv(req, chunk, MIN(remaining, MAX_CHUNK_SIZE))) <= 0) + { + // Retry if timeout occurred. + if (chunkSize == HTTPD_SOCK_ERR_TIMEOUT) + continue; + + // Release memory, error!! + delete chunk; + + // Respond with 500 Internal Server Error when a reception error occurs. + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to receive file"); + return(ESP_FAIL); + } + + // Store the data chunk into the keymap file. + if(pThis->keyIf->storeDataToKeyMapFile(keyFileOut, chunk, chunkSize) == false) + { + // Cleanup the mess! + pThis->keyIf->closeAndCommitKeyMapFile(keyFileOut, true); + + // Release memory, error!! + delete chunk; + + // Respond with 500 Internal Server Error when a file error occurs. + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to write data into file"); + return(ESP_FAIL); + } + + // Update counters. + remaining -= chunkSize; + } + // Release memory, all done! + delete chunk; + + // Close and commit the file. + if(pThis->keyIf->closeAndCommitKeyMapFile(keyFileOut, false) == false) + { + // Respond with 500 Internal Server Error if the commit fails. + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to write data into file"); + return(ESP_FAIL); + } + + // Done, send positive status. + vTaskDelay(500); + httpd_resp_set_status(req, "200 OK"); + httpd_resp_sendstr(req, ""); + + // Send result. + return(ESP_OK); +} + + +// Method to store the keymap table data. The POST data is captured in chunks and sent to the underlying interface method +// for parsing and extraction. +esp_err_t WiFi::keymapTablePOSTHandler(httpd_req_t *req) +{ + // Locals. + // + int startPos; + int endPos; + int commaPos; + std::string resp = ""; + std::string jsonData = ""; + std::string jsonArray = ""; + std::fstream keyFileOut; + std::vector dataArray; + + // Retrieve pointer to object in order to access data. + WiFi* pThis = (WiFi*)req->user_ctx; + + // Attempt to open the keymap file for writing. + // + if(pThis->keyIf->createKeyMapFile(keyFileOut) == false) + { + // Respond with 500 Internal Server Error - File creation error. + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to create temporary keymap file"); + return(ESP_FAIL); + } + + // Allocate heap space for our receive buffer. + // + char *chunk = new char[MAX_CHUNK_SIZE]; + size_t chunkSize; + + // Use the Content length as the size of the JSON array to be uploaded. + int remaining = req->content_len; + + // Loop while data is still expected. + while(remaining > 0) + { + ESP_LOGI(WIFITAG, "Remaining size : %d", remaining); + + // The file is received in chunks according to the free memory available for a buffer. + if((chunkSize = httpd_req_recv(req, chunk, MIN(remaining, MAX_CHUNK_SIZE))) <= 0) + { + // Retry if timeout occurred. + if (chunkSize == HTTPD_SOCK_ERR_TIMEOUT) + continue; + + // Release memory, error!! + delete chunk; + + // Respond with 500 Internal Server Error when a reception error occurs. + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to receive file"); + return(ESP_FAIL); + } + + // Build up the JSON Data array and process piecemeal to keep overall memory usage low. + jsonData.append(chunk, chunkSize); + do { + startPos = jsonData.find("[["); + if(startPos != std::string::npos) { startPos++; } else { startPos = jsonData.find("["); } + endPos = jsonData.find("]"); + + if(startPos != std::string::npos && endPos != std::string::npos) + { + // Extract the array and parse into bytes. + jsonArray = jsonData.substr(startPos, endPos+1); + do { + commaPos = jsonArray.find("\""); + if(commaPos != std::string::npos) + { + jsonArray.erase(0, commaPos+1); + commaPos = jsonArray.find("\""); + if(commaPos != std::string::npos) + { + std::istringstream iss(jsonArray.substr(0, commaPos)); + uint32_t word; + iss >> std::hex >> word; + dataArray.push_back(word); + } + commaPos = jsonArray.find(","); + if(commaPos != std::string::npos) + { + jsonArray.erase(0, commaPos+1); + } + } + } while(jsonArray.size() > 0 && commaPos != std::string::npos); + + // Remove the array and the comma (or ending ]) seperator. + jsonData.erase(0, endPos + 2); + } + } while(startPos != std::string::npos && endPos != std::string::npos); + + // Store the data chunk into the keymap file. + if(pThis->keyIf->storeDataToKeyMapFile(keyFileOut, dataArray) == false) + { + // Cleanup the mess! + pThis->keyIf->closeAndCommitKeyMapFile(keyFileOut, true); + + // Release memory, error!! + delete chunk; + + // Respond with 500 Internal Server Error when a file error occurs. + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to write data into file"); + return(ESP_FAIL); + } + + // Free up vector for next loop. + dataArray.clear(); + + // Update counters. + remaining -= chunkSize; + } + // Release memory, all done! + delete chunk; + + // Close and commit the file. + if(pThis->keyIf->closeAndCommitKeyMapFile(keyFileOut, false) == false) + { + // Respond with 500 Internal Server Error if the commit fails. + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to write data into file"); + return(ESP_FAIL); + } + + // Done, send positive status. + vTaskDelay(500); + httpd_resp_set_status(req, "200 OK"); + httpd_resp_sendstr(req, ""); + + // Send result. + return(ESP_OK); +} + +// Method to extract the Key/Value pairs from a received POST request. +// +esp_err_t WiFi::getPOSTData(httpd_req_t *req, std::vector *pairs) +{ + // Locals. + // + char buf[100]; + int ret; + int rcvBytes = req->content_len; + std::string post; + t_kvPair keyValue; + + // Loop retrieving the POST in chunks and assemble into a string. + while(rcvBytes > 0) + { + // Read data for the request. + if((ret = httpd_req_recv(req, buf, MIN(rcvBytes, sizeof(buf)-1))) <= 0) + { + if(ret == HTTPD_SOCK_ERR_TIMEOUT) + { + /* Retry receiving if timeout occurred */ + continue; + } + return(ESP_FAIL); + } + buf[ret] = '\0'; + + // Add to our string for tokenising. + post = post + buf; + + // Update bytes still to arrive. + rcvBytes -= ret; + } + + // Split the POST into key pairs. + std::vector keys = this->split(post, "&"); + for(auto key : keys) + { + size_t pos = key.find('='); + keyValue.name = key.substr(0, pos); + keyValue.value = key.substr(pos + 1); + pairs->push_back(keyValue); + } + + // Successful extraction of the Key/Value pairs from the POST. + return(ESP_OK); +} + +// Method to process POST data specifically for the WiFi configuration. The key:value pairs are parsed, data extracted +// and validated. Any errors are sent back to the UI/Browser. +esp_err_t WiFi::wifiDataPOSTHandler(httpd_req_t *req, std::vector pairs, std::string& resp) +{ + // Locals. + // + bool dataError = false; + + // Retrieve pointer to object in order to access data. + WiFi* pThis = (WiFi*)req->user_ctx; + + // Loop through all the URI key pairs, updating configuration values as necessary. + for(auto pair : pairs) + { + //printf("%s->%s\n", pair.name.c_str(), pair.value.c_str()); + if(pair.name.compare("wifiMode") == 0) + { + if(pair.value.compare("ap") == 0) + { + pThis->wifiConfig.params.wifiMode = WIFI_CONFIG_AP; + dataError = false; + } else + { + pThis->wifiConfig.params.wifiMode = WIFI_CONFIG_CLIENT; + dataError = false; + } + } + if(pair.name.compare("clientSSID") == 0) + { + strncpy(pThis->wifiConfig.clientParams.ssid, pair.value.c_str(), MAX_WIFI_SSID_LEN); + } + if(pair.name.compare("apSSID") == 0) + { + strncpy(pThis->wifiConfig.apParams.ssid, pair.value.c_str(), MAX_WIFI_SSID_LEN); + } + if(pair.name.compare("clientPWD") == 0) + { + strncpy(pThis->wifiConfig.clientParams.pwd, pair.value.c_str(), MAX_WIFI_PWD_LEN); + } + if(pair.name.compare("apPWD") == 0) + { + strncpy(pThis->wifiConfig.apParams.pwd, pair.value.c_str(), MAX_WIFI_PWD_LEN); + } + if(pair.name.compare("dhcpMode") == 0) + { + if(pair.value.compare("on") == 0) + { + pThis->wifiConfig.clientParams.useDHCP = true; + } else + { + pThis->wifiConfig.clientParams.useDHCP = false; + } + } + if(pair.name.compare("clientIP") == 0) + { + strncpy(pThis->wifiConfig.clientParams.ip, pair.value.c_str(), MAX_WIFI_IP_LEN); + } + if(pair.name.compare("apIP") == 0) + { + strncpy(pThis->wifiConfig.apParams.ip, pair.value.c_str(), MAX_WIFI_IP_LEN); + } + if(pair.name.compare("clientNETMASK") == 0) + { + strncpy(pThis->wifiConfig.clientParams.netmask, pair.value.c_str(), MAX_WIFI_NETMASK_LEN); + } + if(pair.name.compare("apNETMASK") == 0) + { + strncpy(pThis->wifiConfig.apParams.netmask, pair.value.c_str(), MAX_WIFI_NETMASK_LEN); + } + if(pair.name.compare("clientGATEWAY") == 0) + { + // Gateway isnt mandatory in client mode. + if(pair.value.size() > 0 && pThis->wifiConfig.params.wifiMode == WIFI_CONFIG_CLIENT) + { + strncpy(pThis->wifiConfig.clientParams.gateway, pair.value.c_str(), MAX_WIFI_GATEWAY_LEN); + } + } + if(pair.name.compare("apGATEWAY") == 0) + { + // Access point mode, if no gateway is given, assign the IP address as the gateway. + strncpy(pThis->wifiConfig.apParams.gateway, pThis->wifiConfig.apParams.ip, MAX_WIFI_GATEWAY_LEN+1); + } + } + + // Validate the data if no error was raised for individual fields. + if(dataError == false) + { + if(pThis->wifiConfig.params.wifiMode == WIFI_CONFIG_AP) + { + if(strlen(pThis->wifiConfig.apParams.ssid) == 0) + { + resp = resp + (resp.size() > 0 ? "," : ""); + resp = resp + "SSID not given!"; + dataError = true; + } + if(strlen(pThis->wifiConfig.apParams.pwd) == 0) + { + resp = resp + (resp.size() > 0 ? "," : ""); + resp = resp + "Password not given!"; + dataError = true; + } + if(!pThis->validateIP(pThis->wifiConfig.apParams.ip)) + { + resp = resp + (resp.size() > 0 ? "," : ""); + resp = resp + "Illegal AP IP address(" + pThis->wifiConfig.apParams.ip + ")"; + dataError = true; + } + if(!pThis->validateIP(pThis->wifiConfig.apParams.netmask)) + { + resp = resp + (resp.size() > 0 ? "," : ""); + resp = resp + "Illegal AP Netmask address(" + pThis->wifiConfig.apParams.netmask + ")"; + dataError = true; + } + // Gateway isnt mandatory, but if filled in, validate it. + if(strlen(pThis->wifiConfig.apParams.gateway) == 0 || !pThis->validateIP(pThis->wifiConfig.apParams.gateway)) + { + resp = resp + (resp.size() > 0 ? "," : ""); + resp = resp + "Illegal AP Gateway address(" + pThis->wifiConfig.clientParams.gateway + ")"; + dataError = true; + } + } + // Only verify client parameters when active. + else if(pThis->wifiConfig.params.wifiMode == WIFI_CONFIG_CLIENT) + { + if(pThis->wifiConfig.clientParams.useDHCP == false) + { + if(strlen(pThis->wifiConfig.clientParams.ssid) == 0) + { + resp = resp + (resp.size() > 0 ? "," : ""); + resp = resp + "SSID not given!"; + dataError = true; + } + if(strlen(pThis->wifiConfig.clientParams.pwd) == 0) + { + resp = resp + (resp.size() > 0 ? "," : ""); + resp = resp + "Password not given!"; + dataError = true; + } + if(!pThis->validateIP(pThis->wifiConfig.clientParams.ip)) + { + resp = resp + (resp.size() > 0 ? "," : ""); + resp = resp + "Illegal IP address(" + pThis->wifiConfig.clientParams.ip + ")"; + dataError = true; + } + if(!pThis->validateIP(pThis->wifiConfig.clientParams.netmask)) + { + resp = resp + (resp.size() > 0 ? "," : ""); + resp = resp + "Illegal Netmask address(" + pThis->wifiConfig.clientParams.netmask + ")"; + dataError = true; + } + // Gateway isnt mandatory, but if filled in, validate it. + if(strlen(pThis->wifiConfig.clientParams.gateway) > 0 && !pThis->validateIP(pThis->wifiConfig.clientParams.gateway)) + { + resp = resp + (resp.size() > 0 ? "," : ""); + resp = resp + "Illegal Gateway address(" + pThis->wifiConfig.clientParams.gateway + ")"; + dataError = true; + } + } + } else + { + resp = resp + (resp.size() > 0 ? "," : ""); + resp = resp + "Unknown WiFi Mode (" + to_str(pThis->wifiConfig.params.wifiMode, 0, 10) + "), internal coding error, please contact support."; + dataError = true; + } + } + + // No errors, save wifi configuration. + if(dataError == false) + { + // Mark data as valid. + pThis->wifiConfig.clientParams.valid = true; + if(pThis->nvs->persistData(pThis->wifiCtrl.run.thisClass.c_str(), &pThis->wifiConfig, sizeof(t_wifiConfig)) == false) + { + ESP_LOGI(WIFITAG, "Persisting SharpKey(%s) configuration data failed, updates will not persist in future power cycles.", pThis->wifiCtrl.run.thisClass.c_str()); + pThis->led->setLEDMode(LED::LED_MODE_BLINK_ONESHOT, LED::LED_DUTY_CYCLE_10, 200, 1000L, 0L); + } else + // Few other updates so make a commit here to ensure data is flushed and written. + if(pThis->nvs->commitData() == false) + { + ESP_LOGI(WIFITAG, "NVS Commit writes operation failed, some previous writes may not persist in future power cycles."); + pThis->led->setLEDMode(LED::LED_MODE_BLINK_ONESHOT, LED::LED_DUTY_CYCLE_10, 200, 500L, 0L); + } + } + + return(dataError == false ? ESP_OK : ESP_FAIL); +} + +// Method to process POST data specifically for the Mouse interface. The key:value pairs are sent to the Mouse +// interface for parsing and storing, any errors are sent back to the UI/Browser. +esp_err_t WiFi::mouseDataPOSTHandler(httpd_req_t *req, std::vector pairs, std::string& resp) +{ + // Locals. + // + bool dataError = false; + KeyInterface *activeMouseIf = (mouseIf == NULL ? keyIf : mouseIf); + + // Run through pairs and send to the mouse interface to interpret. + resp = ""; + for(auto pair : pairs) + { + // Call the Mouse configuration handler tp validate and set the parameter. + dataError = activeMouseIf->setMouseConfigValue(pair.name, pair.value); + if(dataError) + { + resp.append("Variable:" + pair.name + " has an invalid value:" + pair.value); + dataError = false; + } + } + + // Update success status. + if(resp.size() > 0) dataError = true; + + // Persist the values if no errors occurred. + if(dataError == false) + { + dataError = activeMouseIf->persistConfig() ? false : true; + if(dataError) + { + resp.append("Save config to NVS RAM failed, retry, if 2nd attempt fails, power cycle the interface."); + } + } + + return(dataError == false ? ESP_OK : ESP_FAIL); +} + +// /data POST handler. Process the request and call service as required. +// +esp_err_t WiFi::defaultDataPOSTHandler(httpd_req_t *req) +{ + // Locals. + // + std::vector pairs; + esp_err_t ret = ESP_OK; + std::string resp = ""; + std::string uriStr; + + // Retrieve pointer to object in order to access data. + WiFi* pThis = (WiFi*)req->user_ctx; + + // Get the subpath from the URI. + if(pThis->getPathFromURI(uriStr, "/data/", req->uri) == ESP_FAIL) + { + // Respond with 500 Internal Server Error. + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to extract URI"); + return(ESP_FAIL); + } + + // Process the POST into key/value pairs. + ret = pThis->getPOSTData(req, &pairs); + if(ret == ESP_OK) + { + if(uriStr.compare("wifi") == 0) + { + if((ret = pThis->wifiDataPOSTHandler(req, pairs, resp)) == ESP_OK) + { + // Success so add reboot button. + pThis->wifiCtrl.run.rebootButton = true; + resp = "Data values accepted. Press 'Reboot' to initiate network connection with the new configuration."; + } + } + if(uriStr.compare("mouse") == 0) + { + if((ret = pThis->mouseDataPOSTHandler(req, pairs, resp)) == ESP_OK) + { + // Success so indicate all ok. + pThis->wifiCtrl.run.rebootButton = true; + resp = "Data values accepted. Press 'Reboot' to restart interface with new values."; + } + } + } else + { + resp = "

No values in POST, check browser!

"; + } + + // Add in an error message if one has been generated. + pThis->wifiCtrl.run.errorMsg = "wifiCtrl.run.errorMsg += (ret == ESP_OK ? "green" : "red"); + pThis->wifiCtrl.run.errorMsg += "\">" + resp + ""; + + // Send message directly, it will appear as an error or success message. + httpd_resp_send_chunk(req, pThis->wifiCtrl.run.errorMsg.c_str(), pThis->wifiCtrl.run.errorMsg.size()+1); + + // End response + httpd_resp_send_chunk(req, NULL, 0); + return(ret); +} + +// /reboot POST handler. +// Simple handler, send a message indicating reboot taking place with a reload URL statement. +// +esp_err_t WiFi::defaultRebootHandler(httpd_req_t *req) +{ + // Locals. + // + esp_err_t ret = ESP_OK; + std::string resp = ""; + + // Retrieve pointer to object in order to access data. + WiFi* pThis = (WiFi*)req->user_ctx; + + // Build a response message. + if(pThis->wifiConfig.clientParams.useDHCP == false) + { + resp = " wifiConfig.clientParams.ip; + resp += "/\" /> Rebooting... Please wait."; + } else + { + resp = " Rebooting...

Please look in your router admin panel for the assigned IP address and enter http://<router assigned ip address> into browser to continue.

"; + } + + // Send the response and wait a while, then request reboot. + httpd_resp_send(req, resp.c_str(), resp.size()+1); + vTaskDelay(100); + pThis->wifiCtrl.run.reboot = true; + + // Get out, a reboot will occur very soon. + return(ret); +} + +// Method to start the basic HTTP webserver. +// +bool WiFi::startWebserver(void) +{ + // Locals. + // + bool result = false; + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + + // Tweak default settings. + config.stack_size = 10240; + config.uri_match_fn = httpd_uri_match_wildcard; + config.lru_purge_enable = true; + config.max_uri_handlers = 12; + + // Setup the required paths and descriptors then register them with the server. + const httpd_uri_t dataPOST = { + .uri = "/data", + .method = HTTP_POST, + .handler = defaultDataPOSTHandler, + .user_ctx = this + }; + const httpd_uri_t dataSubPOST = { + .uri = "/data/*", + .method = HTTP_POST, + .handler = defaultDataPOSTHandler, + .user_ctx = this + }; + const httpd_uri_t dataGET = { + .uri = "/data", + .method = HTTP_GET, + .handler = defaultDataGETHandler, + .user_ctx = this + }; + const httpd_uri_t dataSubGET = { + .uri = "/data/*", + .method = HTTP_GET, + .handler = defaultDataGETHandler, + .user_ctx = this + }; + const httpd_uri_t keymapTablePOST = { + .uri = "/keymap/table", + .method = HTTP_POST, + .handler = keymapTablePOSTHandler, + .user_ctx = this + }; + const httpd_uri_t keymap = { + .uri = "/keymap", + .method = HTTP_POST, + .handler = keymapUploadPOSTHandler, + .user_ctx = this + }; + const httpd_uri_t otafw = { + .uri = "/ota/firmware", + .method = HTTP_POST, + .handler = otaFirmwareUpdatePOSTHandler, + .user_ctx = this + }; + const httpd_uri_t otafp = { + .uri = "/ota/filepack", + .method = HTTP_POST, + .handler = otaFilepackUpdatePOSTHandler, + .user_ctx = this + }; + const httpd_uri_t rebootPOST = { + .uri = "/reboot", + .method = HTTP_POST, + .handler = defaultRebootHandler, + .user_ctx = this + }; + const httpd_uri_t rebootGET = { + .uri = "/reboot", + .method = HTTP_GET, + .handler = defaultRebootHandler, + .user_ctx = this + }; + const httpd_uri_t root = { + .uri = "/", + .method = HTTP_GET, + .handler = defaultFileHandler, + .user_ctx = this + }; + // Catch all, assume files if no handler setup. + const httpd_uri_t files = { + .uri = "/*", + .method = HTTP_GET, + .handler = defaultFileHandler, + .user_ctx = this + }; + + // Store the file system basepath on t + strlcpy(this->wifiCtrl.run.basePath, this->wifiCtrl.run.fsPath, sizeof(this->wifiCtrl.run.basePath)); + + // Start the web server. + ESP_LOGI(WIFITAG, "Starting server on port: '%d'", config.server_port); + + if (httpd_start(&wifiCtrl.run.server, &config) == ESP_OK) + { + // Set URI handlers + ESP_LOGI(WIFITAG, "Registering URI handlers"); + + // Root directory handler. Equivalent to index.html/index.htm. The method, based on the current mode (AP/Client) decides on which + // file to serve. + httpd_register_uri_handler(wifiCtrl.run.server, &root); + + // POST handlers. + httpd_register_uri_handler(wifiCtrl.run.server, &dataSubPOST); + httpd_register_uri_handler(wifiCtrl.run.server, &dataPOST); + httpd_register_uri_handler(wifiCtrl.run.server, &dataSubGET); + httpd_register_uri_handler(wifiCtrl.run.server, &dataGET); + httpd_register_uri_handler(wifiCtrl.run.server, &keymapTablePOST); + httpd_register_uri_handler(wifiCtrl.run.server, &keymap); + httpd_register_uri_handler(wifiCtrl.run.server, &otafw); + httpd_register_uri_handler(wifiCtrl.run.server, &otafp); + httpd_register_uri_handler(wifiCtrl.run.server, &rebootPOST); + httpd_register_uri_handler(wifiCtrl.run.server, &rebootGET); + + // If no URL matches then default to serving files. + httpd_register_uri_handler(wifiCtrl.run.server, &files); + result = true; + } + + // Return result of startup. + return(result); +} + +// Method to stop the basic HTTP webserver. +// +void WiFi::stopWebserver(void) +{ + // Stop the web server and set the handle to NULL to indicate state. + httpd_stop(wifiCtrl.run.server); + wifiCtrl.run.server = NULL; + return; +} + +// Event handler for Client mode Wifi event callback. +// +IRAM_ATTR void WiFi::wifiClientHandler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) +{ + // Locals. + // + + // Retrieve pointer to object in order to access data. + WiFi* pThis = (WiFi*)arg; + + if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) + { + esp_wifi_connect(); + } + else if(event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) + { + if(pThis->wifiCtrl.client.clientRetryCnt < CONFIG_IF_WIFI_MAX_RETRIES) + { + esp_wifi_connect(); + pThis->wifiCtrl.client.clientRetryCnt++; + ESP_LOGI(WIFITAG, "retry to connect to the AP"); + } else + { + xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT); + } + ESP_LOGI(WIFITAG,"connect to the AP fail"); + } + else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) + { + ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data; + + // Copy details into control structure for ease of use in rendering pages. + strncpy(pThis->wifiCtrl.ap.ssid, pThis->wifiConfig.clientParams.ssid, MAX_WIFI_SSID_LEN+1); + strncpy(pThis->wifiCtrl.ap.pwd, pThis->wifiConfig.clientParams.pwd, MAX_WIFI_PWD_LEN+1); + sprintf(pThis->wifiCtrl.client.ip, IPSTR, IP2STR(&event->ip_info.ip)); + sprintf(pThis->wifiCtrl.client.netmask, IPSTR, IP2STR(&event->ip_info.netmask)); + sprintf(pThis->wifiCtrl.client.gateway, IPSTR, IP2STR(&event->ip_info.gw)); + pThis->wifiCtrl.client.connected = true; + pThis->wifiCtrl.client.clientRetryCnt = 0; + + ESP_LOGI(WIFITAG, "got ip:" IPSTR " Netmask:" IPSTR " Gateway:" IPSTR, IP2STR(&event->ip_info.ip), IP2STR(&event->ip_info.netmask), IP2STR(&event->ip_info.gw)); + xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT); + + // Start the webserver if it hasnt been configured. + if(pThis->wifiCtrl.run.server == NULL) + { + pThis->startWebserver(); + } + } + return; +} + +// Event handler for Access Point mode Wifi event callback. +// +IRAM_ATTR void WiFi::wifiAPHandler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) +{ + // Locals. + // + + // Retrieve pointer to object in order to access data. + WiFi* pThis = (WiFi*)arg; + + if (event_id == WIFI_EVENT_AP_STACONNECTED) + { + wifi_event_ap_staconnected_t* event = (wifi_event_ap_staconnected_t*) event_data; + ESP_LOGI(WIFITAG, "station " MACSTR " join, AID=%d", MAC2STR(event->mac), event->aid); + + // Start the webserver if it hasnt been configured. + if(pThis->wifiCtrl.run.server == NULL) + { + pThis->startWebserver(); + } + } + else if (event_id == WIFI_EVENT_AP_STADISCONNECTED) + { + wifi_event_ap_stadisconnected_t* event = (wifi_event_ap_stadisconnected_t*) event_data; + ESP_LOGI(WIFITAG, "station " MACSTR " leave, AID=%d", MAC2STR(event->mac), event->aid); + } + return; +} + +// Method to initialise the interface as a client to a known network, the SSID +// and password have already been setup. +// +bool WiFi::setupWifiClient(void) +{ + // Locals. + // + wifi_init_config_t wifiInitConfig = WIFI_INIT_CONFIG_DEFAULT(); + esp_netif_t *netConfig; + esp_netif_ip_info_t ipInfo; + esp_event_handler_instance_t instID; + esp_event_handler_instance_t instIP; + EventBits_t bits; + wifi_config_t wifiConfig = { .sta = { + /* ssid */ {}, + /* password */ {}, + /* scan_method */ {}, + /* bssid_set */ {}, + /* bssid */ {}, + /* channel */ {}, + /* listen_interval */ {}, + /* sort_method */ {}, + /* threshold */ { + /* rssi */ {}, + /* authmode */ WIFI_AUTH_WPA2_PSK + }, + /* pmf_cfg */ { + /* capable */ true, + /* required */ false + }, + /* rm_enabled */ {}, + /* btm_enabled */ {}, + /* mbo_enabled */ {}, // For IDF 4.4 and higher + /* reserved */ {} + } + }; + + // Add in configured SSID/Password parameters. + strncpy((char *)wifiConfig.sta.ssid, this->wifiConfig.clientParams.ssid, MAX_WIFI_SSID_LEN+1); + strncpy((char *)wifiConfig.sta.password, this->wifiConfig.clientParams.pwd, MAX_WIFI_PWD_LEN+1); + + //nvs_handle_t net80211_handle; + //nvs_open("nvs.net80211", NVS_READWRITE, &net80211_handle); + //nvs_erase_all(net80211_handle); + //nvs_commit(net80211_handle); + //nvs_close(net80211_handle); + + // Initialise control structure. + // + wifiCtrl.client.connected = false; + wifiCtrl.client.ip[0] = '\0'; + wifiCtrl.client.netmask[0] = '\0'; + wifiCtrl.client.gateway[0] = '\0'; + + // Create an event handler group to manage callbacks. + s_wifi_event_group = xEventGroupCreate(); + + // Setup the network interface. + if(esp_netif_init()) + { + ESP_LOGI(WIFITAG, "Couldnt initialise netif, disabling WiFi."); + return(false); + } + + // Setup the event loop. + if(esp_event_loop_create_default()) + { + ESP_LOGI(WIFITAG, "Couldnt initialise event loop, disabling WiFi."); + return(false); + } + + // Setup the wifi client (station). + netConfig = esp_netif_create_default_wifi_sta(); + // If fixed IP is configured, set it up. + if(!this->wifiConfig.clientParams.useDHCP) + { + int a, b, c, d; + esp_netif_dhcpc_stop(netConfig); + if(!splitIP(this->wifiConfig.clientParams.ip, &a, &b, &c, &d)) + { + ESP_LOGI(WIFITAG, "Client IP invalid:%s", this->wifiConfig.clientParams.ip); + return false; + } + IP4_ADDR(&ipInfo.ip, a, b, c, d); + + if(!splitIP(this->wifiConfig.clientParams.netmask, &a, &b, &c, &d)) + { + ESP_LOGI(WIFITAG, "Client NETMASK invalid:%s", this->wifiConfig.clientParams.netmask); + return false; + } + IP4_ADDR(&ipInfo.netmask, a, b, c, d); + + if(!splitIP(this->wifiConfig.clientParams.gateway, &a, &b, &c, &d)) + { + ESP_LOGI(WIFITAG, "Client GATEWAY invalid:%s", this->wifiConfig.clientParams.gateway); + return false; + } + IP4_ADDR(&ipInfo.gw, a, b, c, d); + esp_netif_set_ip_info(netConfig, &ipInfo); + } + + // Set TX power to max. + esp_wifi_set_max_tx_power(127); + + // Setup the config for wifi. + wifiInitConfig = WIFI_INIT_CONFIG_DEFAULT(); + if(esp_wifi_init(&wifiInitConfig)) + { + ESP_LOGI(WIFITAG, "Couldnt initialise wifi with default parameters, disabling WiFi."); + return(false); + } + + // Register event handlers. + if(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &wifiClientHandler, this, &instID)) + { + ESP_LOGI(WIFITAG, "Couldnt register event handler for ID, disabling WiFi."); + return(false); + } + if(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &wifiClientHandler, this, &instIP)) + { + ESP_LOGI(WIFITAG, "Couldnt register event handler for IP, disabling WiFi."); + return(false); + } + if(esp_wifi_set_mode(WIFI_MODE_STA)) + { + ESP_LOGI(WIFITAG, "Couldnt set Wifi mode to Client, disabling WiFi."); + return(false); + } + if(esp_wifi_set_config(WIFI_IF_STA, &wifiConfig)) + { + ESP_LOGI(WIFITAG, "Couldnt configure client mode, disabling WiFi."); + return(false); + } + if(esp_wifi_start()) + { + ESP_LOGI(WIFITAG, "Couldnt start Client session, disabling WiFi."); + return(false); + } + + // Waiting until either the connection is established (WIFI_CONNECTED_BIT) or connection failed for the maximum + // number of re-tries (WIFI_FAIL_BIT). The bits are set by wifiClientHandler() (see above) + bits = xEventGroupWaitBits(s_wifi_event_group, WIFI_CONNECTED_BIT | WIFI_FAIL_BIT, pdFALSE, pdFALSE, portMAX_DELAY); + + // xEventGroupWaitBits() returns the bits before the call returned, hence we can test which event actually + // happened. + if(bits & WIFI_CONNECTED_BIT) + { + ESP_LOGI(WIFITAG, "Connected: SSID:%s password:%s", this->wifiConfig.clientParams.ssid, this->wifiConfig.clientParams.pwd); + } + else if (bits & WIFI_FAIL_BIT) + { + ESP_LOGI(WIFITAG, "Connection Fail: SSID:%s, password:%s.", this->wifiConfig.clientParams.ssid, this->wifiConfig.clientParams.pwd); + return(false); + } + else + { + ESP_LOGE(WIFITAG, "Unknown evemt, bits:%d", bits); + return(false); + } + + // No errors. + return(true); +} + +// Method to initialise the interface as a Soft Access point with a given SSID +// and password. +// The Access Point mode is basically to bootstrap a Client connection where the +// client connecting provides the credentials in order to connect as a client to +// another AP to join a local network. +// +bool WiFi::setupWifiAP(void) +{ + // Locals. + // + esp_err_t retcode; + wifi_init_config_t wifiInitConfig; + esp_netif_t *wifiAP; + esp_netif_ip_info_t ipInfo; + wifi_config_t wifiConfig = { .ap = { + /* ssid */ CONFIG_IF_WIFI_SSID, + /* password */ CONFIG_IF_WIFI_DEFAULT_SSID_PWD, + /* ssid_len */ strlen(CONFIG_IF_WIFI_SSID), + /* channel */ CONFIG_IF_WIFI_AP_CHANNEL, + /* authmode */ WIFI_AUTH_WPA_WPA2_PSK, + /* hidden */ CONFIG_IF_WIFI_SSID_HIDDEN, + /* max_connection */ CONFIG_IF_WIFI_MAX_CONNECTIONS, + /* beacon_interval */ 100, + /* pairwise_cipher */ WIFI_CIPHER_TYPE_TKIP, + /* ftm_responder */ 0, + // /* pmf_cfg */ { + // /* capable */ true, + // /* required */ false + // } + } + }; + + // Intialise the network interface. + if(esp_netif_init()) + { + ESP_LOGI(WIFITAG, "Couldnt initialise network interface, disabling WiFi."); + return(false); + } + if((retcode = esp_event_loop_create_default())) + { + ESP_LOGI(WIFITAG, "Couldnt create default loop(%d), disabling WiFi.", retcode); + return(false); + } + + // Create the default Access Point. + // + wifiAP = esp_netif_create_default_wifi_ap(); + + // Setup the base parameters of the Access Point which may differ from ESP32 defaults. + int a, b, c, d; + if(!splitIP(this->wifiConfig.apParams.ip, &a, &b, &c, &d)) + { + ESP_LOGI(WIFITAG, "AP IP invalid:%s", this->wifiConfig.apParams.ip); + return false; + } + IP4_ADDR(&ipInfo.ip, a, b, c, d); + + if(!splitIP(this->wifiConfig.apParams.netmask, &a, &b, &c, &d)) + { + ESP_LOGI(WIFITAG, "AP NETMASK invalid:%s", this->wifiConfig.apParams.netmask); + return false; + } + IP4_ADDR(&ipInfo.netmask, a, b, c, d); + + if(!splitIP(this->wifiConfig.apParams.gateway, &a, &b, &c, &d)) + { + ESP_LOGI(WIFITAG, "AP GATEWAY invalid:%s", this->wifiConfig.apParams.gateway); + return false; + } + IP4_ADDR(&ipInfo.gw, a, b, c, d); + + // Update the SSID/Password from NVS. + strncpy((char *)wifiConfig.ap.ssid, this->wifiConfig.apParams.ssid, MAX_WIFI_SSID_LEN+1); + strncpy((char *)wifiConfig.ap.password, this->wifiConfig.apParams.pwd, MAX_WIFI_PWD_LEN+1); + wifiConfig.ap.ssid_len = (uint8_t)strlen(this->wifiConfig.apParams.ssid); + + // Copy the configured params into the runtime params, just in case they change prior to next boot. + // (this) used for clarity as wifi config local have similar names to global persistence names. + strncpy(this->wifiCtrl.ap.ssid, this->wifiConfig.apParams.ssid, MAX_WIFI_SSID_LEN+1); + strncpy(this->wifiCtrl.ap.pwd, this->wifiConfig.apParams.pwd, MAX_WIFI_PWD_LEN+1); + strncpy(this->wifiCtrl.ap.ip, this->wifiConfig.apParams.ip, MAX_WIFI_IP_LEN+1); + strncpy(this->wifiCtrl.ap.netmask, this->wifiConfig.apParams.netmask, MAX_WIFI_NETMASK_LEN+1); + strncpy(this->wifiCtrl.ap.gateway, this->wifiConfig.apParams.gateway, MAX_WIFI_GATEWAY_LEN+1); + + // Reconfigure the DHCP Server. + esp_netif_dhcps_stop(wifiAP); + esp_netif_set_ip_info(wifiAP, &ipInfo); + esp_netif_dhcps_start(wifiAP); + + // Set TX power to max. + esp_wifi_set_max_tx_power(127); + + // Initialise AP with default parameters. + wifiInitConfig = WIFI_INIT_CONFIG_DEFAULT(); + if(esp_wifi_init(&wifiInitConfig)) + { + ESP_LOGI(WIFITAG, "Couldnt setup AP with default parameters, disabling WiFi."); + return(false); + } + + // Setup callback handlers for wifi events. + if(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &wifiAPHandler, this, NULL)) + { + ESP_LOGI(WIFITAG, "Couldnt setup event handlers, disabling WiFi."); + return(false); + } + + // If there is no password for the access point set authentication to open. + if (strlen(CONFIG_IF_WIFI_DEFAULT_SSID_PWD) == 0) + { + wifiConfig.ap.authmode = WIFI_AUTH_OPEN; + } + + // Setup as an Access Point. + if(esp_wifi_set_mode(WIFI_MODE_AP)) + { + ESP_LOGI(WIFITAG, "Couldnt set mode to Access Point, disabling WiFi."); + return(false); + } + // Configure the Access Point + if(esp_wifi_set_config(WIFI_IF_AP, &wifiConfig)) + { + ESP_LOGI(WIFITAG, "Couldnt configure Access Point, disabling WiFi."); + return(false); + } + // Start the Access Point. + if(esp_wifi_start()) + { + ESP_LOGI(WIFITAG, "Couldnt start Access Point session, disabling WiFi."); + return(false); + } + + // No errors. + return(true); +} + +// Method to disable the wifi turning the transceiver off. +// +bool WiFi::stopWifi(void) +{ + if(esp_wifi_stop()) + { + ESP_LOGI(WIFITAG, "Couldnt stop the WiFi, reboot needed."); + return(false); + } + if(esp_wifi_deinit()) + { + ESP_LOGI(WIFITAG, "Couldnt deactivate WiFi, reboot needed."); + return(false); + } + + // No errors. + return(true); +} + +// WiFi interface runtime logic. This method provides a browser interface to the SharpKey for status query and configuration. +// +void WiFi::run(void) +{ + // Locals. + #define WIFIIFTAG "wifiRun" + + // If Access Point mode has been forced, set the config parameter to AP so that Access Point mode is entered regardless of NVS setting. + if(wifiCtrl.run.wifiMode == WIFI_CONFIG_AP) + { + wifiConfig.params.wifiMode = WIFI_CONFIG_AP; + + // Reset the configured addresses, SSID and password of the Access Point to factory default. This ensures known connection data. + strncpy(wifiConfig.apParams.ssid, CONFIG_IF_WIFI_SSID, MAX_WIFI_SSID_LEN); + strncpy(wifiConfig.apParams.pwd, CONFIG_IF_WIFI_DEFAULT_SSID_PWD, MAX_WIFI_PWD_LEN); + strncpy(wifiConfig.apParams.ip, WIFI_AP_DEFAULT_IP, MAX_WIFI_IP_LEN); + strncpy(wifiConfig.apParams.netmask, WIFI_AP_DEFAULT_NETMASK, MAX_WIFI_NETMASK_LEN); + strncpy(wifiConfig.apParams.gateway, WIFI_AP_DEFAULT_GW, MAX_WIFI_GATEWAY_LEN); + } + + // Enable Access Point mode if configured. + if(wifiConfig.params.wifiMode == WIFI_CONFIG_AP) + { + if(!setupWifiAP()) + { + wifiCtrl.run.reboot = true; + } else + { + wifiCtrl.run.wifiMode = wifiConfig.params.wifiMode; + + // Flash LED to indicate access point mode is active. + // + led->setLEDMode(LED::LED_MODE_BLINK_ONESHOT, LED::LED_DUTY_CYCLE_10, 5, 10000L, 500L); + } + } else + { + // Setup as a client for general browser connectivity. + if(!setupWifiClient()) + { + wifiCtrl.run.reboot = true; + } else + { + wifiCtrl.run.wifiMode = wifiConfig.params.wifiMode; + + // Flash LED to indicate client mode is active. + // + led->setLEDMode(LED::LED_MODE_BLINK_ONESHOT, LED::LED_DUTY_CYCLE_50, 5, 10000L, 500L); + } + } + + // Enter a loop, only exitting if a reboot is required. + do { + // Let other tasks run. NB. This value affects the debounce counter, update as necessary. + vTaskDelay(500); + } while(wifiCtrl.run.reboot == false); + + return; +} + +// Constructor. No overloading methods. +WiFi::WiFi(KeyInterface *hdlKeyIf, KeyInterface *hdlMouseIf, bool defaultMode, NVS *nvs, LED *led, const char *fsPath, t_versionList *versionList) +{ + // Initialise variables. + // + wifiCtrl.client.clientRetryCnt = 0; + wifiCtrl.run.server = NULL; + wifiCtrl.run.errorMsg = ""; + wifiCtrl.run.rebootButton = false; + wifiCtrl.run.reboot = false; + wifiCtrl.run.wifiMode = (defaultMode == true ? WIFI_CONFIG_AP : WIFI_ON); + + // The Non Volatile Storage object is bound to this object for storage and retrieval of configuration data. + this->nvs = nvs; + + // The LED activity indicator object. + this->led = led; + + // Setup the default path on the underlying filesystem. + this->wifiCtrl.run.fsPath = fsPath; + + // Store the version list, used in html variable expansion for version number reporting. + this->wifiCtrl.run.versionList = versionList; + + // Store the classname, used for NVS keys. + this->wifiCtrl.run.thisClass = keyIf->getClassName(__PRETTY_FUNCTION__); + + // Retrieve configuration, if it doesnt exist, set defaults. + // + if(nvs->retrieveData(this->wifiCtrl.run.thisClass.c_str(), &this->wifiConfig, sizeof(t_wifiConfig)) == false) + { + ESP_LOGI(WIFITAG, "Wifi configuration set to default, no valid config in NVS found."); + // Empty set for the client parameters until configured. + wifiConfig.clientParams.valid = false; + wifiConfig.clientParams.ssid[0] = '\0'; + wifiConfig.clientParams.pwd[0] = '\0'; + wifiConfig.clientParams.ip[0] = '\0'; + wifiConfig.clientParams.netmask[0] = '\0'; + wifiConfig.clientParams.gateway[0] = '\0'; + strncpy(wifiConfig.apParams.ssid, CONFIG_IF_WIFI_SSID, MAX_WIFI_SSID_LEN); + strncpy(wifiConfig.apParams.pwd, CONFIG_IF_WIFI_DEFAULT_SSID_PWD, MAX_WIFI_PWD_LEN); + strncpy(wifiConfig.apParams.ip, WIFI_AP_DEFAULT_IP, MAX_WIFI_IP_LEN); + strncpy(wifiConfig.apParams.netmask, WIFI_AP_DEFAULT_NETMASK, MAX_WIFI_NETMASK_LEN); + strncpy(wifiConfig.apParams.gateway, WIFI_AP_DEFAULT_GW, MAX_WIFI_GATEWAY_LEN); + wifiConfig.params.wifiMode = WIFI_CONFIG_AP; + + // Persist the data for next time. + if(nvs->persistData(wifiCtrl.run.thisClass.c_str(), &this->wifiConfig, sizeof(t_wifiConfig)) == false) + { + ESP_LOGI(WIFITAG, "Persisting Default Wifi configuration data failed, check NVS setup."); + } + // Commit data, ensuring values are written to NVS and the mutex is released. + else if(nvs->commitData() == false) + { + ESP_LOGI(WIFITAG, "NVS Commit writes operation failed, some previous writes may not persist in future power cycles."); + } + } + + // The interface objects are bound to this object so that configuration and rendering of web pages can take place. As the KeyInterface class knows about its + // data set and configuration requirements it is the only object which can render web pages for it. + // + keyIf = NULL; + mouseIf = NULL; + if(hdlKeyIf != NULL && hdlMouseIf == NULL) + { + this->keyIf = hdlKeyIf; + } + else if(hdlKeyIf == NULL && hdlMouseIf != NULL) + { + this->keyIf = mouseIf; + } + else if(hdlKeyIf != NULL && hdlMouseIf != NULL) + { + this->keyIf = hdlKeyIf; + this->mouseIf = hdlMouseIf; + } +} + +// Constructor, used for version reporting so no hardware is initialised. +WiFi::WiFi(void) +{ + return; +} + +// Destructor - only ever called when the class is used for version reporting. +WiFi::~WiFi(void) +{ + return; +} + +// End of compile time enabled build of the WiFi module. +#endif diff --git a/main/X1.cpp b/main/X1.cpp deleted file mode 120000 index e0c3ded..0000000 --- a/main/X1.cpp +++ /dev/null @@ -1 +0,0 @@ -../../sharpkey/main/X1.cpp \ No newline at end of file diff --git a/main/X1.cpp b/main/X1.cpp new file mode 100644 index 0000000..2c64f89 --- /dev/null +++ b/main/X1.cpp @@ -0,0 +1,1124 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// Name: X1.cpp +// Created: Mar 2022 +// Version: v1.0 +// Author(s): Philip Smart +// Description: HID (PS/2 or BT keyboard) to Sharp X1 Interface logic. +// This source file contains the singleton class containing logic to obtain +// PS/2 or BT scan codes, map them into Sharp X1 keys and transmit the key to the X1 host. +// +// The class uses a modified version of the PS2KeyAdvanced +// https://github.com/techpaul/PS2KeyAdvanced class from Paul Carpenter. +// +// The whole application of which this class is a member, uses the Espressif Development +// environment with Arduino components. This is necessary for the PS2KeyAdvanced class, +// which I may in future convert to use esp-idf library calls rather than Arduino. +// +// Credits: +// Copyright: (c) 2022 Philip Smart +// +// History: Mar 2022 - Initial write. +// v1.01 May 2022 - Initial release version. +// v1.02 Jun 2022 - Updates to reflect changes realised in other modules due to addition of +// bluetooth and suspend logic due to NVS issues using both cores. +// +// 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 +#include +#include +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/queue.h" +#include "driver/gpio.h" +#include "esp_log.h" +#include "soc/timer_group_struct.h" +#include "soc/timer_group_reg.h" +#include "driver/timer.h" +#include "sys/stat.h" +#include "esp_littlefs.h" +#include "PS2KeyAdvanced.h" +#include "sdkconfig.h" +#include "X1.h" + +// Tag for ESP main application logging. +#define MAINTAG "x1key" + +// FreeRTOS Queue handle to pass messages from the HID Keyboard Mapper into the X1 transmission logic. +static QueueHandle_t xmitQueue; + +// X1 Protocol +// ----------- +// Mode A (general) - = 16bits per key. +// Mode B (game) - +// +// Mode A +// 16 bits: <8bit CTRL><8bit ASCII Code> +// +// : +// /TEN /KIN /REP /GRPH /CAPS /KANA /SFT /CTRL +// +// Parameter name value Parameter description +// /TEN 0 Input from the numeric keypad, function keys, and special keys +// 1 Normal key +// /KIN 0 Key Press +// 1 No key or key Release +// /REP 0 Repeat key input +// 1 First key input +// /GRPH 0 GRAPH key ON +// 1 GRAPH key OFF +// /CAPS 0 CAPS key ON +// 1 CAPS key OFF +// /KANA 0 Kana key ON +// 1 Kana key OFF +// /SFT 0 Shift key ON +// 1 Shift key OFF +// /CTRL 0 CTRL key ON +// 1 CTRL key OFF +// +// : +// ASCII code in range 00H - FFH where 00H is no key (used when sending control only updates). +// +// Mode B +// 24 bits: +// +// Mode B is intended for gaming and sends a subset of keys as direct bit representation. 24bits are transmitted on each key press/change and using a faster serial protocol so minimise lag. +// 7 6 5 4 3 2 1 0 +// BYTE1 : Q W E A D Z X C - Direct key press, 0 if pressed. +// BYTE2 : 7 4 1 8 2 9 6 3 +// BYTE3 : E 1 - + * H S R +// S T P E +// C A T +// B +// Keys which use the numeric keypad as well as normal keys: 1,2,3,4,6,7,8,9, *, +,- +// RET key is the main keyboard RET. + +// Function to push a keycode onto the key queue ready for transmission. +// +void X1::pushKeyToQueue(bool keybMode, uint32_t key) +{ + // Locals. + t_xmitQueueMessage xmitMsg; + #define PUSHKEYTAG "pushKeyToQueue" + + xmitMsg.modeB = keybMode; + xmitMsg.keyCode = key; + if( xQueueSend(xmitQueue, (void *)&xmitMsg, 10) != pdPASS) + { + ESP_LOGW(PUSHKEYTAG, "Failed to put scancode:%04x into xmitQueue", key); + } + return; +} + +// Method to realise the X1 1 wire serial protocol in order to transmit key presses to the X1. +// This method uses Core 1 and it will hold it in a spinlock as necessary to ensure accurate timing. +// A key is passed into the method via the FreeRTOS Queue handle xmitQueue. +IRAM_ATTR void X1::x1Interface( void * pvParameters ) +{ + // Locals. + t_xmitQueueMessage rcvMsg; + + // Mask values declared as variables, let the optimiser decide wether they are constants or placed in-memory. + uint32_t X1DATA_MASK = (1 << CONFIG_HOST_KDO0); + uint64_t delayTimer = 0LL; + uint64_t curTime = 0LL; + bool bitStart = true; + uint32_t bitCount = 0; + enum X1XMITSTATE { + FSM_IDLE = 0, + FSM_STARTXMIT = 1, + FSM_HEADER = 2, + FSM_START = 3, + FSM_DATA = 4, + FSM_STOP = 5, + FSM_ENDXMIT = 6 + } state = FSM_IDLE; + + // Retrieve pointer to object in order to access data. + X1* pThis = (X1*)pvParameters; + + // Initialise the MUTEX which prevents this core from being released to other tasks. + pThis->x1Mutex = portMUX_INITIALIZER_UNLOCKED; + + // Initial delay needed because the xQueue will assert probably on a suspended task ALL if delay not inserted! + vTaskDelay(1000); + + // Sign on. + ESP_LOGW(MAINTAG, "Starting X1 thread."); + + // X1 data out default state is high. + GPIO.out_w1ts = X1DATA_MASK; + + // Configure a timer to be used for X1 protocol spacing with 1uS resolution. The default clock source is the APB running at 80MHz. + timer_config_t timerConfig = { + .alarm_en = TIMER_ALARM_DIS, // No alarm, were not using interrupts as we are in a dedicated thread. + .counter_en = TIMER_PAUSE, // Timer paused until required. + .intr_type = TIMER_INTR_LEVEL, // No interrupts used. + .counter_dir = TIMER_COUNT_UP, // Timing a fixed period. + .auto_reload = TIMER_AUTORELOAD_DIS, // No need for auto reload, fixed time period. + .divider = 80 // 1Mhz operation giving 1uS resolution. + }; + ESP_ERROR_CHECK(timer_init(TIMER_GROUP_0, TIMER_0, &timerConfig)); + ESP_ERROR_CHECK(timer_set_counter_value(TIMER_GROUP_0, TIMER_0, 0)); + + // Permanent loop, wait for an incoming message on the key to send queue, read it then transmit to the X1, repeat! + for(;;) + { + // Get the current timer value, only run the FSM when the timer is idle. + timer_get_counter_value(TIMER_GROUP_0, TIMER_0, &curTime); + if(curTime >= delayTimer) + { + // Ensure the timer is stopped. + timer_pause(TIMER_GROUP_0, TIMER_0); + delayTimer = 0LL; + + // Finite state machine to retrieve a key for transmission then serialise it according to the X1 protocol. + switch(state) + { + case FSM_IDLE: + // Yield if the suspend flag is set. + pThis->yield(0); + + // Check stack space, report if it is getting low. + if(uxTaskGetStackHighWaterMark(NULL) < 1024) + { + ESP_LOGW(MAINTAG, "THREAD STACK SPACE(%d)\n",uxTaskGetStackHighWaterMark(NULL)); + } + + // If a new message arrives, start the serialiser to send it to the X1. + if(xQueueReceive(xmitQueue, (void *)&rcvMsg, 0) == pdTRUE) + { + ESP_LOGW(MAINTAG, "Received:%08x, %d", rcvMsg.keyCode, rcvMsg.modeB); + state = FSM_STARTXMIT; + + // Create, initialise and hold a spinlock so the current core is bound to this one method. + portENTER_CRITICAL(&pThis->x1Mutex); + } + break; + + case FSM_STARTXMIT: + // Ensure all variables and states correct before entering serialisation. + bitStart = true; + GPIO.out_w1ts = X1DATA_MASK; + state = FSM_HEADER; + if(rcvMsg.modeB) + bitCount = 24; + else + bitCount = 16; + break; + + case FSM_HEADER: + if(bitStart) + { + // Send out the header by bringing X1DATA low for 1000us then high for 700uS. + GPIO.out_w1tc = X1DATA_MASK; + delayTimer = pThis->x1Control.modeB ? 400LL : 1000LL; + } else + { + // Bring high for 700us. + GPIO.out_w1ts = X1DATA_MASK; + delayTimer = pThis->x1Control.modeB ? 200LL : 700LL; + state = FSM_DATA; // Jump past the Start Bit, I think the header is the actual start bit as there is an error in the X1 Center specs. + } + bitStart = !bitStart; + break; + + // The original X1 Center specification shows a start bit but this doesnt seem necessary, in fact it is interpreted as a data bit, hence the + // FSM jumps this state. + case FSM_START: + if(bitStart) + { + // Send out the start bit by bringing X1DATA low for 250us then high for 750uS. + GPIO.out_w1tc = X1DATA_MASK; + delayTimer = pThis->x1Control.modeB ? 250LL : 250LL; + } else + { + // Bring high for 750us. + GPIO.out_w1ts = X1DATA_MASK; + delayTimer = pThis->x1Control.modeB ? 250LL : 750LL; + state = FSM_DATA; + } + bitStart = !bitStart; + break; + + case FSM_DATA: + if(bitCount > 0) + { + if(bitStart) + { + // Send out the data bit by bringing X1DATA low for 250us then high for 1750uS when bit = 1 else 750uS when bit = 0. + GPIO.out_w1tc = X1DATA_MASK; + delayTimer = 250LL; + delayTimer = pThis->x1Control.modeB ? 250LL : 250LL; + } else + { + // Bring X1DATA high... + GPIO.out_w1ts = X1DATA_MASK; + + // ... Mode A 1750us as bit = 1, mode B 750uS. + if((rcvMsg.modeB && rcvMsg.keyCode & 0x800000) || (!rcvMsg.modeB && rcvMsg.keyCode & 0x8000)) + { + delayTimer = pThis->x1Control.modeB ? 750LL : 1750LL; + } else + // ... Mode A 750us as bit = 0, mode B 250uS. + { + delayTimer = pThis->x1Control.modeB ? 250LL : 750LL; + } + rcvMsg.keyCode = (rcvMsg.keyCode << 1); + bitCount--; + } + bitStart = !bitStart; + } else + { + state = FSM_STOP; + } + break; + + case FSM_STOP: + if(bitStart) + { + // Send out the stop bit, same in Mode A and B, by bringing X1DATA low for 250us then high for 250uS. + GPIO.out_w1tc = X1DATA_MASK; + delayTimer = 250LL; + delayTimer = pThis->x1Control.modeB ? 250LL : 250LL; + } else + { + // Bring high for 250us. + GPIO.out_w1ts = X1DATA_MASK; + delayTimer = pThis->x1Control.modeB ? 250LL : 250LL; + state = FSM_ENDXMIT; + } + bitStart = !bitStart; + break; + + case FSM_ENDXMIT: + // End of critical timing loop, release the core. + portEXIT_CRITICAL(&pThis->x1Mutex); + state = FSM_IDLE; + break; + + } + + // If a new delay is requested, set the value into the timer and start. + if(delayTimer > 0LL) + { + timer_set_counter_value(TIMER_GROUP_0, TIMER_0, 0LL); + timer_start(TIMER_GROUP_0, TIMER_0); + } + } + + // Logic to feed the watchdog if needed. Watchdog disabled in menuconfig but if enabled this will need to be used. + //TIMERG0.wdt_wprotect=TIMG_WDT_WKEY_VALUE; // write enable + //TIMERG0.wdt_feed=1; // feed dog + //TIMERG0.wdt_wprotect=0; // write protect + //TIMERG1.wdt_wprotect=TIMG_WDT_WKEY_VALUE; // write enable + //TIMERG1.wdt_feed=1; // feed dog + //TIMERG1.wdt_wprotect=0; // write protect + } +} + +// Method to select keyboard configuration options. When a key sequence is pressed, ie. SHIFT+CTRL+ESC then the fourth simultaneous key is the required option and given to this +// method to act on. Options can be machine model, keyboard map etc. +// +void X1::selectOption(uint8_t optionCode) +{ + // Locals. + // + bool updated = true; + #define SELOPTTAG "selectOption" + + // Simple switch to decode the required option and act on it. + switch(optionCode) + { + // Select a keymap using 1..8 or default (STANDARD) using 0. + case PS2_KEY_1: + this->x1Config.params.activeKeyboardMap = KEYMAP_UK_WYSE_KB3926; + break; + case PS2_KEY_2: + this->x1Config.params.activeKeyboardMap = KEYMAP_JAPAN_OADG109; + break; + case PS2_KEY_3: + this->x1Config.params.activeKeyboardMap = KEYMAP_JAPAN_SANWA_SKBL1; + break; + case PS2_KEY_4: + this->x1Config.params.activeKeyboardMap = KEYMAP_NOT_ASSIGNED_4; + break; + case PS2_KEY_5: + this->x1Config.params.activeKeyboardMap = KEYMAP_NOT_ASSIGNED_5; + break; + case PS2_KEY_6: + this->x1Config.params.activeKeyboardMap = KEYMAP_NOT_ASSIGNED_6; + break; + case PS2_KEY_7: + this->x1Config.params.activeKeyboardMap = KEYMAP_UK_PERIBOARD_810; + break; + case PS2_KEY_8: + this->x1Config.params.activeKeyboardMap = KEYMAP_UK_OMOTON_K8508; + break; + case PS2_KEY_0: + this->x1Config.params.activeKeyboardMap = KEYMAP_STANDARD; + break; + + // Select the model of the host to enable specific mappings. + case PS2_KEY_END: + this->x1Config.params.activeMachineModel = X1_ORIG; + break; + case PS2_KEY_DN_ARROW: + this->x1Config.params.activeMachineModel = X1_TURBO; + break; + case PS2_KEY_PGDN: + this->x1Config.params.activeMachineModel = X1_TURBOZ; + break; + case PS2_KEY_INSERT: + this->x1Config.params.activeMachineModel = X1_ALL; + break; + + // Switch to keyboard Mode A. This mode is not persisted. + case PS2_KEY_HOME: + updated = false; + this->x1Control.modeB = false; + break; + // Switch to keyboard Mode B. This mode is not persisted. + case PS2_KEY_PGUP: + updated = false; + this->x1Control.modeB = true; + break; + + // Unknown option so ignore. + default: + updated = false; + break; + } + + // If an update was made, persist it for power cycles. + // + if(updated) + { + this->x1Control.persistConfig = true; + } + + return; +} + +// Method to take a PS/2 key and control data and map it into an X1 key and control equivalent, updating state values accordingly (ie. CAPS). +// A mapping table is used which maps a key and state values into an X1 key and control values, the emphasis being on readability and easy configuration +// as opposed to concatenated byte tables. +// +uint32_t X1::mapKey(uint16_t scanCode) +{ + // Locals. + uint32_t idx; + uint8_t keyCode = (scanCode & 0xFF); + bool mapped = false; + bool matchExact = false; + uint32_t mappedKey = 0x00000000; + #define MAPKEYTAG "mapKey" + + // Intercept control keys and set state variables. + // + // + if(scanCode & PS2_BREAK) + { + if((keyCode == PS2_KEY_L_SHIFT || keyCode == PS2_KEY_R_SHIFT) && (scanCode & PS2_SHIFT) == 0) { mapped=true; this->x1Control.keyCtrl |= X1_CTRL_SHIFT; } + if((keyCode == PS2_KEY_L_CTRL || keyCode == PS2_KEY_R_CTRL) && (scanCode & PS2_CTRL) == 0) { mapped=true; this->x1Control.keyCtrl |= X1_CTRL_CTRL; } + if(keyCode == PS2_KEY_SCROLL) { mapped = true; this->x1Control.modeB = false; } + + // Any break key clears the option select flag. + this->x1Control.optionSelect = false; + + // Clear any feature LED blinking. + led->setLEDMode(LED::LED_MODE_OFF, LED::LED_DUTY_CYCLE_OFF, 0, 0L, 0L); + } else + { + if((keyCode == PS2_KEY_L_SHIFT || keyCode == PS2_KEY_R_SHIFT) && (scanCode & PS2_SHIFT)) { mapped=true; this->x1Control.keyCtrl &= ~X1_CTRL_SHIFT; } + if((keyCode == PS2_KEY_L_CTRL || keyCode == PS2_KEY_R_CTRL) && (scanCode & PS2_CTRL)) { mapped=true; this->x1Control.keyCtrl &= ~X1_CTRL_CTRL; } + if(keyCode == PS2_KEY_L_ALT) { mapped = true; this->x1Control.keyCtrl ^= X1_CTRL_KANA; } + if(keyCode == PS2_KEY_R_ALT) { mapped = true; this->x1Control.keyCtrl ^= X1_CTRL_GRAPH; } + if(keyCode == PS2_KEY_CAPS) { mapped = true; this->x1Control.keyCtrl ^= X1_CTRL_CAPS; } + if(keyCode == PS2_KEY_SCROLL) { mapped = true; this->x1Control.modeB = true; } + // Special mapping to allow selection of keyboard options. If the user presses CTRL+SHIFT+ESC then a flag becomes active and should a fourth key be pressed before a BREAK then the fourth key is taken as an option key and processed accordingly. + if(this->x1Control.optionSelect == true) { mapped = true; this->x1Control.optionSelect = false; selectOption(keyCode); } + if(keyCode == PS2_KEY_ESC && (scanCode & PS2_CTRL) && (scanCode & PS2_SHIFT)) { mapped = true; this->x1Control.optionSelect = true; } + if(this->x1Control.optionSelect == true && keyCode != PS2_KEY_ESC) + { + mapped = true; + this->x1Control.optionSelect = false; + selectOption(keyCode); + } + if(keyCode == PS2_KEY_ESC && (scanCode & PS2_CTRL) && (scanCode & PS2_SHIFT) && this->x1Control.optionSelect == false) + { + // Prime flag ready for fourth option key and start LED blinking periodically. + mapped = true; + this->x1Control.optionSelect = true; + led->setLEDMode(LED::LED_MODE_BLINK, LED::LED_DUTY_CYCLE_50, 1, 500L, 500L); + } + } + + // If the key already mapped, ie. due to control signals, send the update as <0x00> so the X1 knows the current control signal state. + if(mapped == true) + { + ESP_LOGW(MAPKEYTAG, "Mapped special key:%02x\n", this->x1Control.keyCtrl); + mappedKey = (this->x1Control.keyCtrl << 8) | 0x00; + } else + { + // Loop through the entire conversion table to find a match on this key, if found map to X1 equivalent. + // switch matrix. + // + for(idx=0, mapped=false, matchExact=false; idx < x1Control.kmeRows && (mapped == false || (mapped == true && matchExact == false)); idx++) + { + // Match key code? Make sure the current machine and keymap match as well. + if(x1Control.kme[idx].ps2KeyCode == (uint8_t)(scanCode&0xFF) && ((x1Control.kme[idx].machine == X1_ALL) || ((x1Control.kme[idx].machine & x1Config.params.activeMachineModel) != 0)) && ((x1Control.kme[idx].keyboardModel & x1Config.params.activeKeyboardMap) != 0) && ((x1Control.kme[idx].x1Mode == X1_MODE_A && this->x1Control.modeB == false) || (x1Control.kme[idx].x1Mode == X1_MODE_B && this->x1Control.modeB == true))) + { + // If CAPS lock is set in the table and in the scanCode, invert SHIFT so we send the correct value. + if((scanCode & PS2_CAPS) && (x1Control.kme[idx].ps2Ctrl & PS2CTRL_CAPS) != 0) + { + scanCode ^= PS2_SHIFT; + } + + // Match Raw, Shift, Function, Control, ALT or ALT-Gr? + if( (((x1Control.kme[idx].ps2Ctrl & PS2CTRL_SHIFT) == 0) && ((x1Control.kme[idx].ps2Ctrl & PS2CTRL_CTRL) == 0) && ((x1Control.kme[idx].ps2Ctrl & PS2CTRL_KANA) == 0) && ((x1Control.kme[idx].ps2Ctrl & PS2CTRL_GRAPH) == 0) && ((x1Control.kme[idx].ps2Ctrl & PS2CTRL_GUI) == 0) && ((x1Control.kme[idx].ps2Ctrl & PS2CTRL_FUNC) == 0)) || + ((scanCode & PS2_SHIFT) && (x1Control.kme[idx].ps2Ctrl & PS2CTRL_SHIFT) != 0) || + ((scanCode & PS2_CTRL) && (x1Control.kme[idx].ps2Ctrl & PS2CTRL_CTRL) != 0) || + ((this->x1Control.keyCtrl & X1_CTRL_KANA) == 0 && (x1Control.kme[idx].ps2Ctrl & PS2CTRL_KANA) != 0) || + ((this->x1Control.keyCtrl & X1_CTRL_GRAPH) == 0 && (x1Control.kme[idx].ps2Ctrl & PS2CTRL_GRAPH) != 0) || + ((scanCode & PS2_GUI) && (x1Control.kme[idx].ps2Ctrl & PS2CTRL_GUI) != 0) || + ((scanCode & PS2_FUNCTION) && (x1Control.kme[idx].ps2Ctrl & PS2CTRL_FUNC) != 0) ) + { + + // Exact entry match, data + control key? On an exact match we only process the first key. On a data only match we fall through to include additional data and control key matches to allow for un-mapped key combinations, ie. Japanese characters. + matchExact = (((scanCode & PS2_SHIFT) && (x1Control.kme[idx].ps2Ctrl & PS2CTRL_SHIFT) != 0) || ((scanCode & PS2_SHIFT) == 0 && (x1Control.kme[idx].ps2Ctrl & PS2CTRL_SHIFT) == 0)) && + (((scanCode & PS2_CTRL) && (x1Control.kme[idx].ps2Ctrl & PS2CTRL_CTRL) != 0) || ((scanCode & PS2_CTRL) == 0 && (x1Control.kme[idx].ps2Ctrl & PS2CTRL_CTRL) == 0)) && + (((this->x1Control.keyCtrl & X1_CTRL_KANA) == 0 && (x1Control.kme[idx].ps2Ctrl & PS2CTRL_KANA) != 0) || ((this->x1Control.keyCtrl & X1_CTRL_KANA) && (x1Control.kme[idx].ps2Ctrl & PS2CTRL_KANA) == 0)) && + (((this->x1Control.keyCtrl & X1_CTRL_GRAPH) == 0 && (x1Control.kme[idx].ps2Ctrl & PS2CTRL_GRAPH) != 0) || ((this->x1Control.keyCtrl & X1_CTRL_GRAPH) && (x1Control.kme[idx].ps2Ctrl & PS2CTRL_GRAPH) == 0)) && + (((scanCode & PS2_GUI) && (x1Control.kme[idx].ps2Ctrl & PS2CTRL_GUI) != 0) || ((scanCode & PS2_GUI) == 0 && (x1Control.kme[idx].ps2Ctrl & PS2CTRL_GUI) == 0)) && + (((scanCode & PS2_FUNCTION) && (x1Control.kme[idx].ps2Ctrl & PS2CTRL_FUNC) != 0) || ((scanCode & PS2_FUNCTION) == 0 && (x1Control.kme[idx].ps2Ctrl & PS2CTRL_FUNC) == 0)); + + // RELEASE (PS2_BREAK == 1) or PRESS? + if((scanCode & PS2_BREAK)) + { + // Special case for the PAUSE / BREAK key. The underlying logic has been modified to send a BREAK key event immediately + // after a PAUSE make, this is necessary as the Sharp machines require SHIFT (pause) BREAK so the PS/2 CTRL+BREAK wont + // work (unless logic is added to insert a SHIFT, pause, add BREAK). The solution was to generate a BREAK event + // when SHIFT+PAUSE is pressed. + if(keyCode == PS2_KEY_PAUSE) + { + vTaskDelay(100); + } + + // Mode A sends a release with 0x00. + if(this->x1Control.modeB == false) + { + mappedKey = (0xFF << 8) | 0x00; + mapped = true; + // vTaskDelay(300); + } else + if(this->x1Control.modeB == true) + { + // Clear only the bits relevant to the released key. + mappedKey &= ((x1Control.kme[idx].x1Ctrl << 16) | (x1Control.kme[idx].x1Key2 << 8) | x1Control.kme[idx].x1Key); + } + } else + { + // Mode A return the key in the table, mode B OR the key to build up a final map. + if(this->x1Control.modeB == false) + mappedKey = ((x1Control.kme[idx].x1Ctrl & this->x1Control.keyCtrl) << 8) | x1Control.kme[idx].x1Key; + else + mappedKey |= ((x1Control.kme[idx].x1Ctrl << 16) | (x1Control.kme[idx].x1Key2 << 8) | x1Control.kme[idx].x1Key); + mapped = true; + //printf("%02x,%02x,%d,%d\n", (x1Control.kme[idx].x1Ctrl & this->x1Control.keyCtrl), x1Control.kme[idx].x1Key, idx,this->x1Control.modeB); + } + } + } + } + } + return(mappedKey); +} + +// Primary HID thread, running on Core 0. +// This thread is responsible for receiving HID (PS/2 or BT) keyboard scan codes and mapping them to Sharp X1 equivalent keys, updating state flags as needed. +// The HID data is received via interrupt. The data to be sent to the X1 is pushed onto a FIFO queue. +// +IRAM_ATTR void X1::hidInterface( void * pvParameters ) +{ + // Locals. + uint16_t scanCode = 0x0000; + uint32_t x1Key = 0x00000000; + + // Map the instantiating object so we can access its methods and data. + X1* pThis = (X1*)pvParameters; + + // Thread never exits, just polls the keyboard and updates the matrix. + while(1) + { + // Check stack space, report if it is getting low. + if(uxTaskGetStackHighWaterMark(NULL) < 1024) + { + ESP_LOGW(MAINTAG, "THREAD STACK SPACE(%d)\n",uxTaskGetStackHighWaterMark(NULL)); + } + + // Check for HID keyboard scan codes. + while((scanCode = pThis->hid->read()) != 0) + { + // Scan Code Breakdown: + // Define name bit description + // PS2_BREAK 15 1 = Break key code + // (MSB) 0 = Make Key code + // PS2_SHIFT 14 1 = Shift key pressed as well (either side) + // 0 = No shift key + // PS2_CTRL 13 1 = Ctrl key pressed as well (either side) + // 0 = No Ctrl key + // PS2_CAPS 12 1 = Caps Lock ON + // 0 = Caps lock OFF + // PS2_ALT 11 1 = Left Alt key pressed as well + // 0 = No Left Alt key + // PS2_ALT_GR 10 1 = Right Alt (Alt GR) key pressed as well + // 0 = No Right Alt key + // PS2_GUI 9 1 = GUI key pressed as well (either) + // 0 = No GUI key + // PS2_FUNCTION 8 1 = FUNCTION key non-printable character (plus space, tab, enter) + // 0 = standard character key + // 7-0 PS/2 Key code. + // + // BREAK code means all keys released so clear out flags and send update. + ESP_LOGW(MAPKEYTAG, "SCANCODE:%04x", scanCode); + + // Map the PS/2 key to an X1 CTRL + KEY + x1Key = pThis->mapKey(scanCode); + if(x1Key != 0L) { pThis->pushKeyToQueue(pThis->x1Control.modeB, x1Key); } + + // Toggle LED to indicate data flow. + if((scanCode & PS2_BREAK) == 0) + pThis->led->setLEDMode(LED::LED_MODE_BLINK_ONESHOT, LED::LED_DUTY_CYCLE_10, 1, 100L, 0L); + } + + // NVS writes require both CPU cores to be free so write config out at a known junction. + if(pThis->x1Control.persistConfig == true) + { + // Request and wait for the interface to suspend. This ensures that the host cpu is not held in a spinlock when NVS update is requested avoiding deadlock. + pThis->suspendInterface(true); + pThis->isSuspended(true); + + if(pThis->nvs->persistData(pThis->getClassName(__PRETTY_FUNCTION__), &pThis->x1Config, sizeof(t_x1Config)) == false) + { + ESP_LOGW(SELOPTTAG, "Persisting X1 configuration data failed, updates will not persist in future power cycles."); + pThis->led->setLEDMode(LED::LED_MODE_BLINK_ONESHOT, LED::LED_DUTY_CYCLE_10, 200, 1000L, 0L); + } else + // Few other updates so make a commit here to ensure data is flushed and written. + if(pThis->nvs->commitData() == false) + { + ESP_LOGW(SELOPTTAG, "NVS Commit writes operation failed, some previous writes may not persist in future power cycles."); + pThis->led->setLEDMode(LED::LED_MODE_BLINK_ONESHOT, LED::LED_DUTY_CYCLE_10, 200, 500L, 0L); + } + + // Release interface. + pThis->suspendInterface(false); + + // Clear flag so we dont persist in a loop. + pThis->x1Control.persistConfig = false; + } + + // Yield if the suspend flag is set. + pThis->yield(10); + } +} + +// A method to load the keyboard mapping table into memory for use in the interface mapping logic. If no persistence file exists or an error reading persistence occurs, the keymap +// uses the internal static default. If no persistence file exists and attempt is made to create it with a copy of the inbuilt static map so that future operations all +// work with persistence such that modifications can be made. +// +bool X1::loadKeyMap(void) +{ + // Locals. + // + bool result = false; + int fileRows = 0; + struct stat keyMapFileNameStat; + + // See if the file exists, if it does, get size so we can compute number of mapping rows. + if(stat(x1Control.keyMapFileName.c_str(), &keyMapFileNameStat) == -1) + { + ESP_LOGW(MAINTAG, "No keymap file, using inbuilt definitions."); + } else + { + // Get number of rows in the file. + fileRows = keyMapFileNameStat.st_size/sizeof(t_keyMapEntry); + + // Subsequent reloads, delete memory prior to building new map, primarily to conserve precious resources rather than trying the memory allocation trying to realloc and then having to copy. + if(x1Control.kme != NULL && x1Control.kme != PS2toX1.kme) + { + delete x1Control.kme; + x1Control.kme = NULL; + } + + // Allocate memory for the new keymap table. + x1Control.kme = new t_keyMapEntry[fileRows]; + if(x1Control.kme == NULL) + { + ESP_LOGW(MAINTAG, "Failed to allocate memory for keyboard map, fallback to inbuilt!"); + } else + { + // Open the keymap extension file for binary reading to add data to our map table. + std::fstream keyFileIn(x1Control.keyMapFileName.c_str(), std::ios::in | std::ios::binary); + + int idx=0; + while(keyFileIn.good()) + { + keyFileIn.read((char *)&x1Control.kme[idx], sizeof(t_keyMapEntry)); + if(keyFileIn.good()) + { + idx++; + } + } + // Any errors, we wind back and use the inbuilt mapping table. + if(keyFileIn.bad()) + { + keyFileIn.close(); + ESP_LOGW(MAINTAG, "Failed to read data from keymap extension file:%s, fallback to inbuilt!", x1Control.keyMapFileName.c_str()); + } else + { + // No longer need the file. + keyFileIn.close(); + + // Max rows in the KME table. + x1Control.kmeRows = fileRows; + + // Good to go, map ready for use with the interface. + result = true; + } + } + } + + // Any failures, free up memory and use the inbuilt mapping table. + if(result == false) + { + if(x1Control.kme != NULL && x1Control.kme != PS2toX1.kme) + { + delete x1Control.kme; + x1Control.kme = NULL; + } + + // No point allocating memory if no extensions exist or an error occurs, just point to the static table. + x1Control.kme = PS2toX1.kme; + x1Control.kmeRows = PS2TBL_X1_MAXROWS; + + // Persist the data so that next load comes from file. + saveKeyMap(); + } + + // Return code. Either memory map was successfully loaded, true or failed, false. + return(result); +} + +// Method to save the current keymap out to an extension file. +// +bool X1::saveKeyMap(void) +{ + // Locals. + // + bool result = false; + int idx = 0; + + // Has a map been defined? Cannot save unless loadKeyMap has been called which sets x1Control.kme to point to the internal keymap or a new memory resident map. + // + if(x1Control.kme == NULL) + { + ESP_LOGW(MAINTAG, "KeyMap hasnt yet been defined, need to call loadKeyMap."); + } else + { + // Open file for binary writing, trunc specified to clear out the file, we arent appending. + std::fstream keyFileOut(x1Control.keyMapFileName.c_str(), std::ios::out | std::ios::binary | std::ios::trunc); + + // Loop whilst no errors and data rows still not written. + while(keyFileOut.good() && idx < x1Control.kmeRows) + { + keyFileOut.write((char *)&x1Control.kme[idx], sizeof(t_keyMapEntry)); + idx++; + } + if(keyFileOut.bad()) + { + ESP_LOGW(MAINTAG, "Failed to write data from the keymap to file:%s, deleting as state is unknown!", x1Control.keyMapFileName.c_str()); + keyFileOut.close(); + std::remove(x1Control.keyMapFileName.c_str()); + } else + { + // Success. + keyFileOut.close(); + result = true; + } + } + + // Return code. Either memory map was successfully saved, true or failed, false. + return(result); +} + +// Public method to open a keymap file for data upload. +// This method opens the file and makes any validation checks as necessary. +// +bool X1::createKeyMapFile(std::fstream &outFile) +{ + // Locals. + // + bool result = true; + std::string fileName; + + // Attempt to open a temporary keymap file for writing. + // + fileName = x1Control.keyMapFileName; + replaceExt(fileName, "tmp"); + outFile.open(fileName.c_str(), std::ios::out | std::ios::binary | std::ios::trunc); + if(outFile.bad()) + { + result = false; + } + + // Send result. + return(result); +} + +// Public method to validate and store data provided by caller into an open file created by 'createKeyMapFile'. +// +bool X1::storeDataToKeyMapFile(std::fstream &outFile, char *data, int size) +{ + // Locals. + // + bool result = true; + + // Check that the file is still writeable then add data. + if(outFile.good()) + { + outFile.write(data, size); + } + if(outFile.bad()) + { + result = false; + } + + // Send result. + return(result); +} + +// Polymorphic alternative to take a vector of bytes for writing to the output file. +// +bool X1::storeDataToKeyMapFile(std::fstream & outFile, std::vector& dataArray) +{ + // Locals. + // + bool result = true; + char data[1]; + + // Check that the file is still writeable then add data. Not best for performace but ease of use and minimum memory. + if(outFile.good()) + { + for(std::size_t idx = 0; idx < dataArray.size(); idx++) + { + data[0] = (char)dataArray[idx]; + outFile.write((char *)&data, 1); + } + } + if(outFile.bad()) + { + result = false; + } + + // Send result. + return(result); +} + +// Public method to close and commit a data file, created by 'createKeyMapFile' and populated by 'storeDataToKeyMapFile'. +// This involves renaming the original keymap file, closing the new file and renaming it to the original keymap filename. +// +bool X1::closeAndCommitKeyMapFile(std::fstream &outFile, bool cleanupOnly) +{ + // Locals. + // + bool result = true; + std::string fileName; + + // Check the file is still accessible and close. + // + outFile.close(); + if(!cleanupOnly) + { + if(outFile.good()) + { + // Rename the original file. + fileName = x1Control.keyMapFileName; + replaceExt(fileName, "bak"); + // Remove old backup file. Dont worry if it is not there! + std::remove(fileName.c_str()); + replaceExt(fileName, "tmp"); + // Rename new file to active. + if(std::rename(fileName.c_str(), x1Control.keyMapFileName.c_str()) != 0) + { + result = false; + } + } else + { + result = false; + } + } + + // Send result. + return(result); +} + +// Method to return the keymap column names as header strings. +// +void X1::getKeyMapHeaders(std::vector& headerList) +{ + // Add the names. + // + headerList.push_back(PS2TBL_PS2KEYCODE_NAME); + headerList.push_back(PS2TBL_PS2CTRL_NAME); + headerList.push_back(PS2TBL_KEYBOARDMODEL_NAME); + headerList.push_back(PS2TBL_MACHINE_NAME); + headerList.push_back(PS2TBL_X1MODE_NAME); + headerList.push_back(PS2TBL_X1KEYCODE_NAME); + headerList.push_back(PS2TBL_X1KEYCODE_BYTE2_NAME); + headerList.push_back(PS2TBL_X1_CTRL_NAME); + + return; +} + +// A method to return the Type of data for a given column in the KeyMap table. +// +void X1::getKeyMapTypes(std::vector& typeList) +{ + // Add the types. + // + typeList.push_back(PS2TBL_PS2KEYCODE_TYPE); + typeList.push_back(PS2TBL_PS2CTRL_TYPE); + typeList.push_back(PS2TBL_KEYBOARDMODEL_TYPE); + typeList.push_back(PS2TBL_MACHINE_TYPE); + typeList.push_back(PS2TBL_X1MODE_TYPE); + typeList.push_back(PS2TBL_X1KEYCODE_TYPE); + typeList.push_back(PS2TBL_X1KEYCODE_BYTE2_TYPE); + typeList.push_back(PS2TBL_X1CTRL_TYPE); + + return; +} + +// Method to return a list of key:value entries for a given keymap column. This represents the +// feature which can be selected and the value it uses. Features can be combined by ORing the values +// together. +bool X1::getKeyMapSelectList(std::vector>& selectList, std::string option) +{ + // Locals. + // + bool result = true; + + // Build up a map, depending on the list required, of name to value. This list can then be used + // by a user front end to select an option based on a name and return its value. + if(option.compare(PS2TBL_PS2CTRL_TYPE) == 0) + { + selectList.push_back(std::make_pair(PS2TBL_PS2CTRL_SEL_SHIFT, PS2CTRL_SHIFT)); + selectList.push_back(std::make_pair(PS2TBL_PS2CTRL_SEL_CTRL, PS2CTRL_CTRL)); + selectList.push_back(std::make_pair(PS2TBL_PS2CTRL_SEL_CAPS, PS2CTRL_CAPS)); + selectList.push_back(std::make_pair(PS2TBL_PS2CTRL_SEL_KANA, PS2CTRL_KANA)); + selectList.push_back(std::make_pair(PS2TBL_PS2CTRL_SEL_GRAPH, PS2CTRL_GRAPH)); + selectList.push_back(std::make_pair(PS2TBL_PS2CTRL_SEL_GUI, PS2CTRL_GUI)); + selectList.push_back(std::make_pair(PS2TBL_PS2CTRL_SEL_FUNC, PS2CTRL_FUNC)); + selectList.push_back(std::make_pair(PS2TBL_PS2CTRL_SEL_EXACT, PS2CTRL_EXACT)); + } + else if(option.compare(PS2TBL_KEYBOARDMODEL_TYPE) == 0) + { + selectList.push_back(std::make_pair(KEYMAP_SEL_STANDARD, KEYMAP_STANDARD)); + selectList.push_back(std::make_pair(KEYMAP_SEL_UK_WYSE_KB3926, KEYMAP_UK_WYSE_KB3926)); + selectList.push_back(std::make_pair(KEYMAP_SEL_JAPAN_OADG109, KEYMAP_JAPAN_OADG109)); + selectList.push_back(std::make_pair(KEYMAP_SEL_JAPAN_SANWA_SKBL1, KEYMAP_JAPAN_SANWA_SKBL1)); + selectList.push_back(std::make_pair(KEYMAP_SEL_NOT_ASSIGNED_4, KEYMAP_NOT_ASSIGNED_4)); + selectList.push_back(std::make_pair(KEYMAP_SEL_NOT_ASSIGNED_5, KEYMAP_NOT_ASSIGNED_5)); + selectList.push_back(std::make_pair(KEYMAP_SEL_NOT_ASSIGNED_6, KEYMAP_NOT_ASSIGNED_6)); + selectList.push_back(std::make_pair(KEYMAP_SEL_UK_PERIBOARD_810, KEYMAP_UK_PERIBOARD_810)); + selectList.push_back(std::make_pair(KEYMAP_SEL_UK_OMOTON_K8508, KEYMAP_UK_OMOTON_K8508)); + } + else if(option.compare(PS2TBL_MACHINE_TYPE) == 0) + { + selectList.push_back(std::make_pair(X1_SEL_ALL, X1_ALL)); + selectList.push_back(std::make_pair(X1_SEL_ORIG, X1_ORIG)); + selectList.push_back(std::make_pair(X1_SEL_TURBO, X1_TURBO)); + selectList.push_back(std::make_pair(X1_SEL_TURBOZ, X1_TURBOZ)); + } + else if(option.compare(PS2TBL_X1MODE_TYPE) == 0) + { + selectList.push_back(std::make_pair(X1_SEL_MODE_A, X1_MODE_A)); + selectList.push_back(std::make_pair(X1_SEL_MODE_B, X1_MODE_B)); + } + else if(option.compare(PS2TBL_X1CTRL_TYPE) == 0) + { + selectList.push_back(std::make_pair(X1_CTRL_SEL_TENKEY, X1_CTRL_TENKEY)); + selectList.push_back(std::make_pair(X1_CTRL_SEL_PRESS, X1_CTRL_PRESS)); + selectList.push_back(std::make_pair(X1_CTRL_SEL_REPEAT, X1_CTRL_REPEAT)); + selectList.push_back(std::make_pair(X1_CTRL_SEL_GRAPH, X1_CTRL_GRAPH)); + selectList.push_back(std::make_pair(X1_CTRL_SEL_CAPS, X1_CTRL_CAPS)); + selectList.push_back(std::make_pair(X1_CTRL_SEL_KANA, X1_CTRL_KANA)); + selectList.push_back(std::make_pair(X1_CTRL_SEL_SHIFT, X1_CTRL_SHIFT)); + selectList.push_back(std::make_pair(X1_CTRL_SEL_CTRL, X1_CTRL_CTRL)); + } else + { + // Not found! + result = false; + } + + // Return result, false if the option not found, true otherwise. + // + return(result); +} + + +// Method to read the Keymap array, 1 row at a time and return it to the caller. +// +bool X1::getKeyMapData(std::vector& dataArray, int *row, bool start) +{ + // Locals. + // + bool result = false; + + // If start flag is set, set row to 0. + if(start == true) + { + (*row) = 0; + } + + // Bound check and if still valid, push data onto the vector. + if((*row) >= x1Control.kmeRows) + { + result = true; + } else + { + dataArray.push_back(x1Control.kme[*row].ps2KeyCode); + dataArray.push_back(x1Control.kme[*row].ps2Ctrl); + dataArray.push_back(x1Control.kme[*row].keyboardModel); + dataArray.push_back(x1Control.kme[*row].machine); + dataArray.push_back(x1Control.kme[*row].x1Mode); + dataArray.push_back(x1Control.kme[*row].x1Key); + dataArray.push_back(x1Control.kme[*row].x1Key2); + dataArray.push_back(x1Control.kme[*row].x1Ctrl); + (*row) = (*row) + 1; + } + + // True if no more rows, false if additional rows can be read. + return(result); +} + + +// Initialisation routine. Start two threads, one to handle the incoming PS/2 keyboard data and map it, the second to handle the host interface. +void X1::init(uint32_t ifMode, NVS *hdlNVS, LED *hdlLED, HID *hdlHID) +{ + // Call the more basic initialisation. + init(hdlNVS, hdlHID); + + // Invoke the prototype init which initialises common variables and devices shared by all subclass. + KeyInterface::init(getClassName(__PRETTY_FUNCTION__), hdlNVS, hdlLED, hdlHID, ifMode); + + // Create a task pinned to core 1 which will fulfill the Sharp X1 interface. This task has the highest priority + // and it will also hold spinlock and manipulate the watchdog to ensure a scan cycle timing can be met. This means + // all other tasks running on Core 1 will suspend as needed. The PS/2 controller will be serviced with core 0. + // + // Core 1 - X1 Interface + ESP_LOGW(MAINTAG, "Starting x1if thread..."); + ::xTaskCreatePinnedToCore(&this->x1Interface, "x1if", 4096, this, 25, &this->TaskHostIF, 1); + vTaskDelay(500); + + // Core 0 - Application + // HID Interface handler thread. + ESP_LOGW(MAINTAG, "Starting hidIf thread..."); + ::xTaskCreatePinnedToCore(&this->hidInterface, "hidIf", 8192, this, 22, &this->TaskHIDIF, 0); + + // Create queue for buffering incoming keys prior to transmitting to the X1. + xmitQueue = xQueueCreate(MAX_X1_XMIT_KEY_BUF, sizeof(t_xmitQueueMessage)); +} + +// Initialisation routine without hardware. +void X1::init(NVS *hdlNVS, HID *hdlHID) +{ + // Initialise control variables. + this->x1Control.keyCtrl = 0xFF; // Negative logic, 0 - active, 1 = inactive. + x1Control.modeB = false; + x1Control.optionSelect = false; + x1Control.keyMapFileName = x1Control.fsPath.append("/").append(X1IF_KEYMAP_FILE); + x1Control.kmeRows = 0; + x1Control.kme = NULL; + x1Control.persistConfig = false; + + // Invoke the prototype init which initialises common variables and devices shared by all subclass. + KeyInterface::init(getClassName(__PRETTY_FUNCTION__), hdlNVS, hdlHID); + + // Load the keyboard mapping table into memory. If the file doesnt exist, create it. + loadKeyMap(); + + // Retrieve configuration, if it doesnt exist, set defaults. + // + if(nvs->retrieveData(getClassName(__PRETTY_FUNCTION__), &this->x1Config, sizeof(t_x1Config)) == false) + { + ESP_LOGW(MAINTAG, "X1 configuration set to default, no valid config in NVS found."); + x1Config.params.activeKeyboardMap = KEYMAP_STANDARD; + x1Config.params.activeMachineModel = X1_ALL; + + // Persist the data for next time. + if(nvs->persistData(getClassName(__PRETTY_FUNCTION__), &this->x1Config, sizeof(t_x1Config)) == false) + { + ESP_LOGW(MAINTAG, "Persisting Default X1 configuration data failed, check NVS setup.\n"); + } + // Few other updates so make a commit here to ensure data is flushed and written. + else if(this->nvs->commitData() == false) + { + ESP_LOGW(SELOPTTAG, "NVS Commit writes operation failed, some previous writes may not persist in future power cycles."); + } + } +} + +// Constructor, basically initialise the Singleton interface and let the threads loose. +X1::X1(uint32_t ifMode, NVS *hdlNVS, LED *hdlLED, HID *hdlHID, const char* fsPath) +{ + // Setup the default path on the underlying filesystem. + this->x1Control.fsPath = fsPath; + + // Initialise the interface. + init(ifMode, hdlNVS, hdlLED, hdlHID); +} + +// Constructor, initialise the Singleton interface without hardware. +X1::X1(NVS *hdlNVS, HID *hdlHID, const char* fsPath) +{ + // Setup the default path on the underlying filesystem. + this->x1Control.fsPath = fsPath; + + // Initialise the interface. + init(hdlNVS, hdlHID); +} + +// Constructor, used for version reporting so no hardware is initialised. +X1::X1(void) +{ + return; +} + +// Destructor - only ever called when the class is used for version reporting. +X1::~X1(void) +{ + return; +} diff --git a/main/X68K.cpp b/main/X68K.cpp deleted file mode 120000 index da1913b..0000000 --- a/main/X68K.cpp +++ /dev/null @@ -1 +0,0 @@ -../../sharpkey/main/X68K.cpp \ No newline at end of file diff --git a/main/X68K.cpp b/main/X68K.cpp new file mode 100644 index 0000000..e65ffd7 --- /dev/null +++ b/main/X68K.cpp @@ -0,0 +1,1067 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// Name: X68K.cpp +// Created: Mar 2022 +// Version: v1.0 +// Author(s): Philip Smart +// Description: HID (PS/2 or BT Keyboard) to Sharp X68000 Interface logic. +// This source file contains the singleton class containing logic to obtain +// PS/2 or BT scan codes, map them into Sharp X68000 keys and transmit the key to the X68000 +// host. +// +// The class uses a modified version of the PS2KeyAdvanced +// https://github.com/techpaul/PS2KeyAdvanced class from Paul Carpenter. +// +// The whole application of which this class is a member, uses the Espressif Development +// environment with Arduino components. This is necessary for the PS2KeyAdvanced class, +// which I may in future convert to use esp-idf library calls rather than Arduino. +// +// Credits: +// Copyright: (c) 2022 Philip Smart +// +// History: Mar 2022 - Initial write. +// v1.01 May 2022 - Initial release version. +// v1.02 Jun 2022 - Updates to reflect changes realised in other modules due to addition of +// bluetooth and suspend logic due to NVS issues using both cores. +// +// 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 +#include +#include +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/queue.h" +#include "driver/uart.h" +#include "driver/gpio.h" +#include "esp_log.h" +#include "soc/timer_group_struct.h" +#include "soc/timer_group_reg.h" +#include "driver/timer.h" +#include "sys/stat.h" +#include "esp_littlefs.h" +#include "PS2KeyAdvanced.h" +#include "sdkconfig.h" +#include "X68K.h" + +// Tag for ESP main application logging. +#define MAINTAG "x68kkey" + +// FreeRTOS Queue handles to pass messages from the HID Keyboard Mapper into the X68000 transmission logic +// and from the X68000 reception logic for later processing. +static QueueHandle_t xmitQueue; +static QueueHandle_t rcvQueue; + +// X68000 Protocol +// --------------- +// +// The X68000 uses an asynchronous serial protocol over two wires (RXD/TXD) with a READY state signal. +// +// Protocol: +// Idle State (RXD/TXD) = High. +// +// +// The READY signal is sent by the X68000 to the keyboard, 1 (High) = READY to accept key data, 0 (Low) = NOT READY to accept key data. +// +// DATA (KEYBOARD -> X68000): +// Bit [7] - Key MAKE (0), BREAK (1) +// Bit [6:0] - Scan Code. +// +// DATA (X68000 -> KEYBOARD): +// LED control: Set following LED's ON / OFF (0/1) +// bit [7] - Command specifier, set to "1" +// bit [6] - LED: full-width +// bit [5] - LED: Hiragana +// bit [4] - LED: INS +// bit [3] - LED: CAPS +// bit [2] - LED: Code input +// bit [1] - LED: Romaji +// bit [0] - LED: Kana +// +// Brightness Control +// bit [7:2] - Command specifier, set to "010101" +// bit [1:0] - 00 = LED's are full brightness +// 01 = slightly bright. +// 10 = slightly dark. +// 11 = dark. +// +// Set Repeat delay +// bit [7:4] - Command specifier, set to "0110" +// bit [3:0] - Delay period in formula, REPEAT DELAY = 200 + (int(bit[3:0])) * 100(ms)). Default delay = 500ms +// +// Set Repeat time +// bit [7:4] - Command specifier, set to "0111" +// bit [3:0] - Repeat time period in formula, REPEAT TIME = 30 + (((int(bit[3:0]))^2) * 5(ms)). Default repeat time = 110ms +// +// Scan Codes +// ---------- +// ,---. ,---. ,-------------------, ,-------------------. ,-----------. ,---------------. +// | 61| | 62| | 63| 64| 65| 66| 67| | 68| 69| 6A| 6B| 6C| | 5A| 5B| 5C| | 5D| 52| 53| 54| +// `---' `---' `-------------------' `-------------------' `-----------' `---------------' +// ,-----------------------------------------------------------. ,-----------. ,---------------. +// | 01| 02| 03| 04| 05| 06| 07| 08| 09| 0A| 0B| 0C| 0D| 0E| 0F| | 36| 5E| 37| | 3F| 40| 41| 42| +// |-----------------------------------------------------------| |------------ |---------------| +// | 10 | 11| 12| 13| 14| 15| 16| 17| 18| 19| 1A| 1B| 1C| | | 38| 39| 3A| | 43| 44| 45| 46| +// |------------------------------------------------------. 1D | `---=====---' |---------------| +// | 71 | 1E| 1F| 20| 21| 2l| 23| 24| 25| 26| 27| 28| 29| | ___| 3C|___ | 47| 48| 49| 4A| +// |-----------------------------------------------------------| | 3B|---| 3D| |-----------|---| +// | 70 | 2A| 2B| 2C| 2D| 2E| 2F| 30| 31| 32| 33| 34| 70 | `---| 3E|---' | 4B| 4C| 4D| | +// `-----------------------------------------------------------| .---=====---. |-----------| 4E| +// | 5F| 55 | 56 | 35 | 57 | 58 | 59 | 60| | 72 | 73 | | 4F| 50| 51| | +// `---------------------------------------------' `-----------' `---------------' +// +// ,---. ,---. ,-------------------, ,-------------------. ,-----------. ,---------------. +// |BRK| |CPY| | F1| F2| F3| F4| F5| | F6| F7| F8| F9|F10| | | | | |CAP| | |HLP| +// `---' `---' `-------------------' `-------------------' `-----------' `---------------' +// ,-----------------------------------------------------------. ,-----------. ,---------------. +// |ESC| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 0 | - | ^ | Yn| BS| |H | | | | | | | | +// |-----------------------------------------------------------| |------------ |---------------| +// | | | | | | | | | | | | | | | | | | | | | | | | +// |------------------------------------------------------. | `---=====---' |---------------| +// | | | | | | l| | | | | | | | | ___| |___ | | | | | +// |-----------------------------------------------------------| | |---| | |-----------|---| +// | | | | | | | | | | | | | | `---| |---' | | | | | +// `-----------------------------------------------------------| .---=====---. |-----------| | +// | | | | | | | | | | | | | | | | | +// `---------------------------------------------' `-----------' `---------------' + + +// Function to push a keycode onto the key queue ready for transmission. +// +IRAM_ATTR void X68K::pushKeyToQueue(uint32_t key) +{ + // Locals. + t_xmitQueueMessage xmitMsg; + #define PUSHKEYTAG "pushKeyToQueue" + + xmitMsg.keyCode = key; + if( xQueueSend(xmitQueue, (void *)&xmitMsg, 10) != pdPASS) + { + ESP_LOGW(PUSHKEYTAG, "Failed to put scancode:%04x into xmitQueue", key); + } + return; +} + +// Function to push a host command onto the processing queue. +// +IRAM_ATTR void X68K::pushHostCmdToQueue(uint8_t cmd) +{ + // Locals. + t_rcvQueueMessage rcvMsg; + #define PUSHCMDTAG "pushHostCmdToQueue" + + rcvMsg.hostCmd = cmd; + if( xQueueSend(rcvQueue, (void *)&rcvMsg, 10) != pdPASS) + { + ESP_LOGW(PUSHCMDTAG, "Failed to put host command:%02x onto rcvQueue", cmd); + } + return; +} + +// Method to interface with the X68000. +// The X68000 uses a standard 2400 baud Asynchronous Serial protocol with READY state signal. +// Data to be sent is received on the event queue filled by the PS/2 interface. Data received is pushed +// to an event queue for processing by the relevant processor. +IRAM_ATTR void X68K::x68kInterface( void * pvParameters ) +{ + // Locals. + t_xmitQueueMessage rcvMsg; + uint8_t uartData[128]; + int uartXmitCnt; + size_t uartRcvCnt; + + // Retrieve pointer to object in order to access data. + X68K* pThis = (X68K*)pvParameters; + + // Initialise the MUTEX which prevents this core from being released to other tasks. + //pThis->x68kMutex = portMUX_INITIALIZER_UNLOCKED; + + // Initial delay needed because the xQueue will assert probably on a suspended task ALL if delay not inserted! + vTaskDelay(1000); + + // Sign on. + ESP_LOGW(MAINTAG, "Starting X68000 thread."); + + // Permanent loop, wait for an incoming message on the key to send queue, read it then transmit to the X68K, repeat! + for(;;) + { + // Check stack space, report if it is getting low. + if(uxTaskGetStackHighWaterMark(NULL) < 1024) + { + ESP_LOGW(MAINTAG, "THREAD STACK SPACE(%d)\n",uxTaskGetStackHighWaterMark(NULL)); + } + + if(xQueueReceive(xmitQueue, (void *)&rcvMsg, 0) == pdTRUE) + { + //ESP_LOGW(MAINTAG, "Received:%08x\n", rcvMsg.keyCode); + + // Allow for multi byte transmissions, MSB sent first. + if(rcvMsg.keyCode != 0x00000000) + { + uartXmitCnt = 0; + while((rcvMsg.keyCode & 0xff000000) == 0x00) { rcvMsg.keyCode = rcvMsg.keyCode << 8; } + for(int idx=0; idx < 4 && (rcvMsg.keyCode & 0xff000000) != 0x00; idx++) + { + if((rcvMsg.keyCode & 0xff000000) != 0) + { + uartData[idx] = (uint8_t)((rcvMsg.keyCode & 0xFF000000) >> 24); + uartXmitCnt++; + } + rcvMsg.keyCode = rcvMsg.keyCode << 8; + } + if(uartXmitCnt > 0) + { + uart_write_bytes(pThis->x68kControl.uartNum, (const char *)uartData, uartXmitCnt); + } + } + } + + // Get data from X68000 - send any relevant commands for processing. + uart_get_buffered_data_len(pThis->x68kControl.uartNum, &uartRcvCnt); + if(uartRcvCnt > 0) + { + do { + uartRcvCnt = uart_read_bytes(pThis->x68kControl.uartNum, uartData, (128 - 1), 20 / portTICK_PERIOD_MS); + for(int idx=0; idx < uartRcvCnt; idx++) + { + // Filter out polling commands and send valid commands to the rcvQueue. + if(uartData[idx] != 0x40 && uartData[idx] != 0x41) + { + pThis->pushHostCmdToQueue(uartData[idx]); + } + } + } while(uartRcvCnt > 0); + } + + // Yield if the suspend flag is set. + pThis->yield(50); + + // Logic to feed the watchdog if needed. Watchdog disabled in menuconfig but if enabled this will need to be used. + //TIMERG0.wdt_wprotect=TIMG_WDT_WKEY_VALUE; // write enable + //TIMERG0.wdt_feed=1; // feed dog + //TIMERG0.wdt_wprotect=0; // write protect + //TIMERG1.wdt_wprotect=TIMG_WDT_WKEY_VALUE; // write enable + //TIMERG1.wdt_feed=1; // feed dog + //TIMERG1.wdt_wprotect=0; // write protect + } +} + +// Method to select keyboard configuration options. When a key sequence is pressed, ie. SHIFT+CTRL+ESC then the fourth simultaneous key is the required option and given to this +// method to act on. Options can be machine model, keyboard map etc. +// +void X68K::selectOption(uint8_t optionCode) +{ + // Locals. + // + bool updated = true; + #define SELOPTTAG "selectOption" + + // Simple switch to decode the required option and act on it. + switch(optionCode) + { + // Select a keymap using 1..8 or default (STANDARD) using 0. + case PS2_KEY_1: + this->x68kConfig.params.activeKeyboardMap = KEYMAP_UK_WYSE_KB3926; + break; + case PS2_KEY_2: + this->x68kConfig.params.activeKeyboardMap = KEYMAP_JAPAN_OADG109; + break; + case PS2_KEY_3: + this->x68kConfig.params.activeKeyboardMap = KEYMAP_JAPAN_SANWA_SKBL1; + break; + case PS2_KEY_4: + this->x68kConfig.params.activeKeyboardMap = KEYMAP_NOT_ASSIGNED_4; + break; + case PS2_KEY_5: + this->x68kConfig.params.activeKeyboardMap = KEYMAP_NOT_ASSIGNED_5; + break; + case PS2_KEY_6: + this->x68kConfig.params.activeKeyboardMap = KEYMAP_NOT_ASSIGNED_6; + break; + case PS2_KEY_7: + this->x68kConfig.params.activeKeyboardMap = KEYMAP_UK_PERIBOARD_810; + break; + case PS2_KEY_8: + this->x68kConfig.params.activeKeyboardMap = KEYMAP_UK_OMOTON_K8508; + break; + case PS2_KEY_0: + this->x68kConfig.params.activeKeyboardMap = KEYMAP_STANDARD; + break; + case PS2_KEY_END: + this->x68kConfig.params.activeMachineModel = X68K_ORIG; + break; + case PS2_KEY_DN_ARROW: + this->x68kConfig.params.activeMachineModel = X68K_ACE; + break; + case PS2_KEY_PGDN: + this->x68kConfig.params.activeMachineModel = X68K_EXPERT; + break; + case PS2_KEY_L_ARROW: + this->x68kConfig.params.activeMachineModel = X68K_PRO; + break; + case PS2_KEY_KP5: + this->x68kConfig.params.activeMachineModel = X68K_SUPER; + break; + case PS2_KEY_R_ARROW: + this->x68kConfig.params.activeMachineModel = X68K_XVI; + break; + case PS2_KEY_HOME: + this->x68kConfig.params.activeMachineModel = X68K_COMPACT; + break; + case PS2_KEY_UP_ARROW: + this->x68kConfig.params.activeMachineModel = X68K_X68030; + break; + case PS2_KEY_INSERT: + this->x68kConfig.params.activeMachineModel = X68K_ALL; + break; + + // Unknown option so ignore. + default: + updated = false; + break; + } + + // If an update was made, persist it for power cycles. + // + if(updated) + { + this->x68kControl.persistConfig = true; + } + + return; +} + +// Method to take a PS/2 key and control data and map it into an X68000 key and control equivalent, updating state values accordingly (ie. CAPS). +// A mapping table is used which maps a key and state values into an X68000 key and control values, the emphasis being on readability and easy configuration +// as opposed to concatenated byte tables. +// +uint32_t X68K::mapKey(uint16_t scanCode) +{ + // Locals. + uint32_t idx; + uint8_t keyCode = (scanCode & 0xFF); + bool mapped = false; + bool matchExact = false; + uint32_t mappedKey = 0x00000000; + #define MAPKEYTAG "mapKey" + + // Intercept control keys and set state variables. + // + // + if(scanCode & PS2_BREAK) + { + // if((keyCode == PS2_KEY_L_SHIFT || keyCode == PS2_KEY_R_SHIFT) && (scanCode & PS2_SHIFT) == 0) { mapped=true; this->x68kControl.keyCtrl |= X68KCTRL_SHIFT; } + if(keyCode == PS2_KEY_R_CTRL && (scanCode & PS2_CTRL) == 0) { mapped=true; this->x68kControl.keyCtrl &= ~X68K_CTRL_R_CTRL; } + + // Any break key clears the option select flag. + this->x68kControl.optionSelect = false; + + // Clear any feature LED blinking. + led->setLEDMode(LED::LED_MODE_OFF, LED::LED_DUTY_CYCLE_OFF, 0, 0L, 0L); + } else + { + // if((keyCode == PS2_KEY_L_SHIFT || keyCode == PS2_KEY_R_SHIFT) && (scanCode & PS2_SHIFT)) { mapped=true; this->x68kControl.keyCtrl &= ~X68KCTRL_SHIFT; } + if(keyCode == PS2_KEY_R_CTRL && (scanCode & PS2_CTRL)) { mapped=true; this->x68kControl.keyCtrl |= X68K_CTRL_R_CTRL; } + // Special mapping to allow selection of keyboard options. If the user presses CTRL+SHIFT+ESC then a flag becomes active and should a fourth key be pressed before a BREAK then the fourth key is taken as an option key and processed accordingly. + if(this->x68kControl.optionSelect == true && keyCode != PS2_KEY_ESC) + { + mapped = true; + this->x68kControl.optionSelect = false; + selectOption(keyCode); + } + if(keyCode == PS2_KEY_ESC && (scanCode & PS2_CTRL) && (scanCode & PS2_SHIFT) && this->x68kControl.optionSelect == false) + { + // Prime flag ready for fourth option key and start LED blinking periodically. + mapped = true; + this->x68kControl.optionSelect = true; + led->setLEDMode(LED::LED_MODE_BLINK, LED::LED_DUTY_CYCLE_50, 1, 500L, 500L); + } + } + + // If the key has been mapped as a special key, no further processing. + if(mapped == true) + { + ESP_LOGW(MAPKEYTAG, "Mapped special key:%02x\n", this->x68kControl.keyCtrl); + // mappedKey = (this->x68kControl.keyCtrl << 8) | 0x00; + } else + { + // Loop through the entire conversion table to find a match on this key, if found map to X68000 equivalent. + // switch matrix. + // + for(idx=0, mapped=false, matchExact=false; idx < x68kControl.kmeRows && (mapped == false || (mapped == true && matchExact == false)); idx++) + { + // Match key code? Make sure the current machine and keymap match as well. + if(x68kControl.kme[idx].ps2KeyCode == (uint8_t)(scanCode&0xFF) && ((x68kControl.kme[idx].machine == X68K_ALL) || ((x68kControl.kme[idx].machine & x68kConfig.params.activeMachineModel) != 0)) && ((x68kControl.kme[idx].keyboardModel & x68kConfig.params.activeKeyboardMap) != 0)) + { + // Match Raw, Shift, Function, Control, ALT or ALT-Gr? + if( (((x68kControl.kme[idx].ps2Ctrl & PS2CTRL_SHIFT) == 0) && ((x68kControl.kme[idx].ps2Ctrl & PS2CTRL_CTRL) == 0) && ((x68kControl.kme[idx].ps2Ctrl & PS2CTRL_R_CTRL) == 0) && ((x68kControl.kme[idx].ps2Ctrl & PS2CTRL_ALTGR) == 0) && ((x68kControl.kme[idx].ps2Ctrl & PS2CTRL_GUI) == 0) && ((x68kControl.kme[idx].ps2Ctrl & PS2CTRL_FUNC) == 0)) || + ((scanCode & PS2_SHIFT) && (x68kControl.kme[idx].ps2Ctrl & PS2CTRL_SHIFT) != 0) || + ((scanCode & PS2_CTRL) && (x68kControl.kme[idx].ps2Ctrl & PS2CTRL_CTRL) != 0) || + ((scanCode & PS2_GUI) && (x68kControl.kme[idx].ps2Ctrl & PS2CTRL_GUI) != 0) || + ((this->x68kControl.keyCtrl & X68K_CTRL_R_CTRL) && (x68kControl.kme[idx].ps2Ctrl & PS2CTRL_R_CTRL)!= 0) || + // ((scanCode & PS2_CAPS) && (x68kControl.kme[idx].ps2Ctrl & PS2CTRL_CAPS) != 0) || + ((scanCode & PS2_FUNCTION) && (x68kControl.kme[idx].ps2Ctrl & PS2CTRL_FUNC) != 0) ) + { + + // Exact entry match, data + control key? On an exact match we only process the first key. On a data only match we fall through to include additional data and control key matches to allow for un-mapped key combinations, ie. Japanese characters. + matchExact = (((scanCode & PS2_SHIFT) && (x68kControl.kme[idx].ps2Ctrl & PS2CTRL_SHIFT) != 0) || ((scanCode & PS2_SHIFT) == 0 && (x68kControl.kme[idx].ps2Ctrl & PS2CTRL_SHIFT) == 0)) && + (((scanCode & PS2_CTRL) && (x68kControl.kme[idx].ps2Ctrl & PS2CTRL_CTRL) != 0) || ((scanCode & PS2_CTRL) == 0 && (x68kControl.kme[idx].ps2Ctrl & PS2CTRL_CTRL) == 0)) && + (((scanCode & PS2_GUI) && (x68kControl.kme[idx].ps2Ctrl & PS2CTRL_GUI) != 0) || ((scanCode & PS2_GUI) == 0 && (x68kControl.kme[idx].ps2Ctrl & PS2CTRL_GUI) == 0)) && + (((this->x68kControl.keyCtrl & X68K_CTRL_R_CTRL) && (x68kControl.kme[idx].ps2Ctrl & PS2CTRL_R_CTRL)!= 0) || ((this->x68kControl.keyCtrl & X68K_CTRL_R_CTRL) == 0 && (x68kControl.kme[idx].ps2Ctrl & PS2CTRL_R_CTRL)== 0)) && + // (((scanCode & PS2_CAPS) && (x68kControl.kme[idx].ps2Ctrl & PS2CTRL_CAPS) != 0) || ((scanCode & PS2_GUI) == 0 && (x68kControl.kme[idx].ps2Ctrl & PS2CTRL_CAPS) == 0)) && + (((scanCode & PS2_FUNCTION) && (x68kControl.kme[idx].ps2Ctrl & PS2CTRL_FUNC) != 0) || ((scanCode & PS2_FUNCTION) == 0 && (x68kControl.kme[idx].ps2Ctrl & PS2CTRL_FUNC) == 0)) + ? true : false; + + // RELEASE (PS2_BREAK == 1) or PRESS? + if((scanCode & PS2_BREAK)) + { + // Special case for the PAUSE / BREAK key. The underlying logic has been modified to send a BREAK key event immediately + // after a PAUSE make, this is necessary as the Sharp machines require SHIFT (pause) BREAK so the PS/2 CTRL+BREAK wont + // work (unless logic is added to insert a SHIFT, pause, add BREAK). The solution was to generate a BREAK event + // when SHIFT+PAUSE is pressed. + if(keyCode == PS2_KEY_PAUSE) + { + vTaskDelay(100); + } + mappedKey = 0x80 | (x68kControl.kme[idx].x68kKey & 0x7F); + mapped = true; + } else + { + // Map key actioning any control overrides. + if((x68kControl.kme[idx].x68kCtrl & X68K_CTRL_RELEASESHIFT) != 0) + { + // RELEASESHIFT infers that the X68000 must cancel the current shift status prior to receiving the key code. This is necessary when using foreign keyboards and a character appears + // on a shifted key whereas on the original X68000 keyboard the character is the primary key. + // + mappedKey = ((0x80 | X68K_KEY_SHIFT) << 16) | 0x00 | ((x68kControl.kme[idx].x68kKey & 0x7F) << 8) | (0x00 | X68K_KEY_SHIFT); + } else + if((x68kControl.kme[idx].x68kCtrl & X68K_CTRL_SHIFT) != 0) + { + // SHIFT infers that the X68000 must invoke shift status prior to receiving the key code. This is necessary when using foreign keyboards and a character appears + // as a primary key on the foreign keyboard but as a shifted key on the X68000 keyboard. + // + mappedKey = ((0x00 | X68K_KEY_SHIFT) << 16) | 0x00 | ((x68kControl.kme[idx].x68kKey & 0x7F) << 8) | (0x80 | X68K_KEY_SHIFT); + } + else + { + mappedKey = 0x00 | (x68kControl.kme[idx].x68kKey & 0x7F); + } + mapped = true; + } + } + } + } + } + return(mappedKey); +} + +// Primary HID thread, running on Core 0. +// This thread is responsible for receiving HID (PS/2 or BT) keyboard scan codes and mapping them to Sharp X68000 equivalent keys, updating state flags as needed. +// The HID data is received via interrupt. The data to be sent to the X68000 is pushed onto a FIFO queue. +// +IRAM_ATTR void X68K::hidInterface( void * pvParameters ) +{ + // Locals. + uint16_t scanCode = 0x0000; + uint32_t x68kKey = 0x00000000; + t_rcvQueueMessage rcvMsg; + + // Map the instantiating object so we can access its methods and data. + X68K* pThis = (X68K*)pvParameters; + + // Thread never exits, just polls the keyboard and updates the matrix. + while(1) + { + // Check stack space, report if it is getting low. + if(uxTaskGetStackHighWaterMark(NULL) < 1024) + { + ESP_LOGW(MAINTAG, "THREAD STACK SPACE(%d)\n",uxTaskGetStackHighWaterMark(NULL)); + } + + // Check for HID keyboard scan codes. + while((scanCode = pThis->hid->read()) != 0) + { + // Scan Code Breakdown: + // Define name bit description + // PS2_BREAK 15 1 = Break key code + // (MSB) 0 = Make Key code + // PS2_SHIFT 14 1 = Shift key pressed as well (either side) + // 0 = No shift key + // PS2_CTRL 13 1 = Ctrl key pressed as well (either side) + // 0 = No Ctrl key + // PS2_CAPS 12 1 = Caps Lock ON + // 0 = Caps lock OFF + // PS2_ALT 11 1 = Left Alt key pressed as well + // 0 = No Left Alt key + // PS2_ALT_GR 10 1 = Right Alt (Alt GR) key pressed as well + // 0 = No Right Alt key + // PS2_GUI 9 1 = GUI key pressed as well (either) + // 0 = No GUI key + // PS2_FUNCTION 8 1 = FUNCTION key non-printable character (plus space, tab, enter) + // 0 = standard character key + // 7-0 PS/2 Key code. + // + // BREAK code means all keys released so clear out flags and send update. + ESP_LOGW(MAPKEYTAG, "SCANCODE:%04x",scanCode); + + // Map the PS/2 key to an X68000 CTRL + KEY + x68kKey = pThis->mapKey(scanCode); + if(x68kKey != 0L) + { + pThis->pushKeyToQueue(x68kKey); + } + + // Toggle LED to indicate data flow. + if((scanCode & PS2_BREAK) == 0) + pThis->led->setLEDMode(LED::LED_MODE_BLINK_ONESHOT, LED::LED_DUTY_CYCLE_10, 1, 100L, 0L); + } + + // Check for incoming host keyboard commands and execute them. + if(xQueueReceive(rcvQueue, (void *)&rcvMsg, 0) == pdTRUE) + { + ESP_LOGD(MAINTAG, "Received Host Cmd:%02x\n", rcvMsg.hostCmd); + } + + // NVS writes require both CPU cores to be free so write config out at a known junction. + if(pThis->x68kControl.persistConfig == true) + { + // Request and wait for the interface to suspend. This ensures that the host cpu is not held in a spinlock when NVS update is requested avoiding deadlock. + pThis->suspendInterface(true); + pThis->isSuspended(true); + + if(pThis->nvs->persistData(pThis->getClassName(__PRETTY_FUNCTION__), &pThis->x68kConfig, sizeof(t_x68kConfig)) == false) + { + ESP_LOGW(SELOPTTAG, "Persisting X68000 configuration data failed, updates will not persist in future power cycles."); + pThis->led->setLEDMode(LED::LED_MODE_BLINK_ONESHOT, LED::LED_DUTY_CYCLE_10, 200, 1000L, 0L); + } else + // Few other updates so make a commit here to ensure data is flushed and written. + if(pThis->nvs->commitData() == false) + { + ESP_LOGW(SELOPTTAG, "NVS Commit writes operation failed, some previous writes may not persist in future power cycles."); + pThis->led->setLEDMode(LED::LED_MODE_BLINK_ONESHOT, LED::LED_DUTY_CYCLE_10, 200, 500L, 0L); + } + + // Release interface. + pThis->suspendInterface(false); + + // Clear flag so we dont persist in a loop. + pThis->x68kControl.persistConfig = false; + } + + // Yield if the suspend flag is set. + pThis->yield(25); + } +} + +// A method to load the keyboard mapping table into memory for use in the interface mapping logic. If no persistence file exists or an error reading persistence occurs, the keymap +// uses the internal static default. If no persistence file exists and attempt is made to create it with a copy of the inbuilt static map so that future operations all +// work with persistence such that modifications can be made. +// +bool X68K::loadKeyMap(void) +{ + // Locals. + // + bool result = false; + int fileRows = 0; + struct stat keyMapFileNameStat; + + // See if the file exists, if it does, get size so we can compute number of mapping rows. + if(stat(x68kControl.keyMapFileName.c_str(), &keyMapFileNameStat) == -1) + { + ESP_LOGW(MAINTAG, "No keymap file, using inbuilt definitions."); + } else + { + // Get number of rows in the file. + fileRows = keyMapFileNameStat.st_size/sizeof(t_keyMapEntry); + + // Subsequent reloads, delete memory prior to building new map, primarily to conserve precious resources rather than trying the memory allocation trying to realloc and then having to copy. + if(x68kControl.kme != NULL && x68kControl.kme != PS2toX68K.kme) + { + delete x68kControl.kme; + x68kControl.kme = NULL; + } + + // Allocate memory for the new keymap table. + x68kControl.kme = new t_keyMapEntry[fileRows]; + if(x68kControl.kme == NULL) + { + ESP_LOGW(MAINTAG, "Failed to allocate memory for keyboard map, fallback to inbuilt!"); + } else + { + // Open the keymap extension file for binary reading to add data to our map table. + std::fstream keyFileIn(x68kControl.keyMapFileName.c_str(), std::ios::in | std::ios::binary); + + int idx=0; + while(keyFileIn.good()) + { + keyFileIn.read((char *)&x68kControl.kme[idx], sizeof(t_keyMapEntry)); + if(keyFileIn.good()) + { + idx++; + } + } + // Any errors, we wind back and use the inbuilt mapping table. + if(keyFileIn.bad()) + { + keyFileIn.close(); + ESP_LOGW(MAINTAG, "Failed to read data from keymap extension file:%s, fallback to inbuilt!", x68kControl.keyMapFileName.c_str()); + } else + { + // No longer need the file. + keyFileIn.close(); + + // Max rows in the KME table. + x68kControl.kmeRows = fileRows; + + // Good to go, map ready for use with the interface. + result = true; + } + } + } + + // Any failures, free up memory and use the inbuilt mapping table. + if(result == false) + { + if(x68kControl.kme != NULL && x68kControl.kme != PS2toX68K.kme) + { + delete x68kControl.kme; + x68kControl.kme = NULL; + } + + // No point allocating memory if no extensions exist or an error occurs, just point to the static table. + x68kControl.kme = PS2toX68K.kme; + x68kControl.kmeRows = PS2TBL_X68K_MAXROWS; + + // Persist the data so that next load comes from file. + saveKeyMap(); + } + + // Return code. Either memory map was successfully loaded, true or failed, false. + return(result); +} + +// Method to save the current keymap out to an extension file. +// +bool X68K::saveKeyMap(void) +{ + // Locals. + // + bool result = false; + int idx = 0; + + // Has a map been defined? Cannot save unless loadKeyMap has been called which sets x68kControl.kme to point to the internal keymap or a new memory resident map. + // + if(x68kControl.kme == NULL) + { + ESP_LOGW(MAINTAG, "KeyMap hasnt yet been defined, need to call loadKeyMap."); + } else + { + // Open file for binary writing, trunc specified to clear out the file, we arent appending. + std::fstream keyFileOut(x68kControl.keyMapFileName.c_str(), std::ios::out | std::ios::binary | std::ios::trunc); + + // Loop whilst no errors and data rows still not written. + while(keyFileOut.good() && idx < x68kControl.kmeRows) + { + keyFileOut.write((char *)&x68kControl.kme[idx], sizeof(t_keyMapEntry)); + idx++; + } + if(keyFileOut.bad()) + { + ESP_LOGW(MAINTAG, "Failed to write data from the keymap to file:%s, deleting as state is unknown!", x68kControl.keyMapFileName.c_str()); + keyFileOut.close(); + std::remove(x68kControl.keyMapFileName.c_str()); + } else + { + // Success. + keyFileOut.close(); + result = true; + } + } + + // Return code. Either memory map was successfully saved, true or failed, false. + return(result); +} + +// Public method to open a keymap file for data upload. +// This method opens the file and makes any validation checks as necessary. +// +bool X68K::createKeyMapFile(std::fstream &outFile) +{ + // Locals. + // + bool result = true; + std::string fileName; + + // Attempt to open a temporary keymap file for writing. + // + fileName = x68kControl.keyMapFileName; + replaceExt(fileName, "tmp"); + outFile.open(fileName.c_str(), std::ios::out | std::ios::binary | std::ios::trunc); + if(outFile.bad()) + { + result = false; + } + + // Send result. + return(result); +} + +// Public method to validate and store data provided by caller into an open file created by 'createKeyMapFile'. +// +bool X68K::storeDataToKeyMapFile(std::fstream &outFile, char *data, int size) +{ + // Locals. + // + bool result = true; + + // Check that the file is still writeable then add data. + if(outFile.good()) + { + outFile.write(data, size); + } + if(outFile.bad()) + { + result = false; + } + + // Send result. + return(result); +} + +// Polymorphic alternative to take a vector of bytes for writing to the output file. +// +bool X68K::storeDataToKeyMapFile(std::fstream & outFile, std::vector& dataArray) +{ + // Locals. + // + bool result = true; + char data[1]; + + // Check that the file is still writeable then add data. Not best for performace but ease of use and minimum memory. + if(outFile.good()) + { + for(std::size_t idx = 0; idx < dataArray.size(); idx++) + { + data[0] = (char)dataArray[idx]; + outFile.write((char *)&data, 1); + } + } + if(outFile.bad()) + { + result = false; + } + + // Send result. + return(result); +} + +// Public method to close and commit a data file, created by 'createKeyMapFile' and populated by 'storeDataToKeyMapFile'. +// This involves renaming the original keymap file, closing the new file and renaming it to the original keymap filename. +// +bool X68K::closeAndCommitKeyMapFile(std::fstream &outFile, bool cleanupOnly) +{ + // Locals. + // + bool result = true; + std::string fileName; + + // Check the file is still accessible and close. + // + outFile.close(); + if(!cleanupOnly) + { + if(outFile.good()) + { + // Rename the original file. + fileName = x68kControl.keyMapFileName; + replaceExt(fileName, "bak"); + // Remove old backup file. Dont worry if it is not there! + std::remove(fileName.c_str()); + replaceExt(fileName, "tmp"); + // Rename new file to active. + if(std::rename(fileName.c_str(), x68kControl.keyMapFileName.c_str()) != 0) + { + result = false; + } + } else + { + result = false; + } + } + + // Send result. + return(result); +} + +// Method to return the keymap column names as header strings. +// +void X68K::getKeyMapHeaders(std::vector& headerList) +{ + // Add the names. + // + headerList.push_back(PS2TBL_PS2KEYCODE_NAME); + headerList.push_back(PS2TBL_PS2CTRL_NAME); + headerList.push_back(PS2TBL_KEYBOARDMODEL_NAME); + headerList.push_back(PS2TBL_MACHINE_NAME); + headerList.push_back(PS2TBL_X68KKEYCODE_NAME); + headerList.push_back(PS2TBL_X68KCTRL_NAME); + + return; +} + +// A method to return the Type of data for a given column in the KeyMap table. +// +void X68K::getKeyMapTypes(std::vector& typeList) +{ + // Add the types. + // + typeList.push_back(PS2TBL_PS2KEYCODE_TYPE); + typeList.push_back(PS2TBL_PS2CTRL_TYPE); + typeList.push_back(PS2TBL_KEYBOARDMODEL_TYPE); + typeList.push_back(PS2TBL_MACHINE_TYPE); + typeList.push_back(PS2TBL_X68KKEYCODE_TYPE); + typeList.push_back(PS2TBL_X68KCTRL_TYPE); + + return; +} + +// Method to return a list of key:value entries for a given keymap column. This represents the +// feature which can be selected and the value it uses. Features can be combined by ORing the values +// together. +bool X68K::getKeyMapSelectList(std::vector>& selectList, std::string option) +{ + // Locals. + // + bool result = true; + + // Build up a map, depending on the list required, of name to value. This list can then be used + // by a user front end to select an option based on a name and return its value. + if(option.compare(PS2TBL_PS2CTRL_TYPE) == 0) + { + selectList.push_back(std::make_pair(PS2TBL_PS2CTRL_SEL_SHIFT, PS2CTRL_SHIFT)); + selectList.push_back(std::make_pair(PS2TBL_PS2CTRL_SEL_CTRL, PS2CTRL_CTRL)); + selectList.push_back(std::make_pair(PS2TBL_PS2CTRL_SEL_CAPS, PS2CTRL_CAPS)); + selectList.push_back(std::make_pair(PS2TBL_PS2CTRL_SEL_R_CTRL, PS2CTRL_R_CTRL)); + selectList.push_back(std::make_pair(PS2TBL_PS2CTRL_SEL_ALTGR, PS2CTRL_ALTGR)); + selectList.push_back(std::make_pair(PS2TBL_PS2CTRL_SEL_GUI, PS2CTRL_GUI)); + selectList.push_back(std::make_pair(PS2TBL_PS2CTRL_SEL_FUNC, PS2CTRL_FUNC)); + selectList.push_back(std::make_pair(PS2TBL_PS2CTRL_SEL_EXACT, PS2CTRL_EXACT)); + } + else if(option.compare(PS2TBL_KEYBOARDMODEL_TYPE) == 0) + { + selectList.push_back(std::make_pair(KEYMAP_SEL_STANDARD, KEYMAP_STANDARD)); + selectList.push_back(std::make_pair(KEYMAP_SEL_UK_WYSE_KB3926, KEYMAP_UK_WYSE_KB3926)); + selectList.push_back(std::make_pair(KEYMAP_SEL_JAPAN_OADG109, KEYMAP_JAPAN_OADG109)); + selectList.push_back(std::make_pair(KEYMAP_SEL_JAPAN_SANWA_SKBL1, KEYMAP_JAPAN_SANWA_SKBL1)); + selectList.push_back(std::make_pair(KEYMAP_SEL_NOT_ASSIGNED_4, KEYMAP_NOT_ASSIGNED_4)); + selectList.push_back(std::make_pair(KEYMAP_SEL_NOT_ASSIGNED_5, KEYMAP_NOT_ASSIGNED_5)); + selectList.push_back(std::make_pair(KEYMAP_SEL_NOT_ASSIGNED_6, KEYMAP_NOT_ASSIGNED_6)); + selectList.push_back(std::make_pair(KEYMAP_SEL_UK_PERIBOARD_810, KEYMAP_UK_PERIBOARD_810)); + selectList.push_back(std::make_pair(KEYMAP_SEL_UK_OMOTON_K8508, KEYMAP_UK_OMOTON_K8508)); + } + else if(option.compare(PS2TBL_MACHINE_TYPE) == 0) + { + selectList.push_back(std::make_pair(X68K_SEL_ALL, X68K_ALL)); + selectList.push_back(std::make_pair(X68K_SEL_ORIG, X68K_ORIG)); + selectList.push_back(std::make_pair(X68K_SEL_ACE, X68K_ACE)); + selectList.push_back(std::make_pair(X68K_SEL_EXPERT, X68K_EXPERT)); + selectList.push_back(std::make_pair(X68K_SEL_PRO, X68K_PRO)); + selectList.push_back(std::make_pair(X68K_SEL_SUPER, X68K_SUPER)); + selectList.push_back(std::make_pair(X68K_SEL_XVI, X68K_XVI)); + selectList.push_back(std::make_pair(X68K_SEL_COMPACT, X68K_COMPACT)); + selectList.push_back(std::make_pair(X68K_SEL_X68030, X68K_X68030)); + } + else if(option.compare(PS2TBL_X68KCTRL_TYPE) == 0) + { + selectList.push_back(std::make_pair(X68K_CTRL_SEL_SHIFT, X68K_CTRL_SHIFT)); + selectList.push_back(std::make_pair(X68K_CTRL_SEL_RELEASESHIFT, X68K_CTRL_RELEASESHIFT)); + selectList.push_back(std::make_pair(X68K_CTRL_SEL_R_CTRL, X68K_CTRL_R_CTRL)); + } else + { + // Not found! + result = false; + } + + // Return result, false if the option not found, true otherwise. + // + return(result); +} + + +// Method to read the Keymap array, 1 row at a time and return it to the caller. +// +bool X68K::getKeyMapData(std::vector& dataArray, int *row, bool start) +{ + // Locals. + // + bool result = false; + + // If start flag is set, set row to 0. + if(start == true) + { + (*row) = 0; + } + + // Bound check and if still valid, push data onto the vector. + if((*row) >= x68kControl.kmeRows) + { + result = true; + } else + { + dataArray.push_back(x68kControl.kme[*row].ps2KeyCode); + dataArray.push_back(x68kControl.kme[*row].ps2Ctrl); + dataArray.push_back(x68kControl.kme[*row].keyboardModel); + dataArray.push_back(x68kControl.kme[*row].machine); + dataArray.push_back(x68kControl.kme[*row].x68kKey); + dataArray.push_back(x68kControl.kme[*row].x68kCtrl); + (*row) = (*row) + 1; + } + + // True if no more rows, false if additional rows can be read. + return(result); +} + + +// Initialisation routine. Start two threads, one to handle the incoming PS/2 keyboard data and map it, the second to handle the host interface. +void X68K::init(uint32_t ifMode, NVS *hdlNVS, LED *hdlLED, HID *hdlHID) +{ + // Basic initialisation. + init(hdlNVS, hdlHID); + + // Invoke the prototype init which initialises common variables and devices shared by all subclass. + KeyInterface::init(getClassName(__PRETTY_FUNCTION__), hdlNVS, hdlLED, hdlHID, ifMode); + + // Prepare the UART to be used for communications with the X68000. + // The X68000 uses Asynchronous protocol with 1 stop bit no parity 2400 baud. + // + uart_config_t uartConfig = { + .baud_rate = 2400, + .data_bits = UART_DATA_8_BITS, + .parity = UART_PARITY_DISABLE, + .stop_bits = UART_STOP_BITS_1, + .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, + .rx_flow_ctrl_thresh = 122, + .source_clk = UART_SCLK_APB, + }; + + // Install UART driver. Use RX/TX buffers without event queue. + ESP_ERROR_CHECK(uart_driver_install(x68kControl.uartNum, x68kControl.uartBufferSize, x68kControl.uartBufferSize, 0, NULL, 0)); + + // Configure UART parameters and pin assignments, software flow control, not RTS/CTS. + ESP_ERROR_CHECK(uart_param_config(x68kControl.uartNum, &uartConfig)); + ESP_ERROR_CHECK(uart_set_pin(x68kControl.uartNum, CONFIG_HOST_KDB0, CONFIG_HOST_KDB1, -1, -1)); + + // Create queue for buffering incoming HID keys prior to transmitting to the X68000. + xmitQueue = xQueueCreate(MAX_X68K_XMIT_KEY_BUF, sizeof(t_xmitQueueMessage)); + // Create queue for buffering incoming X68000 data for later processing. + rcvQueue = xQueueCreate(MAX_X68K_RCV_KEY_BUF, sizeof(t_rcvQueueMessage)); + + // Create a task pinned to core 1 which will fulfill the Sharp X68000 interface. This task has the highest priority + // and it will also hold spinlock and manipulate the watchdog to ensure a scan cycle timing can be met. This means + // all other tasks running on Core 1 will suspend as needed. The HID devices will be serviced with core 0. + // + // Core 1 - X68000 Interface + ESP_LOGW(MAINTAG, "Starting x68kif thread..."); + ::xTaskCreatePinnedToCore(&this->x68kInterface, "x68kif", 4096, this, 25, &this->TaskHostIF, 1); + vTaskDelay(500); + + // Core 0 - Application + // HID Interface handler thread. + ESP_LOGW(MAINTAG, "Starting hidIf thread..."); + ::xTaskCreatePinnedToCore(&this->hidInterface, "hidIf", 8192, this, 22, &this->TaskHIDIF, 0); +} + +// Initialisation routine without hardware. +void X68K::init(NVS *hdlNVS, HID *hdlHID) +{ + // Initialise control variables. + this->x68kControl.keyCtrl = 0x00; + x68kControl.optionSelect = false; + x68kControl.uartNum = UART_NUM_2; + x68kControl.uartBufferSize = 256; + x68kControl.uartQueueSize = 10; + x68kControl.keyMapFileName = x68kControl.fsPath.append("/").append(X68KIF_KEYMAP_FILE); + x68kControl.kmeRows = 0; + x68kControl.kme = NULL; + x68kControl.persistConfig = false; + + // Invoke the prototype init which initialises common variables and devices shared by all subclass. + KeyInterface::init(getClassName(__PRETTY_FUNCTION__), hdlNVS, hdlHID); + + // Load the keyboard mapping table into memory. If the file doesnt exist, create it. + loadKeyMap(); + + // Retrieve configuration, if it doesnt exist, set defaults. + // + if(nvs->retrieveData(getClassName(__PRETTY_FUNCTION__), &this->x68kConfig, sizeof(t_x68kConfig)) == false) + { + ESP_LOGW(MAINTAG, "X68000 configuration set to default, no valid config in NVS found."); + x68kConfig.params.activeKeyboardMap = KEYMAP_STANDARD; + x68kConfig.params.activeMachineModel = X68K_ALL; + + // Persist the data for next time. + if(nvs->persistData(getClassName(__PRETTY_FUNCTION__), &this->x68kConfig, sizeof(t_x68kConfig)) == false) + { + ESP_LOGW(MAINTAG, "Persisting Default X68000 configuration data failed, check NVS setup.\n"); + } + // Few other updates so make a commit here to ensure data is flushed and written. + else if(this->nvs->commitData() == false) + { + ESP_LOGW(MAINTAG, "NVS Commit writes operation failed, some previous writes may not persist in future power cycles."); + } + } +} + +// Constructor, basically initialise the Singleton interface and let the threads loose. +X68K::X68K(uint32_t ifMode, NVS *hdlNVS, LED *hdlLED, HID *hdlHID, const char* fsPath) +{ + // Setup the default path on the underlying filesystem. + this->x68kControl.fsPath = fsPath; + + // Initialise the interface. + init(ifMode, hdlNVS, hdlLED, hdlHID); +} + +// Constructor, basic initialisation without hardware. +X68K::X68K(NVS *hdlNVS, HID *hdlHID, const char* fsPath) +{ + // Setup the default path on the underlying filesystem. + this->x68kControl.fsPath = fsPath; + + // Initialise the interface. + init(hdlNVS, hdlHID); +} + +// Constructor, used for version reporting so no hardware is initialised. +X68K::X68K(void) +{ + return; +} + +// Destructor - only ever called when the class is used for version reporting. +X68K::~X68K(void) +{ + return; +} diff --git a/main/component.mk b/main/component.mk deleted file mode 120000 index 9678c69..0000000 --- a/main/component.mk +++ /dev/null @@ -1 +0,0 @@ -../../sharpkey/main/component.mk \ No newline at end of file diff --git a/main/component.mk b/main/component.mk new file mode 100644 index 0000000..61f8990 --- /dev/null +++ b/main/component.mk @@ -0,0 +1,8 @@ +# +# Main component makefile. +# +# This Makefile can be left empty. By default, it will take the sources in the +# src/ directory, compile them and link them into lib(subdirectory_name).a +# in the build directory. This behaviour is entirely configurable, +# please read the ESP-IDF documents if you need to do this. +# diff --git a/main/include/BT.h b/main/include/BT.h deleted file mode 120000 index 1ffaf30..0000000 --- a/main/include/BT.h +++ /dev/null @@ -1 +0,0 @@ -../../../sharpkey/main/include/BT.h \ No newline at end of file diff --git a/main/include/BT.h b/main/include/BT.h new file mode 100644 index 0000000..5156351 --- /dev/null +++ b/main/include/BT.h @@ -0,0 +1,222 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// Name: BT.h +// Created: Jan 2022 +// Version: v1.0 +// Author(s): Philip Smart +// Description: Header file for the Bluetooth Class. +// +// Credits: +// Copyright: (c) 2022 Philip Smart +// +// History: Mar 2022 - Initial write. +// +// 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 . +///////////////////////////////////////////////////////////////////////////////////////////////////////// +#ifndef BT_H_ +#define BT_H_ + +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/semphr.h" +#include "esp_err.h" +#include "esp_log.h" +#include "esp_system.h" +#include "esp_event.h" +#include "esp_bt.h" +#include "esp_bt_defs.h" +#include "esp_bt_main.h" +#include "esp_hidh.h" +#include "esp_hid_common.h" +#include "esp_gap_bt_api.h" +#include "esp_gap_ble_api.h" + +// Bluetooth interface class. Provides Mouse and Keyboard functionality via the Bluetooth wireless interface. +class BT { + #define SIZEOF_ARRAY(a) (sizeof(a) / sizeof(*a)) + + public: + typedef void t_pairingHandler(uint32_t code, uint8_t trigger); + + // Structure to contain details of a single device forming a scanned device list. + // + typedef struct { + esp_bd_addr_t bda; + std::string name; + int8_t rssi; + esp_hid_usage_t usage; + esp_hid_transport_t transport; //BT, BLE or USB + + union { + struct { + esp_bt_cod_t cod; + esp_bt_uuid_t uuid; + } bt; + struct { + esp_ble_addr_type_t addr_type; + uint16_t appearance; + } ble; + }; + + // Display format values. + std::string deviceAddr; // MAC address of the Bluetooth device. + std::string deviceType; // BT, BLE or USB + } t_scanListItem; + + + // Prototypes. + BT(void); + virtual ~BT(void); + void getDeviceList(std::vector &scanList, int waitTime); + bool setup(t_pairingHandler *handler = nullptr); + + inline uint8_t getBatteryLevel() { return btCtrl.batteryLevel; } + inline void setBatteryLevel(uint8_t level) { btCtrl.batteryLevel = level; } + + private: + static constexpr char const *TAG = "BT"; + #ifdef CONFIG_CLASSIC_BT_ENABLED + const char *gap_bt_prop_type_names[5] = { "", "BDNAME", "COD", "RSSI", "EIR" }; + const char *bt_gap_evt_names[10] = { "DISC_RES", "DISC_STATE_CHANGED", "RMT_SRVCS", "RMT_SRVC_REC", "AUTH_CMPL", "PIN_REQ", "CFM_REQ", "KEY_NOTIF", "KEY_REQ", "READ_RSSI_DELTA" }; + #endif + const char *ble_gap_evt_names[28] = { "ADV_DATA_SET_COMPLETE", "SCAN_RSP_DATA_SET_COMPLETE", "SCAN_PARAM_SET_COMPLETE", "SCAN_RESULT", "ADV_DATA_RAW_SET_COMPLETE", + "SCAN_RSP_DATA_RAW_SET_COMPLETE", "ADV_START_COMPLETE", "SCAN_START_COMPLETE", "AUTH_CMPL", "KEY", + "SEC_REQ", "PASSKEY_NOTIF", "PASSKEY_REQ", "OOB_REQ", "LOCAL_IR", + "LOCAL_ER", "NC_REQ", "ADV_STOP_COMPLETE", "SCAN_STOP_COMPLETE", "SET_STATIC_RAND_ADDR", + "UPDATE_CONN_PARAMS", "SET_PKT_LENGTH_COMPLETE", "SET_LOCAL_PRIVACY_COMPLETE", "REMOVE_BOND_DEV_COMPLETE", "CLEAR_BOND_DEV_COMPLETE", + "GET_BOND_DEV_COMPLETE", "READ_RSSI_COMPLETE", "UPDATE_WHITELIST_COMPLETE" }; + const char *ble_addr_type_names[4] = { "PUBLIC", "RANDOM", "RPA_PUBLIC", "RPA_RANDOM" }; + + // Define possible HIDH host modes. + static const esp_bt_mode_t HIDH_IDLE_MODE = (esp_bt_mode_t) 0x00; + static const esp_bt_mode_t HIDH_BLE_MODE = (esp_bt_mode_t) 0x01; + static const esp_bt_mode_t HIDH_BT_MODE = (esp_bt_mode_t) 0x02; + static const esp_bt_mode_t HIDH_BTDM_MODE = (esp_bt_mode_t) 0x03; + + // Structure to maintain control variables. + typedef struct { + #ifdef CONFIG_CLASSIC_BT_ENABLED + std::vector btScanList; + #endif + std::vector bleScanList; + + t_pairingHandler *pairingHandler; + esp_hidh_dev_t *hidhDevHdl; + + int8_t batteryLevel; + + #ifdef CONFIG_CLASSIC_BT_ENABLED + xSemaphoreHandle bt_hidh_cb_semaphore; + #endif + xSemaphoreHandle ble_hidh_cb_semaphore; + + BT *pThis; + } t_btCtrl; + + // All control variables are stored in a struct for ease of reference. + t_btCtrl btCtrl; + + + // Prototypes. + static void processBTGapEvent(esp_bt_gap_cb_event_t event, esp_bt_gap_cb_param_t * param); + static void processBLEGapEvent(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t * param); + t_scanListItem* findValidScannedDevice(esp_bd_addr_t bda, std::vector &scanList); + void processBLEDeviceScanResult(esp_ble_gap_cb_param_t * scan_rst); + void addBLEScanDevice(esp_bd_addr_t bda, esp_ble_addr_type_t addr_type, uint16_t appearance, uint8_t *name, uint8_t name_len, int rssi); + + #ifdef CONFIG_CLASSIC_BT_ENABLED + void processBTDeviceScanResult(esp_bt_gap_cb_param_t * param); + void addBTScanDevice(esp_bd_addr_t bda, esp_bt_cod_t *cod, esp_bt_uuid_t *uuid, uint8_t *name, uint8_t name_len, int rssi); + #endif + esp_err_t scanForBLEDevices(uint32_t timeout); + esp_err_t scanForBTDevices(uint32_t timeout); + esp_err_t scanForAllDevices(uint32_t timeout, size_t *noDevices, std::vector &scanList); + void printUUID(esp_bt_uuid_t * uuid); + + const char *ble_addr_type_str(esp_ble_addr_type_t ble_addr_type) + { + if (ble_addr_type > BLE_ADDR_TYPE_RPA_RANDOM) + { + return "UNKNOWN"; + } + return ble_addr_type_names[ble_addr_type]; + } + + const char *ble_gap_evt_str(uint8_t event) + { + if (event >= SIZEOF_ARRAY(ble_gap_evt_names)) + { + return "UNKNOWN"; + } + return ble_gap_evt_names[event]; + } + + #ifdef CONFIG_CLASSIC_BT_ENABLED + const char *bt_gap_evt_str(uint8_t event) + { + if (event >= SIZEOF_ARRAY(bt_gap_evt_names)) + { + return "UNKNOWN"; + } + return bt_gap_evt_names[event]; + } + #endif + + const char *ble_key_type_str(esp_ble_key_type_t key_type) + { + const char *key_str = nullptr; + switch (key_type) + { + case ESP_LE_KEY_NONE: + key_str = "ESP_LE_KEY_NONE"; + break; + case ESP_LE_KEY_PENC: + key_str = "ESP_LE_KEY_PENC"; + break; + case ESP_LE_KEY_PID: + key_str = "ESP_LE_KEY_PID"; + break; + case ESP_LE_KEY_PCSRK: + key_str = "ESP_LE_KEY_PCSRK"; + break; + case ESP_LE_KEY_PLK: + key_str = "ESP_LE_KEY_PLK"; + break; + case ESP_LE_KEY_LLK: + key_str = "ESP_LE_KEY_LLK"; + break; + case ESP_LE_KEY_LENC: + key_str = "ESP_LE_KEY_LENC"; + break; + case ESP_LE_KEY_LID: + key_str = "ESP_LE_KEY_LID"; + break; + case ESP_LE_KEY_LCSRK: + key_str = "ESP_LE_KEY_LCSRK"; + break; + default: + key_str = "INVALID BLE KEY TYPE"; + break; + } + + return key_str; + } +}; + +#endif // BT_H_ diff --git a/main/include/BTHID.h b/main/include/BTHID.h deleted file mode 120000 index 431f68d..0000000 --- a/main/include/BTHID.h +++ /dev/null @@ -1 +0,0 @@ -../../../sharpkey/main/include/BTHID.h \ No newline at end of file diff --git a/main/include/BTHID.h b/main/include/BTHID.h new file mode 100644 index 0000000..917bceb --- /dev/null +++ b/main/include/BTHID.h @@ -0,0 +1,701 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// Name: BTHID.h +// Created: Mar 2022 +// Version: v1.0 +// Author(s): Philip Smart +// Description: Header file for the Bluetooth Keyboard Class. +// +// Credits: +// Copyright: (c) 2022 Philip Smart +// +// History: Mar 2022 - Initial write. +// Jun 2022 - Updated with latest findings. Now checks the bonded list and opens +// connections or scans for new devices if no connections exist. +// +// 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 . +///////////////////////////////////////////////////////////////////////////////////////////////////////// +#ifndef BT_KEYBOARD_H_ +#define BT_KEYBOARD_H_ + +#include +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/semphr.h" +#include "esp_err.h" +#include "esp_log.h" +#include "esp_system.h" +#include "esp_event.h" +#include "esp_bt.h" +#include "esp_bt_defs.h" +#include "esp_bt_main.h" +#include "esp_hidh.h" +#include "esp_hid_common.h" +#include "esp_gap_bt_api.h" +#include "esp_gap_ble_api.h" +#include "PS2KeyAdvanced.h" +#include "PS2Mouse.h" +#include "BT.h" + +// Keyboard is a sub-class of BT which provides methods to setup BT for use by a keyboard. +class BTHID : public BT { + // Macros. + // + #define NUMELEM(a) (sizeof(a)/sizeof(a[0])) + + // Global constants + #define MAX_KEYBOARD_DATA_BYTES 8 + #define MAX_CCONTROL_DATA_BYTES 3 + #define MAX_MOUSE_DATA_BYTES 7 + #define MAX_BT2PS2_MAP_ENTRIES 179 + #define MAX_BTMEDIA2PS2_MAP_ENTRIES 8 + + // LED's + #define BT_LED_NUMLOCK 0x01 + #define BT_LED_CAPSLOCK 0x02 + #define BT_LED_SCROLLLOCK 0x04 + + // Control keys. + #define BT_NONE 0x0000 + #define BT_CTRL_LEFT 0x0001 + #define BT_SHIFT_LEFT 0x0002 + #define BT_ALT_LEFT 0x0004 + #define BT_GUI_LEFT 0x0008 + #define BT_CTRL_RIGHT 0x0010 + #define BT_SHIFT_RIGHT 0x0020 + #define BT_ALT_RIGHT 0x0040 + #define BT_GUI_RIGHT 0x0080 + #define BT_CAPS_LOCK 0x0100 + #define BT_NUM_LOCK 0x0200 + #define BT_SCROLL_LOCK 0x0400 + #define BT_DUPLICATE 0xFFFF // Duplicate BT flags onto PS/2 flags. + + #define BT_PS2_FUNCTION 0x01 + #define BT_PS2_GUI 0x02 + #define BT_PS2_ALT_GR 0x04 + #define BT_PS2_ALT 0x08 + #define BT_PS2_CAPS 0x10 + #define BT_PS2_CTRL 0x20 + #define BT_PS2_SHIFT 0x40 + #define BT_PS2_BREAK 0x80 + + #define BT_KEY_NONE 0x00 // No key pressed + #define BT_KEY_ERR_OVF 0x01 // Keyboard Error Roll Over + // 0x02 // Keyboard POST Fail + // 0x03 // Keyboard Error Undefined + #define BT_KEY_A 0x04 // Keyboard a and A + #define BT_KEY_B 0x05 // Keyboard b and B + #define BT_KEY_C 0x06 // Keyboard c and C + #define BT_KEY_D 0x07 // Keyboard d and D + #define BT_KEY_E 0x08 // Keyboard e and E + #define BT_KEY_F 0x09 // Keyboard f and F + #define BT_KEY_G 0x0a // Keyboard g and G + #define BT_KEY_H 0x0b // Keyboard h and H + #define BT_KEY_I 0x0c // Keyboard i and I + #define BT_KEY_J 0x0d // Keyboard j and J + #define BT_KEY_K 0x0e // Keyboard k and K + #define BT_KEY_L 0x0f // Keyboard l and L + #define BT_KEY_M 0x10 // Keyboard m and M + #define BT_KEY_N 0x11 // Keyboard n and N + #define BT_KEY_O 0x12 // Keyboard o and O + #define BT_KEY_P 0x13 // Keyboard p and P + #define BT_KEY_Q 0x14 // Keyboard q and Q + #define BT_KEY_R 0x15 // Keyboard r and R + #define BT_KEY_S 0x16 // Keyboard s and S + #define BT_KEY_T 0x17 // Keyboard t and T + #define BT_KEY_U 0x18 // Keyboard u and U + #define BT_KEY_V 0x19 // Keyboard v and V + #define BT_KEY_W 0x1a // Keyboard w and W + #define BT_KEY_X 0x1b // Keyboard x and X + #define BT_KEY_Y 0x1c // Keyboard y and Y + #define BT_KEY_Z 0x1d // Keyboard z and Z + + #define BT_KEY_1 0x1e // Keyboard 1 and ! + #define BT_KEY_2 0x1f // Keyboard 2 and @ + #define BT_KEY_3 0x20 // Keyboard 3 and # + #define BT_KEY_4 0x21 // Keyboard 4 and $ + #define BT_KEY_5 0x22 // Keyboard 5 and % + #define BT_KEY_6 0x23 // Keyboard 6 and ^ + #define BT_KEY_7 0x24 // Keyboard 7 and & + #define BT_KEY_8 0x25 // Keyboard 8 and * + #define BT_KEY_9 0x26 // Keyboard 9 and ( + #define BT_KEY_0 0x27 // Keyboard 0 and ) + + #define BT_KEY_ENTER 0x28 // Keyboard Return (ENTER) + #define BT_KEY_ESC 0x29 // Keyboard ESCAPE + #define BT_KEY_BACKSPACE 0x2a // Keyboard DELETE (Backspace) + #define BT_KEY_TAB 0x2b // Keyboard Tab + #define BT_KEY_SPACE 0x2c // Keyboard Spacebar + #define BT_KEY_MINUS 0x2d // Keyboard - and _ + #define BT_KEY_EQUAL 0x2e // Keyboard = and + + #define BT_KEY_LEFTBRACE 0x2f // Keyboard [ and { + #define BT_KEY_RIGHTBRACE 0x30 // Keyboard ] and } + #define BT_KEY_BACKSLASH 0x31 // Keyboard \ and | + #define BT_KEY_HASHTILDE 0x32 // Keyboard Non-US # and ~ + #define BT_KEY_SEMICOLON 0x33 // Keyboard ; and : + #define BT_KEY_APOSTROPHE 0x34 // Keyboard ' and " + #define BT_KEY_GRAVE 0x35 // Keyboard ` and ~ + #define BT_KEY_COMMA 0x36 // Keyboard , and < + #define BT_KEY_DOT 0x37 // Keyboard . and > + #define BT_KEY_SLASH 0x38 // Keyboard / and ? + #define BT_KEY_CAPSLOCK 0x39 // Keyboard Caps Lock + + #define BT_KEY_F1 0x3a // Keyboard F1 + #define BT_KEY_F2 0x3b // Keyboard F2 + #define BT_KEY_F3 0x3c // Keyboard F3 + #define BT_KEY_F4 0x3d // Keyboard F4 + #define BT_KEY_F5 0x3e // Keyboard F5 + #define BT_KEY_F6 0x3f // Keyboard F6 + #define BT_KEY_F7 0x40 // Keyboard F7 + #define BT_KEY_F8 0x41 // Keyboard F8 + #define BT_KEY_F9 0x42 // Keyboard F9 + #define BT_KEY_F10 0x43 // Keyboard F10 + #define BT_KEY_F11 0x44 // Keyboard F11 + #define BT_KEY_F12 0x45 // Keyboard F12 + + #define BT_KEY_SYSRQ 0x46 // Keyboard Print Screen + #define BT_KEY_SCROLLLOCK 0x47 // Keyboard Scroll Lock + #define BT_KEY_PAUSE 0x48 // Keyboard Pause + #define BT_KEY_INSERT 0x49 // Keyboard Insert + #define BT_KEY_HOME 0x4a // Keyboard Home + #define BT_KEY_PAGEUP 0x4b // Keyboard Page Up + #define BT_KEY_DELETE 0x4c // Keyboard Delete Forward + #define BT_KEY_END 0x4d // Keyboard End + #define BT_KEY_PAGEDOWN 0x4e // Keyboard Page Down + #define BT_KEY_RIGHT 0x4f // Keyboard Right Arrow + #define BT_KEY_LEFT 0x50 // Keyboard Left Arrow + #define BT_KEY_DOWN 0x51 // Keyboard Down Arrow + #define BT_KEY_UP 0x52 // Keyboard Up Arrow + + #define BT_KEY_NUMLOCK 0x53 // Keyboard Num Lock and Clear + #define BT_KEY_KPSLASH 0x54 // Keypad / + #define BT_KEY_KPASTERISK 0x55 // Keypad * + #define BT_KEY_KPMINUS 0x56 // Keypad - + #define BT_KEY_KPPLUS 0x57 // Keypad + + #define BT_KEY_KPENTER 0x58 // Keypad ENTER + #define BT_KEY_KP1 0x59 // Keypad 1 and End + #define BT_KEY_KP2 0x5a // Keypad 2 and Down Arrow + #define BT_KEY_KP3 0x5b // Keypad 3 and PageDn + #define BT_KEY_KP4 0x5c // Keypad 4 and Left Arrow + #define BT_KEY_KP5 0x5d // Keypad 5 + #define BT_KEY_KP6 0x5e // Keypad 6 and Right Arrow + #define BT_KEY_KP7 0x5f // Keypad 7 and Home + #define BT_KEY_KP8 0x60 // Keypad 8 and Up Arrow + #define BT_KEY_KP9 0x61 // Keypad 9 and Page Up + #define BT_KEY_KP0 0x62 // Keypad 0 and Insert + #define BT_KEY_KPDOT 0x63 // Keypad . and Delete + + #define BT_KEY_102ND 0x64 // Keyboard Non-US \ and | + #define BT_KEY_COMPOSE 0x65 // Keyboard Application + #define BT_KEY_POWER 0x66 // Keyboard Power + #define BT_KEY_KPEQUAL 0x67 // Keypad = + + #define BT_KEY_F13 0x68 // Keyboard F13 + #define BT_KEY_F14 0x69 // Keyboard F14 + #define BT_KEY_F15 0x6a // Keyboard F15 + #define BT_KEY_F16 0x6b // Keyboard F16 + #define BT_KEY_F17 0x6c // Keyboard F17 + #define BT_KEY_F18 0x6d // Keyboard F18 + #define BT_KEY_F19 0x6e // Keyboard F19 + #define BT_KEY_F20 0x6f // Keyboard F20 + #define BT_KEY_F21 0x70 // Keyboard F21 + #define BT_KEY_F22 0x71 // Keyboard F22 + #define BT_KEY_F23 0x72 // Keyboard F23 + #define BT_KEY_F24 0x73 // Keyboard F24 + + #define BT_KEY_OPEN 0x74 // Keyboard Execute + #define BT_KEY_HELP 0x75 // Keyboard Help + #define BT_KEY_PROPS 0x76 // Keyboard Menu + #define BT_KEY_FRONT 0x77 // Keyboard Select + #define BT_KEY_STOP 0x78 // Keyboard Stop + #define BT_KEY_AGAIN 0x79 // Keyboard Again + #define BT_KEY_UNDO 0x7a // Keyboard Undo + #define BT_KEY_CUT 0x7b // Keyboard Cut + #define BT_KEY_COPY 0x7c // Keyboard Copy + #define BT_KEY_PASTE 0x7d // Keyboard Paste + #define BT_KEY_FIND 0x7e // Keyboard Find + #define BT_KEY_MUTE 0x7f // Keyboard Mute + #define BT_KEY_VOLUMEUP 0x80 // Keyboard Volume Up + #define BT_KEY_VOLUMEDOWN 0x81 // Keyboard Volume Down + // 0x82 Keyboard Locking Caps Lock + // 0x83 Keyboard Locking Num Lock + // 0x84 Keyboard Locking Scroll Lock + #define BT_KEY_KPCOMMA 0x85 // Keypad Comma + // 0x86 Keypad Equal Sign + #define BT_KEY_RO 0x87 // Keyboard International1 + #define BT_KEY_KATAKANAHIRAGANA 0x88 // Keyboard International2 + #define BT_KEY_YEN 0x89 // Keyboard International3 + #define BT_KEY_HENKAN 0x8a // Keyboard International4 + #define BT_KEY_MUHENKAN 0x8b // Keyboard International5 + #define BT_KEY_KPJPCOMMA 0x8c // Keyboard International6 + // 0x8d Keyboard International7 + // 0x8e Keyboard International8 + // 0x8f Keyboard International9 + #define BT_KEY_HANGEUL 0x90 // Keyboard LANG1 + #define BT_KEY_HANJA 0x91 // Keyboard LANG2 + #define BT_KEY_KATAKANA 0x92 // Keyboard LANG3 + #define BT_KEY_HIRAGANA 0x93 // Keyboard LANG4 + #define BT_KEY_ZENKAKUHANKAKU 0x94 // Keyboard LANG5 + // 0x95 Keyboard LANG6 + // 0x96 Keyboard LANG7 + // 0x97 Keyboard LANG8 + // 0x98 Keyboard LANG9 + // 0x99 Keyboard Alternate Erase + // 0x9a Keyboard SysReq/Attention + // 0x9b Keyboard Cancel + // 0x9c Keyboard Clear + // 0x9d Keyboard Prior + // 0x9e Keyboard Return + // 0x9f Keyboard Separator + // 0xa0 Keyboard Out + // 0xa1 Keyboard Oper + // 0xa2 Keyboard Clear/Again + // 0xa3 Keyboard CrSel/Props + // 0xa4 Keyboard ExSel + + // 0xb0 Keypad 00 + // 0xb1 Keypad 000 + // 0xb2 Thousands Separator + // 0xb3 Decimal Separator + // 0xb4 Currency Unit + // 0xb5 Currency Sub-unit + #define BT_KEY_KPLEFTPAREN 0xb6 // Keypad ( + #define BT_KEY_KPRIGHTPAREN 0xb7 // Keypad ) + // 0xb8 Keypad { + // 0xb9 Keypad } + // 0xba Keypad Tab + // 0xbb Keypad Backspace + // 0xbc Keypad A + // 0xbd Keypad B + // 0xbe Keypad C + // 0xbf Keypad D + // 0xc0 Keypad E + // 0xc1 Keypad F + // 0xc2 Keypad XOR + // 0xc3 Keypad ^ + // 0xc4 Keypad % + // 0xc5 Keypad < + // 0xc6 Keypad > + // 0xc7 Keypad & + // 0xc8 Keypad && + // 0xc9 Keypad | + // 0xca Keypad || + // 0xcb Keypad : + // 0xcc Keypad # + // 0xcd Keypad Space + // 0xce Keypad @ + // 0xcf Keypad ! + // 0xd0 Keypad Memory Store + // 0xd1 Keypad Memory Recall + // 0xd2 Keypad Memory Clear + // 0xd3 Keypad Memory Add + // 0xd4 Keypad Memory Subtract + // 0xd5 Keypad Memory Multiply + // 0xd6 Keypad Memory Divide + // 0xd7 Keypad +/- + // 0xd8 Keypad Clear + // 0xd9 Keypad Clear Entry + // 0xda Keypad Binary + // 0xdb Keypad Octal + // 0xdc Keypad Decimal + // 0xdd Keypad Hexadecimal + + #define BT_KEY_LEFTCTRL 0xe0 // Keyboard Left Control + #define BT_KEY_LEFTSHIFT 0xe1 // Keyboard Left Shift + #define BT_KEY_LEFTALT 0xe2 // Keyboard Left Alt + #define BT_KEY_LEFTMETA 0xe3 // Keyboard Left GUI + #define BT_KEY_RIGHTCTRL 0xe4 // Keyboard Right Control + #define BT_KEY_RIGHTSHIFT 0xe5 // Keyboard Right Shift + #define BT_KEY_RIGHTALT 0xe6 // Keyboard Right Alt + #define BT_KEY_RIGHTMETA 0xe7 // Keyboard Right GUI + + #define BT_KEY_MEDIA_PLAYPAUSE 0xe8 + #define BT_KEY_MEDIA_STOPCD 0xe9 + #define BT_KEY_MEDIA_PREVIOUSSONG 0xea + #define BT_KEY_MEDIA_NEXTSONG 0xeb + #define BT_KEY_MEDIA_EJECTCD 0xec + #define BT_KEY_MEDIA_VOLUMEUP 0xed + #define BT_KEY_MEDIA_VOLUMEDOWN 0xee + #define BT_KEY_MEDIA_MUTE 0xef + #define BT_KEY_MEDIA_WWW 0xf0 + #define BT_KEY_MEDIA_BACK 0xf1 + #define BT_KEY_MEDIA_FORWARD 0xf2 + #define BT_KEY_MEDIA_STOP 0xf3 + #define BT_KEY_MEDIA_FIND 0xf4 + #define BT_KEY_MEDIA_SCROLLUP 0xf5 + #define BT_KEY_MEDIA_SCROLLDOWN 0xf6 + #define BT_KEY_MEDIA_EDIT 0xf7 + #define BT_KEY_MEDIA_SLEEP 0xf8 + #define BT_KEY_MEDIA_COFFEE 0xf9 + #define BT_KEY_MEDIA_REFRESH 0xfa + #define BT_KEY_MEDIA_CALC 0xfb + + // Media key definition. On the ESP module a seperate usage type, CCONTROL is created for media keys and it delivers a 24bit word, each bit signifying a key. + #define BT_MEDIA_SEARCH 0x00200000 + #define BT_MEDIA_HOME 0x00080000 + #define BT_MEDIA_BRIGHTNESS_UP 0x00004000 + #define BT_MEDIA_BRIGHTNESS_DOWN 0x00008000 + #define BT_MEDIA_MUTE 0x00000040 + #define BT_MEDIA_VOL_DOWN 0x00000020 + #define BT_MEDIA_VOL_UP 0x00000010 + #define BT_MEDIA_TRACK_PREV 0x00000001 + + // PS2 Flag definitions. + #define PS2_FLG_NONE 0x00 // No keys active = 0 + #define PS2_FLG_SHIFT PS2_SHIFT >> 8 // Shift Key active = 1 + #define PS2_FLG_CTRL PS2_CTRL >> 8 // Ctrl Key active = 1 + #define PS2_FLG_CAPS PS2_CAPS >> 8 // CAPS active = 1 + #define PS2_FLG_ALT PS2_ALT >> 8 // ALT flag used as Right CTRL flag, active = 1 + #define PS2_FLG_ALTGR PS2_ALT_GR >> 8 // ALTGR active = 1 + #define PS2_FLG_GUI PS2_GUI >> 8 // GUI Key active = 1 + #define PS2_FLG_FUNC PS2_FUNCTION >> 8 // Special Function Keys active = 1 + #define PS2_FLG_BREAK PS2_BREAL >> 8 // BREAK Key active = 1 + + + public: + + struct KeyInfo { + uint8_t keys[MAX_KEYBOARD_DATA_BYTES]; + uint8_t length; + bool cControl; + esp_hidh_dev_t *hdlDev; + }; + + // Prototypes. + BTHID(void); + virtual ~BTHID(void); + bool setup(t_pairingHandler *handler); + bool openDevice(esp_bd_addr_t bda, esp_hid_transport_t transport, esp_ble_addr_type_t addrType); + bool closeDevice(esp_bd_addr_t bda); + void checkBTDevices(void); + bool setResolution(enum PS2Mouse::PS2_RESOLUTION resolution); + bool setScaling(enum PS2Mouse::PS2_SCALING scaling); + bool setSampleRate(enum PS2Mouse::PS2_SAMPLING rate); + void processBTKeys(void); + uint16_t getKey(uint32_t timeout = 0); + + // Method to register an object method for callback with context. + template + void setMouseDataCallback(A func_ptr, B obj_ptr) + { + btHIDCtrl.ms.mouseDataCallback = bind(func_ptr, obj_ptr, 1, std::placeholders::_1); + } + + // Template to aid in conversion of an enum to integer. + template constexpr typename std::underlying_type::type to_underlying(E e) noexcept + { + return static_cast::type>(e); + } + + private: + static constexpr char const * TAG = "BTHID"; + + // Structure to hold details of an active or post-active connection. + typedef struct { + esp_bd_addr_t bda; + esp_hid_transport_t transport; + esp_ble_addr_type_t addrType; + esp_hid_usage_t usage; + esp_hidh_dev_t *hidhDevHdl; + uint32_t nextCheckTime; + bool open; + } t_activeDev; + + // Structure to encapsulate a single key map from Bluetooth to PS/2. + typedef struct { + uint8_t btKeyCode; + uint16_t btCtrl; + uint8_t ps2KeyCode; + uint16_t ps2Ctrl; + } t_keyMapEntry; + + // Structure to encapsulate the entire static keyboard mapping table. + typedef struct { + t_keyMapEntry kme[MAX_BT2PS2_MAP_ENTRIES]; + } t_keyMap; + + // Structure to contain a media key map. + typedef struct { + uint32_t mediaKey; // 24bit Media key value. + uint8_t ps2Key; // Equivalent PS/2 key for media key. + uint16_t ps2Ctrl; // PS/2 translated control flags. + } t_mediaMapEntry; + + // Structure to encapsulate Media key mappings. + typedef struct { + t_mediaMapEntry kme[MAX_BTMEDIA2PS2_MAP_ENTRIES]; + } t_mediaKeyMap; + + // Structure to maintain control variables. + typedef struct { + // Array of active devices which connect with the SharpKey. + std::vector devices; + + // Keyboard handling. + struct { + // Queues for storing data in the 2 processing stages. + xQueueHandle rawKeyQueue; + xQueueHandle keyQueue; + + uint8_t lastKeys[MAX_KEYBOARD_DATA_BYTES]; // Required to generate a PS/2 break event when a key is released. + uint32_t lastMediaKey; // Required to detect changes in the media control keys, ie. release. + uint16_t btFlags; // Bluetooth control flags. + uint16_t ps2Flags; // PS/2 translated control flags. + uint8_t statusLED; // Keyboard LED state. + t_keyMapEntry *kme; // Pointer to the mapping array. + t_mediaMapEntry *kmeMedia; // Pointer to the media key mapping array. + int kmeRows; // Number of entries in the BT to PS/2 mapping table. + int kmeMediaRows; // Number of entries in the BT to PS/2 media key mapping table. + } kbd; + + // Mouse handling. + struct { + int resolution; // PS/2 compatible resolution (pixels per mm) setting. + int scaling; // PS/2 compatible scaling (1:1 or 2:1). + int sampleRate; // PS/2 compatible sample rate (10 .. 200). + int xDivisor; // Divisor on the X plane to scale down the 12bit BT resolution. + int yDivisor; // Divisor on the Y plane to scale down the 12bit BT resolution. + + // Callback for streaming processed mouse data to HID handler. + std::function mouseDataCallback; + } ms; + + BTHID *pThis; + } t_btHIDCtrl; + + // All control variables are stored in a struct for ease of reference. + t_btHIDCtrl btHIDCtrl; + + // Prototypes. + static void hidh_callback(void * handler_args, esp_event_base_t base, int32_t id, void * event_data); + void pushKeyToFIFO(esp_hid_usage_t src, esp_hidh_dev_t *hdlDev, uint8_t *keys, uint8_t size); + void setStatusLED(esp_hidh_dev_t *dev, uint8_t led); + void clearStatusLED(esp_hidh_dev_t *dev, uint8_t led); + uint16_t mapBTMediaToPS2(uint32_t key); + uint16_t mapBTtoPS2(uint8_t key); + inline uint32_t milliSeconds(void) + { + return( (uint32_t) (clock() ) ); + } + + // Mapping for Media keys. ESP module seperates them but not properly, some media keys are sent as normal key scancodes others as control key bit maps. + // Hence two mapping tables, one for normal scancodes and one for media codes. + t_mediaKeyMap MediaKeyToPS2 = { + { + { BT_MEDIA_SEARCH, PS2_KEY_WEB_SEARCH, PS2_FLG_NONE, }, + { BT_MEDIA_HOME, PS2_KEY_WEB_HOME, PS2_FLG_NONE, }, + { BT_MEDIA_BRIGHTNESS_UP, PS2_KEY_WEB_FORWARD, PS2_FLG_NONE, }, + { BT_MEDIA_BRIGHTNESS_DOWN, PS2_KEY_WEB_BACK, PS2_FLG_NONE, }, + { BT_MEDIA_MUTE, PS2_KEY_MUTE, PS2_FLG_NONE, }, + { BT_MEDIA_VOL_DOWN, PS2_KEY_VOL_DN, PS2_FLG_NONE, }, + { BT_MEDIA_VOL_UP, PS2_KEY_VOL_UP, PS2_FLG_NONE, }, + { BT_MEDIA_TRACK_PREV, PS2_KEY_PREV_TR, PS2_FLG_NONE, }, + }}; + + // Mapping table between BT Keyboard Scan Codes and PS/2 Keyboard Scan Codes. + // + t_keyMap BTKeyToPS2 = { + { + // Bluetooth Key Bluetooth Control, PS/2 Key PS/2 Control, + { BT_KEY_A, BT_NONE, PS2_KEY_A, PS2_FLG_NONE, }, + { BT_KEY_B, BT_NONE, PS2_KEY_B, PS2_FLG_NONE, }, + { BT_KEY_C, BT_NONE, PS2_KEY_C, PS2_FLG_NONE, }, + { BT_KEY_D, BT_NONE, PS2_KEY_D, PS2_FLG_NONE, }, + { BT_KEY_E, BT_NONE, PS2_KEY_E, PS2_FLG_NONE, }, + { BT_KEY_F, BT_NONE, PS2_KEY_F, PS2_FLG_NONE, }, + { BT_KEY_G, BT_NONE, PS2_KEY_G, PS2_FLG_NONE, }, + { BT_KEY_H, BT_NONE, PS2_KEY_H, PS2_FLG_NONE, }, + { BT_KEY_I, BT_NONE, PS2_KEY_I, PS2_FLG_NONE, }, + { BT_KEY_J, BT_NONE, PS2_KEY_J, PS2_FLG_NONE, }, + { BT_KEY_K, BT_NONE, PS2_KEY_K, PS2_FLG_NONE, }, + { BT_KEY_L, BT_NONE, PS2_KEY_L, PS2_FLG_NONE, }, + { BT_KEY_M, BT_NONE, PS2_KEY_M, PS2_FLG_NONE, }, + { BT_KEY_N, BT_NONE, PS2_KEY_N, PS2_FLG_NONE, }, + { BT_KEY_O, BT_NONE, PS2_KEY_O, PS2_FLG_NONE, }, + { BT_KEY_P, BT_NONE, PS2_KEY_P, PS2_FLG_NONE, }, + { BT_KEY_Q, BT_NONE, PS2_KEY_Q, PS2_FLG_NONE, }, + { BT_KEY_R, BT_NONE, PS2_KEY_R, PS2_FLG_NONE, }, + { BT_KEY_S, BT_NONE, PS2_KEY_S, PS2_FLG_NONE, }, + { BT_KEY_T, BT_NONE, PS2_KEY_T, PS2_FLG_NONE, }, + { BT_KEY_U, BT_NONE, PS2_KEY_U, PS2_FLG_NONE, }, + { BT_KEY_V, BT_NONE, PS2_KEY_V, PS2_FLG_NONE, }, + { BT_KEY_W, BT_NONE, PS2_KEY_W, PS2_FLG_NONE, }, + { BT_KEY_X, BT_NONE, PS2_KEY_X, PS2_FLG_NONE, }, + { BT_KEY_Y, BT_NONE, PS2_KEY_Y, PS2_FLG_NONE, }, + { BT_KEY_Z, BT_NONE, PS2_KEY_Z, PS2_FLG_NONE, }, + { BT_KEY_1, BT_NONE, PS2_KEY_1, PS2_FLG_NONE, }, + { BT_KEY_2, BT_NONE, PS2_KEY_2, PS2_FLG_NONE, }, + { BT_KEY_3, BT_NONE, PS2_KEY_3, PS2_FLG_NONE, }, + { BT_KEY_4, BT_NONE, PS2_KEY_4, PS2_FLG_NONE, }, + { BT_KEY_5, BT_NONE, PS2_KEY_5, PS2_FLG_NONE, }, + { BT_KEY_6, BT_NONE, PS2_KEY_6, PS2_FLG_NONE, }, + { BT_KEY_7, BT_NONE, PS2_KEY_7, PS2_FLG_NONE, }, + { BT_KEY_8, BT_NONE, PS2_KEY_8, PS2_FLG_NONE, }, + { BT_KEY_9, BT_NONE, PS2_KEY_9, PS2_FLG_NONE, }, + { BT_KEY_0, BT_NONE, PS2_KEY_0, PS2_FLG_NONE, }, + { BT_KEY_ENTER, BT_NONE, PS2_KEY_ENTER, PS2_FLG_NONE, }, + { BT_KEY_ESC, BT_NONE, PS2_KEY_ESC, PS2_FLG_NONE, }, + { BT_KEY_BACKSPACE, BT_NONE, PS2_KEY_BS, PS2_FLG_NONE, }, + { BT_KEY_TAB, BT_NONE, PS2_KEY_TAB, PS2_FLG_NONE, }, + { BT_KEY_SPACE, BT_NONE, PS2_KEY_SPACE, PS2_FLG_NONE, }, + { BT_KEY_MINUS, BT_NONE, PS2_KEY_MINUS, PS2_FLG_NONE, }, + { BT_KEY_EQUAL, BT_NONE, PS2_KEY_EQUAL, PS2_FLG_NONE, }, + { BT_KEY_LEFTBRACE, BT_NONE, PS2_KEY_OPEN_SQ, PS2_FLG_NONE, }, + { BT_KEY_RIGHTBRACE, BT_NONE, PS2_KEY_CLOSE_SQ, PS2_FLG_NONE, }, + { BT_KEY_BACKSLASH, BT_NONE, PS2_KEY_BACK, PS2_FLG_NONE, }, + { BT_KEY_HASHTILDE, BT_NONE, PS2_KEY_HASH, PS2_FLG_NONE, }, + { BT_KEY_SEMICOLON, BT_NONE, PS2_KEY_SEMI, PS2_FLG_NONE, }, + { BT_KEY_APOSTROPHE, BT_NONE, PS2_KEY_APOS, PS2_FLG_NONE, }, + { BT_KEY_GRAVE, BT_NONE, PS2_KEY_BTICK, PS2_FLG_NONE, }, + { BT_KEY_COMMA, BT_NONE, PS2_KEY_COMMA, PS2_FLG_NONE, }, + { BT_KEY_DOT, BT_NONE, PS2_KEY_DOT, PS2_FLG_NONE, }, + { BT_KEY_SLASH, BT_NONE, PS2_KEY_DIV, PS2_FLG_NONE, }, + { BT_KEY_CAPSLOCK, BT_NONE, PS2_KEY_CAPS, PS2_FLG_NONE, }, + { BT_KEY_F1, BT_NONE, PS2_KEY_F1, PS2_FLG_NONE, }, + { BT_KEY_F2, BT_NONE, PS2_KEY_F2, PS2_FLG_NONE, }, + { BT_KEY_F3, BT_NONE, PS2_KEY_F3, PS2_FLG_NONE, }, + { BT_KEY_F4, BT_NONE, PS2_KEY_F4, PS2_FLG_NONE, }, + { BT_KEY_F5, BT_NONE, PS2_KEY_F5, PS2_FLG_NONE, }, + { BT_KEY_F6, BT_NONE, PS2_KEY_F6, PS2_FLG_NONE, }, + { BT_KEY_F7, BT_NONE, PS2_KEY_F7, PS2_FLG_NONE, }, + { BT_KEY_F8, BT_NONE, PS2_KEY_F8, PS2_FLG_NONE, }, + { BT_KEY_F9, BT_NONE, PS2_KEY_F9, PS2_FLG_NONE, }, + { BT_KEY_F10, BT_NONE, PS2_KEY_F10, PS2_FLG_NONE, }, + { BT_KEY_F11, BT_NONE, PS2_KEY_F11, PS2_FLG_NONE, }, + { BT_KEY_F12, BT_NONE, PS2_KEY_F12, PS2_FLG_NONE, }, + { BT_KEY_SYSRQ, BT_NONE, PS2_KEY_PRTSCR, PS2_FLG_NONE, }, + { BT_KEY_SCROLLLOCK, BT_NONE, PS2_KEY_SCROLL, PS2_FLG_NONE, }, + { BT_KEY_PAUSE, BT_NONE, PS2_KEY_PAUSE, PS2_FLG_NONE, }, + { BT_KEY_INSERT, BT_NONE, PS2_KEY_INSERT, PS2_FLG_NONE, }, + { BT_KEY_HOME, BT_NONE, PS2_KEY_HOME, PS2_FLG_NONE, }, + { BT_KEY_PAGEUP, BT_NONE, PS2_KEY_PGUP, PS2_FLG_NONE, }, + { BT_KEY_DELETE, BT_NONE, PS2_KEY_DELETE, PS2_FLG_NONE, }, + { BT_KEY_END, BT_NONE, PS2_KEY_END, PS2_FLG_NONE, }, + { BT_KEY_PAGEDOWN, BT_NONE, PS2_KEY_PGDN, PS2_FLG_NONE, }, + { BT_KEY_RIGHT, BT_NONE, PS2_KEY_R_ARROW, PS2_FLG_NONE, }, + { BT_KEY_LEFT, BT_NONE, PS2_KEY_L_ARROW, PS2_FLG_NONE, }, + { BT_KEY_DOWN, BT_NONE, PS2_KEY_DN_ARROW, PS2_FLG_NONE, }, + { BT_KEY_UP, BT_NONE, PS2_KEY_UP_ARROW, PS2_FLG_NONE, }, + { BT_KEY_NUMLOCK, BT_NONE, PS2_KEY_NUM, PS2_FLG_NONE, }, + { BT_KEY_KPSLASH, BT_NONE, PS2_KEY_KP_DIV, PS2_FLG_NONE, }, + { BT_KEY_KPASTERISK, BT_NONE, PS2_KEY_KP_TIMES, PS2_FLG_NONE, }, + { BT_KEY_KPMINUS, BT_NONE, PS2_KEY_KP_MINUS, PS2_FLG_NONE, }, + { BT_KEY_KPPLUS, BT_NONE, PS2_KEY_KP_PLUS, PS2_FLG_NONE, }, + { BT_KEY_KPENTER, BT_NONE, PS2_KEY_KP_ENTER, PS2_FLG_NONE, }, + { BT_KEY_KP1, BT_NUM_LOCK, PS2_KEY_KP1, PS2_FLG_NONE, }, + { BT_KEY_KP2, BT_NUM_LOCK, PS2_KEY_KP2, PS2_FLG_NONE, }, + { BT_KEY_KP3, BT_NUM_LOCK, PS2_KEY_KP3, PS2_FLG_NONE, }, + { BT_KEY_KP4, BT_NUM_LOCK, PS2_KEY_KP4, PS2_FLG_NONE, }, + { BT_KEY_KP5, BT_NUM_LOCK, PS2_KEY_KP5, PS2_FLG_NONE, }, + { BT_KEY_KP6, BT_NUM_LOCK, PS2_KEY_KP6, PS2_FLG_NONE, }, + { BT_KEY_KP7, BT_NUM_LOCK, PS2_KEY_KP7, PS2_FLG_NONE, }, + { BT_KEY_KP8, BT_NUM_LOCK, PS2_KEY_KP8, PS2_FLG_NONE, }, + { BT_KEY_KP9, BT_NUM_LOCK, PS2_KEY_KP9, PS2_FLG_NONE, }, + { BT_KEY_KP0, BT_NUM_LOCK, PS2_KEY_KP0, PS2_FLG_NONE, }, + { BT_KEY_KPDOT, BT_NUM_LOCK, PS2_KEY_KP_DOT, PS2_FLG_NONE, }, + { BT_KEY_KP1, BT_NONE, PS2_KEY_END, PS2_FLG_NONE, }, + { BT_KEY_KP2, BT_NONE, PS2_KEY_DN_ARROW, PS2_FLG_NONE, }, + { BT_KEY_KP3, BT_NONE, PS2_KEY_PGDN, PS2_FLG_NONE, }, + { BT_KEY_KP4, BT_NONE, PS2_KEY_L_ARROW, PS2_FLG_NONE, }, + { BT_KEY_KP5, BT_NONE, 0x00, PS2_FLG_NONE, }, + { BT_KEY_KP6, BT_NONE, PS2_KEY_R_ARROW, PS2_FLG_NONE, }, + { BT_KEY_KP7, BT_NONE, PS2_KEY_HOME, PS2_FLG_NONE, }, + { BT_KEY_KP8, BT_NONE, PS2_KEY_UP_ARROW, PS2_FLG_NONE, }, + { BT_KEY_KP9, BT_NONE, PS2_KEY_PGUP, PS2_FLG_NONE, }, + { BT_KEY_KP0, BT_NONE, PS2_KEY_INSERT, PS2_FLG_NONE, }, + { BT_KEY_KPDOT, BT_NONE, PS2_KEY_DELETE, PS2_FLG_NONE, }, + { BT_KEY_102ND, BT_NONE, PS2_KEY_BACK, PS2_FLG_NONE, }, + { BT_KEY_COMPOSE, BT_NONE, PS2_KEY_MENU, PS2_FLG_NONE, }, + { BT_KEY_POWER, BT_NONE, PS2_KEY_POWER, PS2_FLG_NONE, }, + { BT_KEY_KPEQUAL, BT_NONE, PS2_KEY_KP_EQUAL, PS2_FLG_NONE, }, + { BT_KEY_F13, BT_NONE, PS2_KEY_F13, PS2_FLG_NONE, }, + { BT_KEY_F14, BT_NONE, PS2_KEY_F14, PS2_FLG_NONE, }, + { BT_KEY_F15, BT_NONE, PS2_KEY_F15, PS2_FLG_NONE, }, + { BT_KEY_F16, BT_NONE, PS2_KEY_F16, PS2_FLG_NONE, }, + { BT_KEY_F17, BT_NONE, PS2_KEY_F17, PS2_FLG_NONE, }, + { BT_KEY_F18, BT_NONE, PS2_KEY_F18, PS2_FLG_NONE, }, + { BT_KEY_F19, BT_NONE, PS2_KEY_F19, PS2_FLG_NONE, }, + { BT_KEY_F20, BT_NONE, PS2_KEY_F20, PS2_FLG_NONE, }, + { BT_KEY_F21, BT_NONE, PS2_KEY_F21, PS2_FLG_NONE, }, + { BT_KEY_F22, BT_NONE, PS2_KEY_F22, PS2_FLG_NONE, }, + { BT_KEY_F23, BT_NONE, PS2_KEY_F23, PS2_FLG_NONE, }, + { BT_KEY_F24, BT_NONE, PS2_KEY_F24, PS2_FLG_NONE, }, + { BT_KEY_OPEN, BT_NONE, 0x00, PS2_FLG_NONE, }, + { BT_KEY_HELP, BT_NONE, 0x00, PS2_FLG_NONE, }, + { BT_KEY_PROPS, BT_NONE, 0x00, PS2_FLG_NONE, }, + { BT_KEY_FRONT, BT_NONE, 0x00, PS2_FLG_NONE, }, + { BT_KEY_STOP, BT_NONE, PS2_KEY_STOP, PS2_FLG_NONE, }, + { BT_KEY_AGAIN, BT_NONE, 0x00, PS2_FLG_NONE, }, + { BT_KEY_UNDO, BT_NONE, 0x00, PS2_FLG_NONE, }, + { BT_KEY_CUT, BT_NONE, 0x00, PS2_FLG_NONE, }, + { BT_KEY_COPY, BT_NONE, 0x00, PS2_FLG_NONE, }, + { BT_KEY_PASTE, BT_NONE, 0x00, PS2_FLG_NONE, }, + { BT_KEY_FIND, BT_NONE, 0x00, PS2_FLG_NONE, }, + { BT_KEY_MUTE, BT_NONE, PS2_KEY_MUTE, PS2_FLG_NONE, }, + { BT_KEY_VOLUMEUP, BT_NONE, PS2_KEY_VOL_UP, PS2_FLG_NONE, }, + { BT_KEY_VOLUMEDOWN, BT_NONE, PS2_KEY_VOL_DN, PS2_FLG_NONE, }, + { BT_KEY_KPCOMMA, BT_NONE, PS2_KEY_KP_COMMA, PS2_FLG_NONE, }, + { BT_KEY_RO, BT_NONE, 0x00, PS2_FLG_NONE, }, + { BT_KEY_KATAKANAHIRAGANA, BT_NONE, 0x00, PS2_FLG_NONE, }, + { BT_KEY_YEN, BT_NONE, 0x00, PS2_FLG_NONE, }, + { BT_KEY_HENKAN, BT_NONE, 0x00, PS2_FLG_NONE, }, + { BT_KEY_MUHENKAN, BT_NONE, 0x00, PS2_FLG_NONE, }, + { BT_KEY_KPJPCOMMA, BT_NONE, 0x00, PS2_FLG_NONE, }, + { BT_KEY_HANGEUL, BT_NONE, 0x00, PS2_FLG_NONE, }, + { BT_KEY_HANJA, BT_NONE, 0x00, PS2_FLG_NONE, }, + { BT_KEY_KATAKANA, BT_NONE, 0x00, PS2_FLG_NONE, }, + { BT_KEY_HIRAGANA, BT_NONE, 0x00, PS2_FLG_NONE, }, + { BT_KEY_ZENKAKUHANKAKU, BT_NONE, 0x00, PS2_FLG_NONE, }, + { BT_KEY_KPLEFTPAREN, BT_NONE, 0x00, PS2_FLG_NONE, }, + { BT_KEY_KPRIGHTPAREN, BT_NONE, 0x00, PS2_FLG_NONE, }, + // Control keys. + { BT_KEY_LEFTCTRL, BT_NONE, PS2_KEY_L_CTRL, PS2_FLG_FUNC | PS2_FLG_CTRL, }, + { BT_KEY_LEFTSHIFT, BT_NONE, PS2_KEY_L_SHIFT, PS2_FLG_FUNC | PS2_FLG_SHIFT, }, + { BT_KEY_LEFTALT, BT_NONE, PS2_KEY_L_ALT, PS2_FLG_FUNC | PS2_FLG_ALT, }, + { BT_KEY_LEFTMETA, BT_NONE, PS2_KEY_L_GUI, PS2_FLG_FUNC | PS2_FLG_GUI, }, + { BT_KEY_RIGHTCTRL, BT_NONE, PS2_KEY_R_CTRL, PS2_FLG_FUNC | PS2_FLG_CTRL, }, + { BT_KEY_RIGHTSHIFT, BT_NONE, PS2_KEY_R_SHIFT, PS2_FLG_FUNC | PS2_FLG_SHIFT, }, + { BT_KEY_RIGHTALT, BT_NONE, PS2_KEY_R_ALT, PS2_FLG_FUNC | PS2_FLG_ALTGR, }, + { BT_KEY_RIGHTMETA, BT_NONE, PS2_KEY_R_GUI, PS2_FLG_FUNC | PS2_FLG_NONE, }, + // Media keys + { BT_KEY_MEDIA_PLAYPAUSE, BT_NONE, PS2_KEY_PLAY, PS2_FLG_NONE, }, + { BT_KEY_MEDIA_STOPCD, BT_NONE, PS2_KEY_STOP, PS2_FLG_NONE, }, + { BT_KEY_MEDIA_PREVIOUSSONG, BT_NONE, PS2_KEY_PREV_TR, PS2_FLG_NONE, }, + { BT_KEY_MEDIA_NEXTSONG, BT_NONE, PS2_KEY_NEXT_TR, PS2_FLG_NONE, }, + { BT_KEY_MEDIA_EJECTCD, BT_NONE, 0x00, PS2_FLG_NONE, }, + { BT_KEY_MEDIA_VOLUMEUP, BT_NONE, PS2_KEY_VOL_UP, PS2_FLG_NONE, }, + { BT_KEY_MEDIA_VOLUMEDOWN, BT_NONE, PS2_KEY_VOL_DN, PS2_FLG_NONE, }, + { BT_KEY_MEDIA_MUTE, BT_NONE, PS2_KEY_MUTE, PS2_FLG_NONE, }, + { BT_KEY_MEDIA_WWW, BT_NONE, PS2_KEY_WEB_SEARCH, PS2_FLG_NONE, }, + { BT_KEY_MEDIA_BACK, BT_NONE, PS2_KEY_WEB_BACK, PS2_FLG_NONE, }, + { BT_KEY_MEDIA_FORWARD, BT_NONE, PS2_KEY_WEB_FORWARD, PS2_FLG_NONE, }, + { BT_KEY_MEDIA_STOP, BT_NONE, PS2_KEY_WEB_STOP, PS2_FLG_NONE, }, + { BT_KEY_MEDIA_FIND, BT_NONE, PS2_KEY_WEB_SEARCH, PS2_FLG_NONE, }, + { BT_KEY_MEDIA_SCROLLUP, BT_NONE, 0x00, PS2_FLG_NONE, }, + { BT_KEY_MEDIA_SCROLLDOWN, BT_NONE, 0x00, PS2_FLG_NONE, }, + { BT_KEY_MEDIA_EDIT, BT_NONE, 0x00, PS2_FLG_NONE, }, + { BT_KEY_MEDIA_SLEEP, BT_NONE, 0x00, PS2_FLG_NONE, }, + { BT_KEY_MEDIA_COFFEE, BT_NONE, 0x00, PS2_FLG_NONE, }, + { BT_KEY_MEDIA_REFRESH, BT_NONE, 0x00, PS2_FLG_NONE, }, + { BT_KEY_MEDIA_CALC, BT_NONE, 0x00, PS2_FLG_NONE, }, + }}; +}; + +#endif // BT_KEYBOARD_H_ diff --git a/main/include/HID.h b/main/include/HID.h deleted file mode 120000 index e3155f6..0000000 --- a/main/include/HID.h +++ /dev/null @@ -1 +0,0 @@ -../../../sharpkey/main/include/HID.h \ No newline at end of file diff --git a/main/include/HID.h b/main/include/HID.h new file mode 100644 index 0000000..d545d88 --- /dev/null +++ b/main/include/HID.h @@ -0,0 +1,385 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// Name: HID.h +// Created: Mar 2022 +// Version: v1.0 +// Author(s): Philip Smart +// Description: A HID Class definition, used to instantiate differing input device classes and +// present a standard API. +// 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 . +///////////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef HID_H +#define HID_H + +#include +#include +#include +#include +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/semphr.h" +#include "esp_log.h" +#include "esp_system.h" +#include "nvs_flash.h" +#include "nvs.h" +#include "soc/timer_group_struct.h" +#include "soc/timer_group_reg.h" +#include "driver/timer.h" +#include "PS2KeyAdvanced.h" +#include "PS2Mouse.h" +#include "BTHID.h" +#include "LED.h" +#include "SWITCH.h" + +// NB: Macros definitions put inside class for clarity, they are still global scope. + +// Define a class which acts as the encapsulation object of many base classes which provide input device functionality. +class HID { + + // Macros. + // + #define NUMELEM(a) (sizeof(a)/sizeof(a[0])) + + // Constants. + #define HID_VERSION 1.02 + #define HID_MOUSE_DATA_POLL_DELAY 10 + #define MAX_MOUSE_INACTIVITY_TIME 500 * HID_MOUSE_DATA_POLL_DELAY + + // Categories of configuration possible with the mouse. These are used primarily with the web based UI for rendering selection choices. + #define HID_MOUSE_HOST_SCALING_TYPE "host_scaling" + #define HID_MOUSE_SCALING_TYPE "mouse_scaling" + #define HID_MOUSE_RESOLUTION_TYPE "mouse_resolution" + #define HID_MOUSE_SAMPLING_TYPE "mouse_sampling" + + #define HID_MOUSE_HOST_SCALING_1_1_NAME "1:1" + #define HID_MOUSE_HOST_SCALING_1_2_NAME "1:2" + #define HID_MOUSE_HOST_SCALING_1_3_NAME "1:3" + #define HID_MOUSE_HOST_SCALING_1_4_NAME "1:4" + #define HID_MOUSE_HOST_SCALING_1_5_NAME "1:5" + + // Names for the configuration value settings. + #define HID_MOUSE_RESOLUTION_1_1_NAME "1 c/mm" + #define HID_MOUSE_RESOLUTION_1_2_NAME "2 c/mm" + #define HID_MOUSE_RESOLUTION_1_4_NAME "4 c/mm" + #define HID_MOUSE_RESOLUTION_1_8_NAME "8 c/mm" + #define HID_MOUSE_SCALING_1_1_NAME "1:1" + #define HID_MOUSE_SCALING_2_1_NAME "2:1" + #define HID_MOUSE_SAMPLE_RATE_10_NAME "10 S/s" + #define HID_MOUSE_SAMPLE_RATE_20_NAME "20 S/s" + #define HID_MOUSE_SAMPLE_RATE_40_NAME "40 S/s" + #define HID_MOUSE_SAMPLE_RATE_60_NAME "60 S/s" + #define HID_MOUSE_SAMPLE_RATE_80_NAME "80 S/s" + #define HID_MOUSE_SAMPLE_RATE_100_NAME "100 S/s" + #define HID_MOUSE_SAMPLE_RATE_200_NAME "200 S/s" + + public: + // Types of devices the HID class can support. + enum HID_DEVICE_TYPES { + HID_DEVICE_TYPE_KEYBOARD = 0x00, + HID_DEVICE_TYPE_MOUSE = 0x01, + HID_DEVICE_TYPE_BLUETOOTH = 0x02, + }; + + // HID class can encapsulate many input device objects, only one at a time though. On startup the device is enumerated and then all + // functionality serves the device object. + enum HID_INPUT_DEVICE { + HID_DEVICE_PS2_KEYBOARD = 0x00, + HID_DEVICE_PS2_MOUSE = 0x01, + HID_DEVICE_BLUETOOTH = 0x02, + HID_DEVICE_BT_KEYBOARD = 0x03, + HID_DEVICE_BT_MOUSE = 0x04 + }; + + // Scaling - The host receiving mouse data may have a different resolution to that of the mouse, so we use configurable host side scaling to compensate. The mouse data received + // is scaled according to the enum setting. + enum HID_MOUSE_HOST_SCALING { + HID_MOUSE_HOST_SCALING_1_1 = 0x00, + HID_MOUSE_HOST_SCALING_1_2 = 0x01, + HID_MOUSE_HOST_SCALING_1_3 = 0x02, + HID_MOUSE_HOST_SCALING_1_4 = 0x03, + HID_MOUSE_HOST_SCALING_1_5 = 0x04, + }; + + // Resolution - the mouse can digitize movement from 1mm to 1/8mm, the default being 1/4 (ie. 1mm = 4 counts). This allows configuration for a finer or rougher + // tracking digitisation. + enum HID_MOUSE_RESOLUTION { + HID_MOUSE_RESOLUTION_1_1 = PS2Mouse::PS2_MOUSE_RESOLUTION_1_1, + HID_MOUSE_RESOLUTION_1_2 = PS2Mouse::PS2_MOUSE_RESOLUTION_1_2, + HID_MOUSE_RESOLUTION_1_4 = PS2Mouse::PS2_MOUSE_RESOLUTION_1_4, + HID_MOUSE_RESOLUTION_1_8 = PS2Mouse::PS2_MOUSE_RESOLUTION_1_8, + }; + + // Scaling - the mouse can provide linear (1:1 no scaling) or non liner (2:1 scaling) adaptation of the digitised data. This allows configuration for amplification of movements. + enum HID_MOUSE_SCALING { + HID_MOUSE_SCALING_1_1 = PS2Mouse::PS2_MOUSE_SCALING_1_1, + HID_MOUSE_SCALING_2_1 = PS2Mouse::PS2_MOUSE_SCALING_2_1, + }; + + // Sampling rate - the mouse, in streaming mode, the mouse sends with movement updates. This allows for finer or rougher digitisation of movements. The default is 100 samples per + // second and the X68000 is fixed at 100 samples per second. + enum HID_MOUSE_SAMPLING { + HID_MOUSE_SAMPLE_RATE_10 = PS2Mouse::PS2_MOUSE_SAMPLE_RATE_10, + HID_MOUSE_SAMPLE_RATE_20 = PS2Mouse::PS2_MOUSE_SAMPLE_RATE_20, + HID_MOUSE_SAMPLE_RATE_40 = PS2Mouse::PS2_MOUSE_SAMPLE_RATE_40, + HID_MOUSE_SAMPLE_RATE_60 = PS2Mouse::PS2_MOUSE_SAMPLE_RATE_60, + HID_MOUSE_SAMPLE_RATE_80 = PS2Mouse::PS2_MOUSE_SAMPLE_RATE_80, + HID_MOUSE_SAMPLE_RATE_100 = PS2Mouse::PS2_MOUSE_SAMPLE_RATE_100, + HID_MOUSE_SAMPLE_RATE_200 = PS2Mouse::PS2_MOUSE_SAMPLE_RATE_200, + }; + + // Suspend flag. When active, the interface components enter an idle state after completing there latest cycle. + bool suspend = false; + bool suspended = true; + + // Element to store mouse data in a queue. The data is actual mouse movements, any control data and private data for the actual mouse is stripped. + typedef struct { + int16_t xPos; + int16_t yPos; + uint8_t status; + uint8_t wheel; + } t_mouseMessageElement; + + // Prototypes. + HID(enum HID_DEVICE_TYPES, NVS *hdlNVS, LED *hdlLED, SWITCH *hdlSWITCH); + HID(NVS *hdlNVS); + HID(void); + virtual ~HID(void); + bool isBluetooth(void); + void enableBluetooth(void); + void disableBluetooth(void); + bool isSuspended(bool waitForSuspend); + void suspendInterface(bool suspendIf); + bool persistConfig(void); + uint16_t read(void); + void setMouseResolution(enum HID_MOUSE_RESOLUTION resolution); + void setMouseHostScaling(enum HID_MOUSE_HOST_SCALING scaling); + void setMouseScaling(enum HID_MOUSE_SCALING scaling); + void setMouseSampleRate(enum HID_MOUSE_SAMPLING sampleRate); + void btStartPairing(void); + void btCancelPairing(void); + + + // Method to register an object method for callback with context. + template + void setDataCallback(A func_ptr, B obj_ptr) + { + hidCtrl.dataCallback = bind(func_ptr, obj_ptr, std::placeholders::_1); + } + + // Method to suspend input device activity, yielding to the OS until suspend is cleared. + inline virtual void yield(uint32_t delay) + { + // If suspended, go into a permanent loop until the suspend flag is reset. + if(this->suspend) + { + // Suspend the keyboard interface. + if(hidCtrl.deviceType == HID_DEVICE_TYPE_KEYBOARD) { printf("SUSPEND\n"); ps2Keyboard->suspend(true); } + this->suspended = true; + + // Sleep while suspended. + while(this->suspend) + { + vTaskDelay(100); + } + + // Release the keyboard interface. + if(hidCtrl.deviceType == HID_DEVICE_TYPE_KEYBOARD) ps2Keyboard->suspend(false); + this->suspended = false; + } else + // Otherwise just delay by the required amount for timing and to give other threads a time slice. + { + vTaskDelay(delay); + } + return; + } + + // Method to see if the interface must enter suspend mode. + // + inline virtual bool suspendRequested(void) + { + return(this->suspend); + } + + // Helper method to identify the sub class, this is used in non volatile key management. + // Warning: This method wont work if optimisation for size is enabled on the compiler. + const char *getClassName(const std::string& prettyFunction) + { + // First find the CLASS :: METHOD seperation. + size_t colons = prettyFunction.find("::"); + + // None, then this is not a class. + if (colons == std::string::npos) + return "::"; + + // Split out the class name. + size_t begin = prettyFunction.substr(0,colons).rfind(" ") + 1; + size_t end = colons - begin; + + // Return the name. + return(prettyFunction.substr(begin,end).c_str()); + } + + // Template to aid in conversion of an enum to integer. + template constexpr typename std::underlying_type::type to_underlying(E e) noexcept + { + return static_cast::type>(e); + } + + // Method to return the class version number. + virtual float version(void) + { + return(HID_VERSION); + } + + // Method to return the name of this class. + virtual std::string ifName(void) + { + return(className); + } + + protected: + + private: + // Prototypes. + void init(const char *className, enum HID_DEVICE_TYPES deviceTypes); + bool nvsPersistData(const char *key, void *pData, uint32_t size); + bool nvsRetrieveData(const char *key, void *pData, uint32_t size); + bool nvsCommitData(void); + void checkKeyboard( void ); + bool checkPS2Keyboard( void ); + bool checkPS2Mouse( void ); + void checkMouse( void ); + void processPS2Mouse( void ); + void checkBTMouse( void ); + void mouseReceiveData(uint8_t src, PS2Mouse::MouseData mouseData); + IRAM_ATTR static void hidControl( void * pvParameters ); + static void btPairingHandler(uint32_t pid, uint8_t trigger); + inline uint32_t milliSeconds(void) + { + return( (uint32_t) (clock() ) ); + } + + + enum HOST_CONFIG_MODES { + HOST_CONFIG_OFF = 0x00, + HOST_CONFIG_SCALING = 0x01, + HOST_CONFIG_RESOLUTION = 0x02, + }; + + // Structure to maintain configuration for the HID. + // + typedef struct { + + struct { + // Mouse data Adjustment and filtering options. + // + enum HID_MOUSE_RESOLUTION resolution; + enum HID_MOUSE_SCALING scaling; + enum HID_MOUSE_SAMPLING sampleRate; + } mouse; + + struct { + // Host data for adjustment and configuration. + enum HID_MOUSE_HOST_SCALING scaling; + } host; + + struct { + // Configuration mode time period used to select configuration option. Once the middle key is held, the configuration option starts at 1, after this number of seconds + // the configuration option advances to the next configuration item, and so on... + uint16_t optionAdvanceDelay; + } params; + + } t_hidConfig; + + // Structure to maintain an active settings for HID devices. + typedef struct { + enum HID_INPUT_DEVICE hidDevice; // Active HID device, only one can be active. + enum HID_DEVICE_TYPES deviceType; // Type of device which is active. + bool ps2Active; // Flag to indicate PS/2 device is online and active. + uint32_t noEchoCount = 0L; // Echo back counter, used for testing if a keyboard is online. + TickType_t ps2CheckTimer = 0; // Check timer, used for timing periodic keyboard checks. + + // Mouse control variables. + uint32_t noValidMouseMessage = 0; + int wheelCnt = 0; + uint32_t loopTimer = 0; + bool middleKeyPressed = false; + PS2Mouse::MouseData mouseData; + + // Flag to indicate the mouse is active and online. + bool active; + + // Flag to indicate the configuration data has been updated. + bool updated; + + // Configuration mode selected when middle button pressed. + enum HOST_CONFIG_MODES configMode; + + // Mutex to block access during maintenance tasks. + SemaphoreHandle_t mutexInternal; + + // Callback for streaming input devices with data to be processed. + std::function dataCallback; + } t_hidControl; + + // Current configuration of the HID. + t_hidConfig hidConfig; + + // Variables to control the HID. + t_hidControl hidCtrl; + + // Handle to the persistent storage api. + nvs_handle_t nvsHandle; + + // Name of this class, used for NVS access. + std::string className; + + // NVS persistence object. + NVS *nvs; + + // LED activity object handle. + LED *led; + + // SWITCH object handle. + SWITCH *sw; + + // Keyboard object for PS/2 data retrieval and management. + PS2KeyAdvanced *ps2Keyboard; + + // Keyboard object for Bluetooth data retrieval and management. + BTHID *btHID; + + // Mouse object for PS/2 data retrieval and management. + PS2Mouse *ps2Mouse; + + // Thread handle for the HID control thread. + TaskHandle_t TaskHID = NULL; +}; +#endif // HID_H diff --git a/main/include/KeyInterface.h b/main/include/KeyInterface.h deleted file mode 120000 index 90f7212..0000000 --- a/main/include/KeyInterface.h +++ /dev/null @@ -1 +0,0 @@ -../../../sharpkey/main/include/KeyInterface.h \ No newline at end of file diff --git a/main/include/KeyInterface.h b/main/include/KeyInterface.h new file mode 100644 index 0000000..ba6c229 --- /dev/null +++ b/main/include/KeyInterface.h @@ -0,0 +1,203 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// Name: KeyInterface.h +// Created: Mar 2022 +// Version: v1.0 +// Author(s): Philip Smart +// Description: Virtual class definition on which all host interfaces, instantiated as a singleton, +// are based. +// Credits: +// Copyright: (c) 2019-2022 Philip Smart +// +// History: Mar 2022 - Initial write. +// v1.01 May 2022 - Initial release version. +// +// 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 . +///////////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef KEYINTERFACE_H +#define KEYINTERFACE_H + +#include +#include +#include +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_log.h" +#include "esp_system.h" +#include "soc/timer_group_struct.h" +#include "soc/timer_group_reg.h" +#include "driver/timer.h" +#include "PS2KeyAdvanced.h" +#include "PS2Mouse.h" +#include "NVS.h" +#include "LED.h" +#include "HID.h" + + +// NB: Macros definitions put inside class for clarity, they are still global scope. + +// Define a virtual class which acts as the base and specification of all super classes forming host +// interface objects. +class KeyInterface { + + // Macros. + // + #define NUMELEM(a) (sizeof(a)/sizeof(a[0])) + + // Constants. + #define KEYIF_VERSION 1.01 + + public: + // Suspend flag. When active, the interface components enter an idle state after completing there latest cycle. + bool suspend = false; + bool suspended = true; + + // NVS object. + NVS *nvs; + + // LED object. + LED *led; + + // HID object, used for keyboard input. + HID *hid; + + // Prototypes. + KeyInterface(void) {}; + virtual ~KeyInterface(void) {}; + KeyInterface(uint32_t ifMode, NVS *hdlNVS, LED *hdlLED, HID *hdlHID) { init(getClassName(__PRETTY_FUNCTION__), hdlNVS, hdlLED, hdlHID, ifMode); }; + KeyInterface(NVS *hdlNVS, HID *hdlHID) { init(getClassName(__PRETTY_FUNCTION__), hdlNVS, hdlHID); }; + void reconfigADC2Ports(bool setAsOutput); + void suspendInterface(bool suspendIf); + virtual bool isSuspended(bool waitForSuspend); + virtual bool isRunning(bool waitForRelease); + virtual void identify(void) { }; + virtual void init(const char * subClassName, NVS *hdlNVS, LED *hdlLED, HID *hdlHID, uint32_t ifMode); + virtual void init(const char * subClassName, NVS *hdlNVS, HID *hdlHID); + // Persistence. + virtual bool persistConfig(void) { return(true); } + + // Key mapping. + virtual IRAM_ATTR uint32_t mapKey(uint16_t scanCode) { return(0); }; + virtual bool createKeyMapFile(std::fstream &outFile) { return(false); }; + virtual bool storeDataToKeyMapFile(std::fstream &outFile, char *data, int size) { return(false); }; + virtual bool storeDataToKeyMapFile(std::fstream & outFile, std::vector& dataArray) { return(false); } + virtual bool closeAndCommitKeyMapFile(std::fstream &outFile, bool cleanupOnly) { return(false); }; + virtual std::string getKeyMapFileName(void) { return("nokeymap.bin"); }; + virtual void getKeyMapHeaders(std::vector& headerList) { }; + virtual void getKeyMapTypes(std::vector& typeList) { }; + virtual bool getKeyMapSelectList(std::vector>& selectList, std::string option) { return(true); } + virtual bool getKeyMapData(std::vector& dataArray, int *row, bool start) { return(true); }; + // Mouse config. + virtual void getMouseConfigTypes(std::vector& typeList) { }; + virtual bool getMouseSelectList(std::vector>& selectList, std::string option) { return(true); } + virtual bool setMouseConfigValue(std::string paramName, std::string paramValue) { return(true); } + + // Method to suspend an active interface thread by holding in a tight loop, yielding to the OS. This method was chosen rather than the more conventional + // vTaskSuspend as it allows multiple threads, without giving a handle, to yield if required for a fixed period or indefinitely until the suspend mode is de-activated. + // The method is inline to avoid a call overhead as it is generally used in time sensitive interface timing. + inline virtual void yield(uint32_t delay) + { + // If suspended, go into a permanent loop until the suspend flag is reset. + if(this->suspend) + { + this->suspended = true; + while(this->suspend) + { + vTaskDelay(100); + } + this->suspended = false; + } else + // Otherwise just delay by the required amount for timing and to give other threads a time slice. + { + vTaskDelay(delay); + } + } + + // Method to see if the interface must enter suspend mode. + // + inline virtual bool suspendRequested(void) + { + return(this->suspend); + } + + // Helper method to identify the sub class, this is used in non volatile key management. + // Warning: This method wont work if optimisation for size is enabled on the compiler. + const char *getClassName(const std::string& prettyFunction) + { + // First find the CLASS :: METHOD seperation. + size_t colons = prettyFunction.find("::"); + + // None, then this is not a class. + if (colons == std::string::npos) + return "::"; + + // Split out the class name. + size_t begin = prettyFunction.substr(0,colons).rfind(" ") + 1; + size_t end = colons - begin; + + // Return the name. + return(prettyFunction.substr(begin,end).c_str()); + } + + // Helper method to change a file extension. + void replaceExt(std::string& fileName, const std::string& newExt) + { + // Locals. + std::string::size_type extPos = fileName.rfind('.', fileName.length()); + + if(extPos != std::string::npos) + { + fileName.replace(extPos+1, newExt.length(), newExt); + } + return; + } + + // Template to aid in conversion of an enum to integer. + template constexpr typename std::underlying_type::type to_underlying(E e) noexcept + { + return static_cast::type>(e); + } + + // Method to return the class version number. + virtual float version(void) + { + return(KEYIF_VERSION); + } + + // Method to return the name of the inferface class. + virtual std::string ifName(void) + { + return(subClassName); + } + + protected: + + private: + // Prototypes. + virtual IRAM_ATTR void selectOption(uint8_t optionCode) {}; + + // Name of the sub-class for this instantiation. + std::string subClassName; + + // Thread handle for the LED control thread. + TaskHandle_t TaskLEDIF = NULL; +}; +#endif // KEYINTERFACE_H diff --git a/main/include/LED.h b/main/include/LED.h deleted file mode 120000 index d4460a3..0000000 --- a/main/include/LED.h +++ /dev/null @@ -1 +0,0 @@ -../../../sharpkey/main/include/LED.h \ No newline at end of file diff --git a/main/include/LED.h b/main/include/LED.h new file mode 100644 index 0000000..ee9ddfe --- /dev/null +++ b/main/include/LED.h @@ -0,0 +1,181 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// Name: LED.h +// Created: Mar 2022 +// Version: v1.0 +// Author(s): Philip Smart +// Description: Class definition for the control of a single LED. The LED is used to indicate to a +// user a desired status. This class is normally instantiated as a singleton and +// manipulated by public methods. +// Credits: +// Copyright: (c) 2019-2022 Philip Smart +// +// History: Mar 2022 - Initial write. +// v1.01 May 2022 - Initial release version. +// +// 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 . +///////////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef LED_H +#define LED_H + +#include +#include +#include +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/semphr.h" +#include "esp_log.h" +#include "esp_system.h" +#include "soc/timer_group_struct.h" +#include "soc/timer_group_reg.h" +#include "driver/timer.h" +#include "PS2KeyAdvanced.h" +#include "PS2Mouse.h" +#include "NVS.h" + +// NB: Macros definitions put inside class for clarity, they are still global scope. + +// Define a class to encapsulate a LED and required control mechanisms, +class LED { + + // Macros. + // + #define NUMELEM(a) (sizeof(a)/sizeof(a[0])) + + // Constants. + #define LED_VERSION 1.01 + + public: + // Interface LED activity modes. + enum LED_MODE { + LED_MODE_OFF = 0x00, + LED_MODE_ON = 0x01, + LED_MODE_BLINK_ONESHOT = 0x02, + LED_MODE_BLINK = 0x03, + }; + + // Interface LED duty cycle. + enum LED_DUTY_CYCLE { + LED_DUTY_CYCLE_OFF = 0x00, + LED_DUTY_CYCLE_10 = 0x01, + LED_DUTY_CYCLE_20 = 0x02, + LED_DUTY_CYCLE_30 = 0x03, + LED_DUTY_CYCLE_40 = 0x04, + LED_DUTY_CYCLE_50 = 0x05, + LED_DUTY_CYCLE_60 = 0x06, + LED_DUTY_CYCLE_70 = 0x07, + LED_DUTY_CYCLE_80 = 0x08, + LED_DUTY_CYCLE_90 = 0x09, + }; + + // Prototypes. + LED(uint32_t hwPin); + LED(void); + virtual ~LED(void) {}; + void identify(void) { }; + + // LED Control. + bool setLEDMode(enum LED_MODE mode, enum LED_DUTY_CYCLE dutyCycle, uint32_t maxBlinks, uint64_t usDutyPeriod, uint64_t msInterPeriod); + IRAM_ATTR static void ledInterface(void *pvParameters); + void ledInit(uint8_t ledPin); + + // Helper method to identify the sub class, this is used in non volatile key management. + // Warning: This method wont work if optimisation for size is enabled on the compiler. + const char *getClassName(const std::string& prettyFunction) + { + // First find the CLASS :: METHOD seperation. + size_t colons = prettyFunction.find("::"); + + // None, then this is not a class. + if (colons == std::string::npos) + return "::"; + + // Split out the class name. + size_t begin = prettyFunction.substr(0,colons).rfind(" ") + 1; + size_t end = colons - begin; + + // Return the name. + return(prettyFunction.substr(begin,end).c_str()); + } + + // Template to aid in conversion of an enum to integer. + template constexpr typename std::underlying_type::type to_underlying(E e) noexcept + { + return static_cast::type>(e); + } + + // Method to return the class version number. + virtual float version(void) + { + return(LED_VERSION); + } + + // Method to return the name of the inferface class. + virtual std::string ifName(void) + { + return(className); + } + + protected: + + private: + // Prototypes. + + // Structure to maintain configuration for the LED. + // + typedef struct { + bool valid; // The configuration is valid and should be processed. + bool updated; // This configuration is an update to override current configuration. + + enum LED_MODE mode; // Mode of LED activity. + enum LED_DUTY_CYCLE dutyCycle; // Duty cycle of the BLINK LED period. + uint32_t maxBlinks; // Maximum number of blinks before switching to LED off mode. + uint64_t dutyPeriod; // Period, is micro-seconds of the full duty cycle. + uint64_t interPeriod; // Period, is milli-seconds between LED activity. + } t_ledConfig; + + // Structure to maintain an active setting for the LED. The LED control thread uses these values to effect the required lighting of the LED. + typedef struct { + // Current, ie. working LED config acted upon by the LED thread. + t_ledConfig currentConfig; + // New config to replace current on next state. + t_ledConfig newConfig; + + // Led GPIO pin. + uint8_t ledPin; + + // Runtime parameters for state machine and control. + uint32_t blinkCnt; // count of blink on periods. + + // Mutex to block access to limit one thread at a time. + SemaphoreHandle_t mutexInternal; + } t_ledControl; + + // Variables to control the LED. + t_ledControl ledCtrl; + + // Name of the class for this instantiation. + std::string className; + + // Thread handle for the LED control thread. + TaskHandle_t TaskLEDIF = NULL; +}; +#endif // LED_H diff --git a/main/include/MZ2528.h b/main/include/MZ2528.h deleted file mode 120000 index df69112..0000000 --- a/main/include/MZ2528.h +++ /dev/null @@ -1 +0,0 @@ -../../../sharpkey/main/include/MZ2528.h \ No newline at end of file diff --git a/main/include/MZ2528.h b/main/include/MZ2528.h new file mode 100644 index 0000000..851d809 --- /dev/null +++ b/main/include/MZ2528.h @@ -0,0 +1,534 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// Name: MZ2528.h +// Created: Mar 2022 +// Version: v1.0 +// Author(s): Philip Smart +// Description: Header for the MZ-2500/MZ-2800 PS/2 logic. +// 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 reflect bluetooth. +// +// 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 . +///////////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef MZ2528_H +#define MZ2528_H + +// Include the specification class. +#include "KeyInterface.h" +#include "NVS.h" +#include "LED.h" +#include "HID.h" +#include +#include + +// NB: Macros definitions put inside class for clarity, they are still global scope. + +// Encapsulate the MZ-2500/MZ-2800 interface. +class MZ2528 : public KeyInterface { + // Macros. + // + #define NUMELEM(a) (sizeof(a)/sizeof(a[0])) + + // Constants. + #define MZ2528IF_VERSION 1.02 + #define MZ2528IF_KEYMAP_FILE "MZ2528_KeyMap.BIN" + #define PS2TBL_MZ_MAXROWS 165 + #define PS2TBL_MZ_MAX_MKROW 3 + #define PS2TBL_MZ_MAX_BRKROW 2 + + // PS2 Flag definitions. + #define PS2CTRL_NONE 0x00 // No keys active = 0 + #define PS2CTRL_SHIFT 0x01 // Shfit Key active = 1 + #define PS2CTRL_CTRL 0x02 // Ctrl Key active = 1 + #define PS2CTRL_CAPS 0x04 // CAPS active = 1 + #define PS2CTRL_ALT 0x08 // ALT active = 1 + #define PS2CTRL_ALTGR 0x10 // ALTGR active = 1 + #define PS2CTRL_GUI 0x20 // GUI Key active = 1 + #define PS2CTRL_FUNC 0x40 // Special Function Keys active = 1 + #define PS2CTRL_BREAK 0x80 // BREAK Key active = 1 + #define PS2CTRL_EXACT 0x80 // EXACT Match active = 1 + + // The MZ-2500 machine can emulate 3 models, the MZ-80B, MZ-2000 and the MZ-2500. The MZ-2800 provides a new mode as well as the MZ-2500 mode and each has slight + // keyboard differences. This requires tagging of machine specific mappings. Normally a mapping would be MZ_ALL, ie. applied to all models, but if a machine specific + // mapping appears and it matches the current machine mode, this mapping is chosen. + #define MZ_ALL 0xFF + #define MZ_80B 0x01 + #define MZ_2000 0x02 + #define MZ_2500 0x04 + #define MZ_2800 0x08 + + // Keyboard mapping table select list for target machine. + #define MZ2528_SEL_ALL "ALL" + #define MZ2528_SEL_MZ_80B "MZ80B" + #define MZ2528_SEL_MZ_2000 "MZ2000" + #define MZ2528_SEL_MZ_2500 "MZ2500" + #define MZ2528_SEL_MZ_2800 "MZ2800" + + // The initial mapping is made inside the PS2KeyAdvanced class from Scan Code Set 2 to ASCII + // for a selected keyboard. Special functions are detected and combined inside this module + // before mapping with the table below to MZ Scan Matrix. + // ie. PS/2 Scan Code -> ASCII + Flags -> MZ Scan Matrix + + + // Keyboard mapping table column names. + #define PS2TBL_PS2KEYCODE_NAME "PS/2 KeyCode" + #define PS2TBL_PS2CTRL_NAME "PS/2 Control Key" + #define PS2TBL_KEYBOARDMODEL_NAME "For Keyboard" + #define PS2TBL_MACHINE_NAME "For Host Model" + #define PS2TBL_MZ_MK_ROW1_NAME "Make Row 1" + #define PS2TBL_MZ_MK_KEY1_NAME "Key 1" + #define PS2TBL_MZ_MK_ROW2_NAME "Row 2" + #define PS2TBL_MZ_MK_KEY2_NAME "Key 2" + #define PS2TBL_MZ_MK_ROW3_NAME "Row 3" + #define PS2TBL_MZ_MK_KEY3_NAME "Key 3" + #define PS2TBL_MZ_BRK_ROW1_NAME "Break Row 1" + #define PS2TBL_MZ_BRK_KEY1_NAME "Key 1" + #define PS2TBL_MZ_BRK_ROW2_NAME "Row 2" + #define PS2TBL_MZ_BRK_KEY2_NAME "Key 2" + + // Keyboard mapping table column types. + #define PS2TBL_PS2KEYCODE_TYPE "hex" + #define PS2TBL_PS2CTRL_TYPE "custom_cbp_ps2ctrl" + #define PS2TBL_KEYBOARDMODEL_TYPE "custom_cbp_keybmodel" + #define PS2TBL_MACHINE_TYPE "custom_cbp_machine" + #define PS2TBL_MZ_MK_ROW1_TYPE "custom_rdp_mzrow" + #define PS2TBL_MZ_MK_KEY1_TYPE "hex" + #define PS2TBL_MZ_MK_ROW2_TYPE "custom_rdp_mzrow" + #define PS2TBL_MZ_MK_KEY2_TYPE "hex" + #define PS2TBL_MZ_MK_ROW3_TYPE "custom_rdp_mzrow" + #define PS2TBL_MZ_MK_KEY3_TYPE "hex" + #define PS2TBL_MZ_BRK_ROW1_TYPE "custom_rdp_mzrow" + #define PS2TBL_MZ_BRK_KEY1_TYPE "hex" + #define PS2TBL_MZ_BRK_ROW2_TYPE "custom_rdp_mzrow" + #define PS2TBL_MZ_BRK_KEY2_TYPE "hex" + + // Keyboard mapping table select list for PS2CTRL. + #define PS2TBL_PS2CTRL_SEL_NONE "NONE" + #define PS2TBL_PS2CTRL_SEL_SHIFT "SHIFT" + #define PS2TBL_PS2CTRL_SEL_CTRL "CTRL" + #define PS2TBL_PS2CTRL_SEL_CAPS "CAPS" + #define PS2TBL_PS2CTRL_SEL_ALT "ALT" + #define PS2TBL_PS2CTRL_SEL_ALTGR "ALTGR" + #define PS2TBL_PS2CTRL_SEL_GUI "GUI" + #define PS2TBL_PS2CTRL_SEL_FUNC "FUNC" + #define PS2TBL_PS2CTRL_SEL_EXACT "EXACT" + + // Keyboard mapping table select list for Model of keyboard. + #define KEYMAP_SEL_STANDARD "ALL" + #define KEYMAP_SEL_UK_WYSE_KB3926 "UK_WYSE_KB3926" + #define KEYMAP_SEL_JAPAN_OADG109 "JAPAN_OADG109" + #define KEYMAP_SEL_JAPAN_SANWA_SKBL1 "JAPAN_SANWA_SKBL1" + #define KEYMAP_SEL_NOT_ASSIGNED_4 "KEYBOARD_4" + #define KEYMAP_SEL_NOT_ASSIGNED_5 "KEYBOARD_5" + #define KEYMAP_SEL_NOT_ASSIGNED_6 "KEYBOARD_6" + #define KEYMAP_SEL_UK_PERIBOARD_810 "UK_PERIBOARD_810" + #define KEYMAP_SEL_UK_OMOTON_K8508 "UK_OMOTON_K8508" + + // Keyboard models. The base on which this interface was created was a Wyse KB3926 PS/2 Keyboard and this is deemed STANDARD. Other models need to insert difference maps + // prior to the STANDARD entry along with the keyboard model so that it is processed first thus allowing differing keyboards with different maps. + #define KEYMAP_STANDARD 0xFF + #define KEYMAP_UK_WYSE_KB3926 0x01 + #define KEYMAP_JAPAN_OADG109 0x02 + #define KEYMAP_JAPAN_SANWA_SKBL1 0x04 + #define KEYMAP_NOT_ASSIGNED_4 0x08 + #define KEYMAP_NOT_ASSIGNED_5 0x10 + #define KEYMAP_NOT_ASSIGNED_6 0x20 + #define KEYMAP_UK_PERIBOARD_810 0x40 + #define KEYMAP_UK_OMOTON_K8508 0x80 + + public: + // Prototypes. + MZ2528(uint32_t ifMode, NVS *hdlNVS, LED *hdlLED, HID *hdlHID, const char *fsPath); + MZ2528(NVS *hdlNVS, HID *hdlHID, const char *fsPath); + MZ2528(void); + ~MZ2528(void); + bool createKeyMapFile(std::fstream &outFile); + bool storeDataToKeyMapFile(std::fstream &outFile, char *data, int size); + bool storeDataToKeyMapFile(std::fstream & outFile, std::vector& dataArray); + bool closeAndCommitKeyMapFile(std::fstream &outFile, bool cleanupOnly); + std::string getKeyMapFileName(void) { return(MZ2528IF_KEYMAP_FILE); }; + void getKeyMapHeaders(std::vector& headerList); + void getKeyMapTypes(std::vector& typeList); + bool getKeyMapSelectList(std::vector>& selectList, std::string option); + bool getKeyMapData(std::vector& dataArray, int *row, bool start); + + // Overloaded method to see if the interface must enter suspend mode, either triggered by an external event or internal. + // + inline bool suspendRequested(void) + { + return(this->suspend); + } + +// // Method to overload the suspend mechanism and include the core release mechanism. Core release is needed in order to use ESP32 API's such as NVS. +// // The method is inline to avoid a call overhead as it is generally used in time sensitive interface timing. +// inline void yield(uint32_t delay) +// { +// // If suspended, go into a permanent loop until the suspend flag is reset. +// if(this->suspend) +// { +// this->suspended = true; +// while(this->suspend) +// { +// vTaskDelay(100); +// } +// this->suspended = false; +// } else +// // Otherwise just delay by the required amount for timing and to give other threads a time slice. +// { + // vTaskDelay(delay); +// } +// return; +// } + + // Method to return the class version number. + float version(void) + { + return(MZ2528IF_VERSION); + } + + protected: + + private: + // Prototypes. + void updateMirrorMatrix(void); + uint32_t mapKey(uint16_t scanCode); + IRAM_ATTR static void mz25Interface(void *pvParameters ); + IRAM_ATTR static void mz28Interface(void *pvParameters ); + IRAM_ATTR static void hidInterface(void *pvParameters ); + void selectOption(uint8_t optionCode); + bool loadKeyMap(); + bool saveKeyMap(void); + void init(uint32_t ifMode, NVS *hdlNVS, LED *hdlLED, HID *hdlHID); + void init(NVS *hdlNVS, HID *hdlHID); + + // Overload the base yield method to include suspension of the PS/2 Keyboard interface. This interface uses interrupts which are not mutex protected and clash with the + // WiFi API methods. +// inline void yield(uint32_t delay) +// { +// // If suspended, go into a permanent loop until the suspend flag is reset. +// if(this->suspend) +// { +// // Suspend the keyboard interface. +// Keyboard->suspend(true); +// +// // Use the base method logic. +// KeyInterface::yield(delay); +// +// // Release the keyboard interface. +// Keyboard->suspend(false); +// } else +// // Otherwise just delay by the required amount for timing and to give other threads a time slice. +// { +// KeyInterface::yield(delay); +// } +// return; +// } + + // Structure to encapsulate a single key map from PS/2 to MZ-2500/MZ-2800. + typedef struct { + uint8_t ps2KeyCode; + uint8_t ps2Ctrl; + uint8_t keyboardModel; + uint8_t machine; + uint8_t mkRow[PS2TBL_MZ_MAX_MKROW]; + uint8_t mkKey[PS2TBL_MZ_MAX_MKROW]; + uint8_t brkRow[PS2TBL_MZ_MAX_BRKROW]; + uint8_t brkKey[PS2TBL_MZ_MAX_BRKROW]; + } t_keyMapEntry; + + // Structure to encapsulate the entire static keyboard mapping table. + typedef struct { + t_keyMapEntry kme[PS2TBL_MZ_MAXROWS]; + } t_keyMap; + + // Structure to maintain the MZ2528 interface configuration data. This data is persisted through powercycles as needed. + typedef struct { + struct { + uint8_t activeKeyboardMap; // Model of keyboard a keymap entry is applicable to. + uint8_t activeMachineModel; // Machine model a keymap entry is applicable to. + } params; + } t_mzConfig; + + // Configuration data. + t_mzConfig mzConfig; + + // Structure to manage the translated key matrix. This is updated by the ps2Interface thread and read by the mzInterface thead. + typedef struct { + uint8_t strobeAll; // Strobe All flag, 16 possible rows have the same column AND'd together to create this 8bit map. It is used to see if any key has been pressed. + uint32_t strobeAllAsGPIO; // Strobe All signal but as a GPIO bit map to save time in the interface thread. + uint8_t keyMatrix[16]; // Key matrix as a 16x8 matrix. + uint32_t keyMatrixAsGPIO[16]; // Key matrix mapped as GPIO bits to save time in the interface thread. + bool mode2500; + bool optionSelect; // Flag to indicate a user requested keyboard configuration option is being selected. + std::string fsPath; // Path on the underlying filesystem where storage is mounted and accessible. + t_keyMapEntry *kme; // Pointer to an array in memory to contain PS2 to MZ-2500/MZ-2800 mapping values. + int kmeRows; // Number of rows in the kme table. + std::string keyMapFileName; // Name of file where extension or replacement key map entries are stored. + bool noKeyPressed; // Flag to indicate no key has been pressed. + bool persistConfig; // Flag to request saving of the config into NVS storage. + } t_mzControl; + + // Thread handles - one per function, ie. HID interface and host target interface. + TaskHandle_t TaskHostIF = NULL; + TaskHandle_t TaskHIDIF = NULL; + + // Control structure to control interaction and mapping of keys for the host. + t_mzControl mzControl; + + // Spin lock mutex to hold a coresied to an uninterruptable method. This only works on dual core ESP32's. + portMUX_TYPE mzMutex; + + // Flag to indicate host interface should yield the CPU. + volatile bool yieldHostInterface; + +// // Keyboard object for PS/2 data retrieval and management. +// PS2KeyAdvanced *Keyboard; + + // HID object, used for keyboard input. +// HID *hid; + + // Lookup table to matrix row/column co-ordinates. + // + // Given that the MZ-2500 can emulate 3 machines and each machine has it's own mapping, differences are tagged by machine name, ie. ALL, MZ80B, MZ2000, MZ2500 + // + // If a PS2 key is matched, then the Matrix is updated using MK_ROW to point into the array with MK_KEY being the column value, equivalent of strobe line and + // the required KEY bits to be set. Upto 3 matrix bits can be set (3 key presses on the MZ-2500 keyboard) per PS/2 key. Upto 2 matrix releases can be set per + // PS/2 key. A key release is used when a modifier may already have been pressed, ie. SHIFT and it needs to be released to set the required key into the matrix. + // A set bit = 1, reset bits = 0 but is inverted in the actual matrix (1 = inactive, 0 = active), this applies for releases two, if bit = 1 then that key will be released. + // The table is scanned for a match from top to bottom. The first match is used so order is important. Japanese characters are being added as I gleam more information. + + /////////////////////////// + // MZ-2500 Keyboard Map. // + /////////////////////////// + // + // Row D7 D6 D5 D4 D3 D2 D1 D0 + //---------------------------------------------------------------------------------- + // 0 F8 F7 F6 F5 F4 F3 F2 F1 + // 1 KP - KP + KP . KP , KP 9 KP 8 F1O F9 + // 2 KP 7 KP 6 KP 5 KP 4 KP 3 KP 2 KP 1 KP 0 + // 3 BREAK RIGHT LEFT DOWN UP RETURN SPACE TAB + // 4 G F E D C B A / ? + // 5 O N M L K J I H + // 6 W V U T S R Q P + // 7 , < . > _ YEN | ^ '¿ Z ¿ Y X ¿ + // 8 7 ' 6 & 5 % 4 $ 3 # 2 " 1 ! 0 + // 9 [ { @ ` - = ; + : * 9 ) 8 ( + // 10 KP / KP * ESC BACKSPACE INST/DEL CLR/HOME COPY ] } + // 11 CTRL KANA SHIFT LOCK GRAPH + // 12 KJ2 KJ1 + // 13 HELP ARGO + // + // Col 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + // -------------------------------------------------------------------------------------------------------------------------------------- + // D0 F1 F9 KP 0 TAB / ? H P X 0 8 ( ] } GRAPH KJ1 ARGO + // D1 F2 F10 KP 1 SPACE A I Q Y 1 ! 9 ) COPY LOCK KJ2 HELP + // D2 F3 KP 8 KP 2 RETURN B J R Z 2 " : * CLR/HOME SHIFT + // D3 F4 KP 9 KP 3 UP C K S ^ '¿ 3 # ; + INST/DEL KANA + // D4 F5 KP , KP 4 DOWN D L T YEN | 4 $ - = BACKSPACE CTRL + // D5 F6 KP . KP 5 LEFT E M U _ 5 % @ ` ESC + // D6 F7 KP + KP 6 RIGHT F N V . > 6 & [ { KP * + // D7 F8 KP - KP 7 BREAK G O W , < 7 ' KP / + // + // This initial mapping is for the UK Wyse KB-3926 PS/2 keyboard and his equates to KEYMAP_STANDARD. + // + t_keyMap PS2toMZ = { + { + // < Keys to be applied on match > < Keys to be reset on match > + // PS2 Code PS2 Ctrl (Flags to Match) Keyboard Model Machine MK_ROW1 MK_ROW2 MK_ROW3 MK_KEY1 MK_KEY2 MK_KEY3 BRK_ROW1 BRK_ROW2 BRK_KEY1 BRK_KEY2 + { PS2_KEY_F1, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ_ALL, 0x00, 0xFF, 0xFF, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // F1 + { PS2_KEY_F2, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ_ALL, 0x00, 0xFF, 0xFF, 0x02, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // F2 + { PS2_KEY_F3, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ_ALL, 0x00, 0xFF, 0xFF, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // F3 + { PS2_KEY_F4, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ_ALL, 0x00, 0xFF, 0xFF, 0x08, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // F4 + { PS2_KEY_F5, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ_ALL, 0x00, 0xFF, 0xFF, 0x10, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // F5 + { PS2_KEY_F6, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ_ALL, 0x00, 0xFF, 0xFF, 0x20, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // F6 + { PS2_KEY_F7, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ_ALL, 0x00, 0xFF, 0xFF, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // F7 + { PS2_KEY_F8, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ_ALL, 0x00, 0xFF, 0xFF, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // F8 + { PS2_KEY_F9, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ_ALL, 0x01, 0xFF, 0xFF, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // F9 + { PS2_KEY_F10, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ_ALL, 0x01, 0xFF, 0xFF, 0x02, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // F10 + { PS2_KEY_F11, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ_ALL, 0x0D, 0xFF, 0xFF, 0x02, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // HELP + { PS2_KEY_F12, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ_ALL, 0x0A, 0xFF, 0xFF, 0x02, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // COPY + { PS2_KEY_TAB, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x03, 0xFF, 0xFF, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // TAB + { PS2_KEY_0, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_ALL, 0x09, 0x0B, 0xFF, 0x02, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Close Right Bracket ) + { PS2_KEY_0, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x08, 0xFF, 0xFF, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // 0 + { PS2_KEY_1, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_ALL, 0x08, 0x0B, 0xFF, 0x02, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Exclamation + { PS2_KEY_1, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x08, 0xFF, 0xFF, 0x02, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // 1 + { PS2_KEY_2, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_ALL, 0x08, 0x0B, 0xFF, 0x04, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Double quote. + { PS2_KEY_2, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x08, 0xFF, 0xFF, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // 2 + { PS2_KEY_3, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_ALL, 0x08, 0x0B, 0xFF, 0x08, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Pound Sign -> Hash + { PS2_KEY_3, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x08, 0xFF, 0xFF, 0x08, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // 3 + { PS2_KEY_4, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_ALL, 0x08, 0x0B, 0xFF, 0x10, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Dollar + { PS2_KEY_4, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x08, 0xFF, 0xFF, 0x10, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // 4 + { PS2_KEY_5, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_ALL, 0x08, 0x0B, 0xFF, 0x20, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Percent + { PS2_KEY_5, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x08, 0xFF, 0xFF, 0x20, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // 5 + { PS2_KEY_6, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_ALL, 0x07, 0xFF, 0xFF, 0x08, 0xFF, 0xFF, 0x0B, 0xFF, 0x04, 0xFF, }, // Kappa + { PS2_KEY_6, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x08, 0xFF, 0xFF, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // 6 + { PS2_KEY_7, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_ALL, 0x08, 0x0B, 0xFF, 0x40, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Ampersand + { PS2_KEY_7, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x08, 0xFF, 0xFF, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // 7 + { PS2_KEY_8, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_ALL, 0x09, 0x0B, 0xFF, 0x04, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Star + { PS2_KEY_8, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x09, 0xFF, 0xFF, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // 8 + { PS2_KEY_9, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_ALL, 0x09, 0x0B, 0xFF, 0x01, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Open Left Bracket ( + { PS2_KEY_9, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x09, 0xFF, 0xFF, 0x02, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // 9 + { PS2_KEY_A, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_ALL, 0x04, 0xFF, 0xFF, 0x02, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // a + { PS2_KEY_A, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x04, 0xFF, 0xFF, 0x02, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // A + { PS2_KEY_B, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_ALL, 0x04, 0xFF, 0xFF, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // b + { PS2_KEY_B, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x04, 0xFF, 0xFF, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // B + { PS2_KEY_C, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_ALL, 0x04, 0xFF, 0xFF, 0x08, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // c + { PS2_KEY_C, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x04, 0xFF, 0xFF, 0x08, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // C + { PS2_KEY_D, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_ALL, 0x04, 0xFF, 0xFF, 0x10, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // d + { PS2_KEY_D, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x04, 0xFF, 0xFF, 0x10, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // D + { PS2_KEY_E, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_ALL, 0x04, 0xFF, 0xFF, 0x20, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // e + { PS2_KEY_E, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x04, 0xFF, 0xFF, 0x20, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // E + { PS2_KEY_F, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_ALL, 0x04, 0xFF, 0xFF, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // f + { PS2_KEY_F, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x04, 0xFF, 0xFF, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // F + { PS2_KEY_G, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_ALL, 0x04, 0xFF, 0xFF, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // g + { PS2_KEY_G, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x04, 0xFF, 0xFF, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // G + { PS2_KEY_H, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_ALL, 0x05, 0xFF, 0xFF, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // h + { PS2_KEY_H, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x05, 0xFF, 0xFF, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // H + { PS2_KEY_I, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_ALL, 0x05, 0xFF, 0xFF, 0x02, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // i + { PS2_KEY_I, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x05, 0xFF, 0xFF, 0x02, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // I + { PS2_KEY_J, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_ALL, 0x05, 0xFF, 0xFF, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // j + { PS2_KEY_J, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x05, 0xFF, 0xFF, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // J + { PS2_KEY_K, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_ALL, 0x05, 0xFF, 0xFF, 0x08, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // k + { PS2_KEY_K, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x05, 0xFF, 0xFF, 0x08, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // K + { PS2_KEY_L, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_ALL, 0x05, 0xFF, 0xFF, 0x10, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // l + { PS2_KEY_L, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x05, 0xFF, 0xFF, 0x10, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // L + { PS2_KEY_M, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_ALL, 0x05, 0xFF, 0xFF, 0x20, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // m + { PS2_KEY_M, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x05, 0xFF, 0xFF, 0x20, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // M + { PS2_KEY_N, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_ALL, 0x05, 0xFF, 0xFF, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // n + { PS2_KEY_N, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x05, 0xFF, 0xFF, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // N + { PS2_KEY_O, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_ALL, 0x05, 0xFF, 0xFF, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // o + { PS2_KEY_O, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x05, 0xFF, 0xFF, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // O + { PS2_KEY_P, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_ALL, 0x06, 0xFF, 0xFF, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // p + { PS2_KEY_P, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x06, 0xFF, 0xFF, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // P + { PS2_KEY_Q, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_ALL, 0x06, 0xFF, 0xFF, 0x02, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // q + { PS2_KEY_Q, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x06, 0xFF, 0xFF, 0x02, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Q + { PS2_KEY_R, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_ALL, 0x06, 0xFF, 0xFF, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // r + { PS2_KEY_R, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x06, 0xFF, 0xFF, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // R + { PS2_KEY_S, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_ALL, 0x06, 0xFF, 0xFF, 0x08, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // s + { PS2_KEY_S, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x06, 0xFF, 0xFF, 0x08, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // S + { PS2_KEY_T, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_ALL, 0x06, 0xFF, 0xFF, 0x10, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // t + { PS2_KEY_T, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x06, 0xFF, 0xFF, 0x10, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // T + { PS2_KEY_U, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_ALL, 0x06, 0xFF, 0xFF, 0x20, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // u + { PS2_KEY_U, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x06, 0xFF, 0xFF, 0x20, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // U + { PS2_KEY_V, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_ALL, 0x06, 0xFF, 0xFF, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // v + { PS2_KEY_V, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x06, 0xFF, 0xFF, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // V + { PS2_KEY_W, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_ALL, 0x06, 0xFF, 0xFF, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // w + { PS2_KEY_W, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x06, 0xFF, 0xFF, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // W + { PS2_KEY_X, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_ALL, 0x07, 0xFF, 0xFF, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // x + { PS2_KEY_X, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x07, 0xFF, 0xFF, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // X + { PS2_KEY_Y, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_ALL, 0x07, 0xFF, 0xFF, 0x02, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // y + { PS2_KEY_Y, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x07, 0xFF, 0xFF, 0x02, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Y + { PS2_KEY_Z, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_ALL, 0x07, 0xFF, 0xFF, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // z + { PS2_KEY_Z, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x07, 0xFF, 0xFF, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Z + // PS2 Code PS2 Ctrl (Flags to Match) Keyboard Model Machine MK_ROW1 MK_ROW2 MK_ROW3 MK_KEY1 MK_KEY2 MK_KEY3 BRK_ROW1 BRK_ROW2 BRK_KEY1 BRK_KEY2 + { PS2_KEY_SPACE, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x03, 0xFF, 0xFF, 0x02, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Space + { PS2_KEY_COMMA, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_ALL, 0x07, 0x0B, 0xFF, 0x80, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Less Than < + { PS2_KEY_COMMA, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x07, 0xFF, 0xFF, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Comma , + { PS2_KEY_SEMI, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_ALL, 0x09, 0xFF, 0xFF, 0x04, 0xFF, 0xFF, 0x0B, 0xFF, 0x04, 0xFF, }, // Colon : + { PS2_KEY_SEMI, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x09, 0xFF, 0xFF, 0x08, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Semi-Colon ; + { PS2_KEY_DOT, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_ALL, 0x07, 0x0B, 0xFF, 0x40, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Greater Than > + { PS2_KEY_DOT, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x07, 0xFF, 0xFF, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Full stop . + { PS2_KEY_DIV, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_2000, 0x07, 0xFF, 0xFF, 0x20, 0xFF, 0xFF, 0x0B, 0xFF, 0x04, 0xFF, }, // Question ? + { PS2_KEY_DIV, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_80B, 0x07, 0xFF, 0xFF, 0x20, 0xFF, 0xFF, 0x0B, 0xFF, 0x04, 0xFF, }, // Question ? + { PS2_KEY_DIV, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_ALL, 0x04, 0x0B, 0xFF, 0x01, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Question ? + { PS2_KEY_DIV, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x04, 0xFF, 0xFF, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Divide / + { PS2_KEY_MINUS, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_2000, 0x08, 0x0B, 0xFF, 0x01, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Upper bar + { PS2_KEY_MINUS, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_80B, 0x08, 0x0B, 0xFF, 0x01, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Upper bar + { PS2_KEY_MINUS, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_ALL, 0x07, 0xFF, 0xFF, 0x20, 0xFF, 0xFF, 0x0B, 0xFF, 0x04, 0xFF, }, // Underscore + { PS2_KEY_MINUS, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x09, 0xFF, 0xFF, 0x10, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // + + { PS2_KEY_APOS, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_80B, 0x09, 0xFF, 0xFF, 0x20, 0xFF, 0xFF, 0x0B, 0xFF, 0x04, 0xFF, }, // At @ + { PS2_KEY_APOS, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_ALL, 0x09, 0xFF, 0xFF, 0x20, 0xFF, 0xFF, 0x0B, 0xFF, 0x04, 0xFF, }, // At @ + { PS2_KEY_APOS, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x08, 0x0B, 0xFF, 0x80, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Single quote ' + + { PS2_KEY_OPEN_SQ, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_ALL, 0x09, 0x0B, 0xFF, 0x40, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Open Left Brace { + { PS2_KEY_OPEN_SQ, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x09, 0xFF, 0xFF, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Open Left Square Bracket [ + { PS2_KEY_EQUAL, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_ALL, 0x09, 0x0B, 0xFF, 0x08, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Plus + + { PS2_KEY_EQUAL, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x09, 0x0B, 0xFF, 0x10, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Equal = + { PS2_KEY_CAPS, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x0B, 0xFF, 0xFF, 0x02, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // LOCK + { PS2_KEY_ENTER, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ_ALL, 0x03, 0xFF, 0xFF, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // ENTER/RETURN + { PS2_KEY_CLOSE_SQ, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_ALL, 0x0A, 0x0B, 0xFF, 0x01, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Close Right Brace } + { PS2_KEY_CLOSE_SQ, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x0A, 0xFF, 0xFF, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Close Right Square Bracket ] + { PS2_KEY_BACK, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_ALL, 0x07, 0x0B, 0xFF, 0x10, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // + { PS2_KEY_BACK, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x07, 0xFF, 0xFF, 0x10, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Back slash maps to Yen + { PS2_KEY_BTICK, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_ALL, 0x07, 0x0B, 0xFF, 0x10, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Pipe + { PS2_KEY_BTICK, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x09, 0x0B, 0xFF, 0x20, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Back tick ` + { PS2_KEY_HASH, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_2000, 0x07, 0x0B, 0xFF, 0x08, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Tilde + { PS2_KEY_HASH, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_80B, 0x07, 0x0B, 0xFF, 0x08, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Tilde + { PS2_KEY_HASH, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ_ALL, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Tilde has no mapping. + { PS2_KEY_HASH, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x08, 0x0B, 0xFF, 0x08, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Hash + { PS2_KEY_BS, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ_ALL, 0x0A, 0xFF, 0xFF, 0x10, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Backspace + { PS2_KEY_ESC, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ_ALL, 0x0A, 0xFF, 0xFF, 0x20, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // ESCape + { PS2_KEY_SCROLL, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ_ALL, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Not assigned. + { PS2_KEY_INSERT, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ_ALL, 0x0A, 0x0B, 0xFF, 0x08, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // INSERT + { PS2_KEY_HOME, PS2CTRL_FUNC | PS2CTRL_SHIFT | PS2CTRL_EXACT, KEYMAP_STANDARD, MZ_ALL, 0x0A, 0x0B, 0xFF, 0x04, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // CLR + { PS2_KEY_HOME, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ_ALL, 0x0A, 0xFF, 0xFF, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // HOME + { PS2_KEY_PGUP, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ_ALL, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Not assigned. + { PS2_KEY_DELETE, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ_ALL, 0x0A, 0xFF, 0xFF, 0x08, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // DELETE + { PS2_KEY_END, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ_ALL, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Not assigned. + { PS2_KEY_PGDN, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ_80B|MZ_2000|MZ_2500, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Not mapped + { PS2_KEY_PGDN, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ_2800, 0x0C, 0xFF, 0xFF, 0x10, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Japanese Key - Previous + { PS2_KEY_UP_ARROW, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ_ALL, 0x03, 0xFF, 0xFF, 0x08, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Up Arrow + { PS2_KEY_L_ARROW, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ_ALL, 0x03, 0xFF, 0xFF, 0x20, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Left Arrow + { PS2_KEY_DN_ARROW, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ_ALL, 0x03, 0xFF, 0xFF, 0x10, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Down Arrow + { PS2_KEY_R_ARROW, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ_ALL, 0x03, 0xFF, 0xFF, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Right Arrow + { PS2_KEY_NUM, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ_ALL, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Not assigned. + + // Keypad. + { PS2_KEY_KP0, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x02, 0xFF, 0xFF, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Keypad 0 + { PS2_KEY_KP1, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x02, 0xFF, 0xFF, 0x02, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Keypad 1 + { PS2_KEY_KP2, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x02, 0xFF, 0xFF, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Keypad 2 + { PS2_KEY_KP3, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x02, 0xFF, 0xFF, 0x08, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Keypad 3 + { PS2_KEY_KP4, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x02, 0xFF, 0xFF, 0x10, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Keypad 4 + { PS2_KEY_KP5, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x02, 0xFF, 0xFF, 0x20, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Keypad 5 + { PS2_KEY_KP6, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x02, 0xFF, 0xFF, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Keypad 6 + { PS2_KEY_KP7, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x02, 0xFF, 0xFF, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Keypad 7 + { PS2_KEY_KP8, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x01, 0xFF, 0xFF, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Keypad 8 + { PS2_KEY_KP9, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x01, 0xFF, 0xFF, 0x08, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Keypad 9 + { PS2_KEY_KP_COMMA, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x01, 0xFF, 0xFF, 0x10, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Keypad Comma , + { PS2_KEY_KP_DOT, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x01, 0xFF, 0xFF, 0x20, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Keypad Full stop . + { PS2_KEY_KP_PLUS, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x01, 0xFF, 0xFF, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Keypad Plus + + { PS2_KEY_KP_MINUS, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x01, 0xFF, 0xFF, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Keypad Minus - + { PS2_KEY_KP_TIMES, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x0A, 0xFF, 0xFF, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Keypad Times * + { PS2_KEY_KP_DIV, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x0A, 0xFF, 0xFF, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Keypad Divide / + { PS2_KEY_KP_ENTER, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0x03, 0xFF, 0xFF, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Keypad Ebter / + + // PS2 Code PS2 Ctrl (Flags to Match) Keyboard Model Machine MK_ROW1 MK_ROW2 MK_ROW3 MK_KEY1 MK_KEY2 MK_KEY3 BRK_ROW1 BRK_ROW2 BRK_KEY1 BRK_KEY2 + // Special keys. + { PS2_KEY_PRTSCR, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ_ALL, 0x0D, 0xFF, 0xFF, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // ARGO KEY + { PS2_KEY_PAUSE, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ_ALL, 0x03, 0xFF, 0xFF, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // BREAK KEY + { PS2_KEY_L_GUI, PS2CTRL_FUNC | PS2CTRL_GUI, KEYMAP_STANDARD, MZ_ALL, 0x0B, 0xFF, 0xFF, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // GRAPH KEY + { PS2_KEY_L_ALT, PS2CTRL_FUNC | PS2CTRL_ALT, KEYMAP_STANDARD, MZ_ALL, 0x0C, 0xFF, 0xFF, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // KJ1 Sentence + { PS2_KEY_R_ALT, PS2CTRL_FUNC | PS2CTRL_ALTGR, KEYMAP_STANDARD, MZ_ALL, 0x0C, 0xFF, 0xFF, 0x02, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // KJ2 Transform + { PS2_KEY_R_GUI, PS2CTRL_FUNC | PS2CTRL_GUI, KEYMAP_STANDARD, MZ_ALL, 0x0B, 0xFF, 0xFF, 0x08, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // KANA KEY + { PS2_KEY_MENU, PS2CTRL_FUNC | PS2CTRL_GUI, KEYMAP_STANDARD, MZ_ALL, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Not assigned. + // Modifiers are last, only being selected if an earlier match isnt made. + { PS2_KEY_L_SHIFT, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ_ALL, 0x0B, 0xFF, 0xFF, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, + { PS2_KEY_R_SHIFT, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ_ALL, 0x0B, 0xFF, 0xFF, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, + { PS2_KEY_L_CTRL, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ_ALL, 0x0B, 0xFF, 0xFF, 0x10, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, + { PS2_KEY_R_CTRL, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ_80B|MZ_2000|MZ_2500, 0x0B, 0xFF, 0xFF, 0x10, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Map to Control + { PS2_KEY_R_CTRL, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ_2800, 0x0C, 0xFF, 0xFF, 0x08, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, // Japanese Key - Cancel + { 0, PS2CTRL_NONE, KEYMAP_STANDARD, MZ_ALL, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }, + }}; +}; + +#endif // MZ2528_H diff --git a/main/include/MZ5665.h b/main/include/MZ5665.h deleted file mode 120000 index 24ca752..0000000 --- a/main/include/MZ5665.h +++ /dev/null @@ -1 +0,0 @@ -../../../sharpkey/main/include/MZ5665.h \ No newline at end of file diff --git a/main/include/MZ5665.h b/main/include/MZ5665.h new file mode 100644 index 0000000..1fa6bb9 --- /dev/null +++ b/main/include/MZ5665.h @@ -0,0 +1,574 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// Name: MZ5665.h +// Created: Apr 2022 +// Version: v1.0 +// Author(s): Philip Smart +// Description: Header for the Sharp MZ-6500 to HID (PS/2, Bluetooth) interface logic. +// Credits: +// Copyright: (c) 2019-2022 Philip Smart +// +// History: Apr 2022 - Initial write. +// v1.01 Jun 2022 - Updates to reflect changes realised in other modules due to addition of +// bluetooth and suspend logic due to NVS issues using both cores. +// +// 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 . +///////////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef MZ5665_H +#define MZ5665_H + +// Include the specification class. +#include "KeyInterface.h" +#include "NVS.h" +#include "LED.h" +#include "HID.h" +#include +#include + +// NB: Macros definitions put inside class for clarity, they are still global scope. + +// Encapsulate the Sharp MZ-6500 interface. +class MZ5665 : public KeyInterface { + // Macros. + // + #define NUMELEM(a) (sizeof(a)/sizeof(a[0])) + + // Constants. + #define MZ5665IF_VERSION 1.01 + #define MZ5665IF_KEYMAP_FILE "MZ5665_KeyMap.BIN" + #define MAX_MZ5665_XMIT_KEY_BUF 16 + #define PS2TBL_MZ5665_MAXROWS 349 + + // MZ-6500 Key control bit mask. + #define MZ5665_CTRL_GRAPH ((unsigned char) (1 << 4)) + #define MZ5665_CTRL_CAPS ((unsigned char) (1 << 3)) + #define MZ5665_CTRL_KANA ((unsigned char) (1 << 2)) + #define MZ5665_CTRL_SHIFT ((unsigned char) (1 << 1)) + #define MZ5665_CTRL_CTRL ((unsigned char) (1 << 0)) + + // Special key definition. + #define MZ5665_KEY_UP 0x1E // ↑ + #define MZ5665_KEY_DOWN 0x1F // ↓ + #define MZ5665_KEY_LEFT 0x1D // ↠+ #define MZ5665_KEY_RIGHT 0x1C // → → + #define MZ5665_KEY_INS 0x12 // INS + #define MZ5665_KEY_DEL 0x08 // DEL + #define MZ5665_KEY_CLR 0x0C // CLR + #define MZ5665_KEY_HOME 0x0B // HOME + + // PS2 Flag definitions. + #define PS2CTRL_NONE 0x00 // No keys active = 0 + #define PS2CTRL_SHIFT 0x01 // Shfit Key active = 1 + #define PS2CTRL_CTRL 0x02 // Ctrl Key active = 1 + #define PS2CTRL_CAPS 0x04 // CAPS active = 1 + #define PS2CTRL_KANA 0x08 // KANA active = 1 + #define PS2CTRL_GRAPH 0x10 // GRAPH active = 1 + #define PS2CTRL_GUI 0x20 // GUI Key active = 1 + #define PS2CTRL_FUNC 0x40 // Special Function Keys active = 1 + #define PS2CTRL_BREAK 0x80 // BREAK Key active = 1 + #define PS2CTRL_EXACT 0x80 // EXACT Match active = 1 + + // The initial mapping is made inside the PS2KeyAdvanced class from Scan Code Set 2 to ASCII + // for a selected keyboard. Special functions are detected and combined inside this module + // before mapping with the table below to extract the MZ-6500 key code and control data. + // ie. PS/2 Scan Code -> ASCII + Flags -> MZ-6500 Key Code + Ctrl Data + + // Keyboard mapping table column names. + #define PS2TBL_PS2KEYCODE_NAME "PS/2 KeyCode" + #define PS2TBL_PS2CTRL_NAME "PS/2 Control Key" + #define PS2TBL_KEYBOARDMODEL_NAME "For Keyboard" + #define PS2TBL_MACHINE_NAME "For Host Model" + #define PS2TBL_MZ5665_KEYCODE_NAME "MZ5665 KeyCode" + #define PS2TBL_MZ5665__CTRL_NAME "MZ5665 Control Key" + + // Keyboard mapping table column types. + #define PS2TBL_PS2KEYCODE_TYPE "hex" + #define PS2TBL_PS2CTRL_TYPE "custom_cbp_ps2ctrl" + #define PS2TBL_KEYBOARDMODEL_TYPE "custom_cbp_keybmodel" + #define PS2TBL_MACHINE_TYPE "custom_cbp_machine" + #define PS2TBL_MZ5665_KEYCODE_TYPE "hex" + #define PS2TBL_MZ5665_CTRL_TYPE "custom_cbn_x1ctrl" + + // Keyboard mapping table select list for PS2CTRL. + #define PS2TBL_PS2CTRL_SEL_NONE "NONE" + #define PS2TBL_PS2CTRL_SEL_SHIFT "SHIFT" + #define PS2TBL_PS2CTRL_SEL_CTRL "CTRL" + #define PS2TBL_PS2CTRL_SEL_CAPS "CAPS" + #define PS2TBL_PS2CTRL_SEL_KANA "KANA" + #define PS2TBL_PS2CTRL_SEL_GRAPH "GRAPH" + #define PS2TBL_PS2CTRL_SEL_GUI "GUI" + #define PS2TBL_PS2CTRL_SEL_FUNC "FUNC" + #define PS2TBL_PS2CTRL_SEL_EXACT "EXACT" + + // Keyboard mapping table select list for Model of keyboard. + #define KEYMAP_SEL_STANDARD "ALL" + #define KEYMAP_SEL_UK_WYSE_KB3926 "UK_WYSE_KB3926" + #define KEYMAP_SEL_JAPAN_OADG109 "JAPAN_OADG109" + #define KEYMAP_SEL_JAPAN_SANWA_SKBL1 "JAPAN_SANWA_SKBL1" + #define KEYMAP_SEL_NOT_ASSIGNED_4 "KEYBOARD_4" + #define KEYMAP_SEL_NOT_ASSIGNED_5 "KEYBOARD_5" + #define KEYMAP_SEL_NOT_ASSIGNED_6 "KEYBOARD_6" + #define KEYMAP_SEL_UK_PERIBOARD_810 "UK_PERIBOARD_810" + #define KEYMAP_SEL_UK_OMOTON_K8508 "UK_OMOTON_K8508" + + // Keyboard mapping table select list for target machine. + #define MZ5665_SEL_ALL "ALL" + + // Keyboard mapping table select list for MZ5665 Control codes. + #define MZ5665_CTRL_SEL_GRAPH "GRAPH" + #define MZ5665_CTRL_SEL_CAPS "CAPS" + #define MZ5665_CTRL_SEL_KANA "KANA" + #define MZ5665_CTRL_SEL_SHIFT "SHIFT" + #define MZ5665_CTRL_SEL_CTRL "CTRL" + + // The Sharp MZ-6500 Series was released over a number of years and each iteration added changes/updates. In order to cater for differences, it is possible to assign a key mapping + // to a specific machine type(s) or all of the series by adding the flags below into the mapping table. + #define MZ5665_ALL 0xFF + + // Keyboard models. The base on which this interface was created was a Wyse KB3926 PS/2 Keyboard and this is deemed STANDARD. Other models need to insert difference maps + // prior to the STANDARD entry along with the keyboard model so that it is processed first thus allowing differing keyboards with different maps. + #define KEYMAP_STANDARD 0xFF + #define KEYMAP_UK_WYSE_KB3926 0x01 + #define KEYMAP_JAPAN_OADG109 0x02 + #define KEYMAP_JAPAN_SANWA_SKBL1 0x04 + #define KEYMAP_NOT_ASSIGNED_4 0x08 + #define KEYMAP_NOT_ASSIGNED_5 0x10 + #define KEYMAP_NOT_ASSIGNED_6 0x20 + #define KEYMAP_UK_PERIBOARD_810 0x40 + #define KEYMAP_UK_OMOTON_K8508 0x80 + + public: + // Prototypes. + MZ5665(void); + MZ5665(uint32_t ifMode, NVS *hdlNVS, LED *hdlLED, HID *hdlHID, const char *fsPath); + MZ5665(NVS *hdlNVS, HID *hdlHID, const char *fsPath); + ~MZ5665(void); + bool createKeyMapFile(std::fstream &outFile); + bool storeDataToKeyMapFile(std::fstream &outFile, char *data, int size); + bool storeDataToKeyMapFile(std::fstream & outFile, std::vector& dataArray); + bool closeAndCommitKeyMapFile(std::fstream &outFile, bool cleanupOnly); + std::string getKeyMapFileName(void) { return(MZ5665IF_KEYMAP_FILE); }; + void getKeyMapHeaders(std::vector& headerList); + void getKeyMapTypes(std::vector& typeList); + bool getKeyMapSelectList(std::vector>& selectList, std::string option); + bool getKeyMapData(std::vector& dataArray, int *row, bool start); + + // Method to return the class version number. + float version(void) + { + return(MZ5665IF_VERSION); + } + + protected: + + private: + // Prototypes. + void pushKeyToQueue(uint32_t key); + IRAM_ATTR static void mzInterface( void * pvParameters ); + IRAM_ATTR static void hidInterface( void * pvParameters ); + void selectOption(uint8_t optionCode); + uint32_t mapKey(uint16_t scanCode); + bool loadKeyMap(); + bool saveKeyMap(void); + void init(uint32_t ifMode, NVS *hdlNVS, LED *hdlLED, HID *hdlHID); + void init(NVS *hdlNVS, HID *hdlHID); + +// // Overload the base yield method to include suspension of the PS/2 Keyboard interface. This interface uses interrupts which are not mutex protected and clash with the +// // WiFi API methods. +// inline void yield(uint32_t delay) +// { +// // If suspended, go into a permanent loop until the suspend flag is reset. +// if(this->suspend) +// { +// // Suspend the keyboard interface. +// Keyboard->suspend(true); +// +// // Use the base method logic. +// KeyInterface::yield(delay); +// +// // Release the keyboard interface. +// Keyboard->suspend(false); +// } else +// // Otherwise just delay by the required amount for timing and to give other threads a time slice. +// { +// KeyInterface::yield(delay); +// } +// return; +// } + + // Structure to encapsulate a single key map from PS/2 to MZ-5600/MZ-6500. + typedef struct { + uint8_t ps2KeyCode; + uint8_t ps2Ctrl; + uint8_t keyboardModel; + uint8_t machine; + uint8_t mzKey; + uint8_t mzCtrl; + } t_keyMapEntry; + + // Structure to encapsulate the entire static keyboard mapping table. + typedef struct { + t_keyMapEntry kme[PS2TBL_MZ5665_MAXROWS]; + } t_keyMap; + + // Structure to maintain the MZ-5600/MZ-6500 interface configuration data. This data is persisted through powercycles as needed. + typedef struct { + struct { + uint8_t activeKeyboardMap; // Model of keyboard a keymap entry is applicable to. + uint8_t activeMachineModel; // Machine model a keymap entry is applicable to. + } params; + } t_mzConfig; + + // Configuration data. + t_mzConfig mzConfig; + + // Structure to manage the control signals signifying the state of the MZ-6500 keyboard. + typedef struct { + bool optionSelect; // Flag to indicate a user requested keyboard configuration option is being selected. + uint8_t keyCtrl; // Keyboard state flag control. + + std::string fsPath; // Path on the underlying filesystem where storage is mounted and accessible. + t_keyMapEntry *kme; // Pointer to an array in memory to contain PS2 to MZ-6500 mapping values. + int kmeRows; // Number of rows in the kme table. + std::string keyMapFileName; // Name of file where extension or replacement key map entries are stored. + } t_mzControl; + + // Transmit buffer queue item. + typedef struct { + uint32_t keyCode; // 16bit, bits 8:0 represent the key, 9 if CTRL to be sent, 10 if ALT to be sent. + } t_xmitQueueMessage; + + // Thread handles - one per function, ie. HID interface and host target interface. + TaskHandle_t TaskHostIF = NULL; + TaskHandle_t TaskHIDIF = NULL; + + // Control structure to control interaction and mapping of keys for the host. + t_mzControl mzCtrl; + + // Spin lock mutex to hold a coresied to an uninterruptable method. This only works on dual core ESP32's. + portMUX_TYPE mzMutex; + + // + // This mapping is for the UK Wyse KB-3926 PS/2 keyboard + // + t_keyMap PS2toMZ5665 = { + { + // HELP + // COPY + ////PS2 Code PS2 Ctrl (Flags to Match) Keyboard Model Machine MZ5665 Data MZ5665 Ctrl (Flags to Set). + //{ PS2_KEY_F1, PS2CTRL_FUNC | PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ5665_ALL, 'v', 0x00, }, // SHIFT+F1 + //{ PS2_KEY_F2, PS2CTRL_FUNC | PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ5665_ALL, 'w', 0x00, }, // SHIFT+F2 + //{ PS2_KEY_F3, PS2CTRL_FUNC | PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ5665_ALL, 'x', 0x00, }, // SHIFT+F3 + //{ PS2_KEY_F4, PS2CTRL_FUNC | PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ5665_ALL, 'y', 0x00, }, // SHIFT+F4 + //{ PS2_KEY_F5, PS2CTRL_FUNC | PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ5665_ALL, 'z', 0x00, }, // SHIFT+F5 + //{ PS2_KEY_F1, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ5665_ALL, 'q', 0x00, }, // F1 + //{ PS2_KEY_F2, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ5665_ALL, 'r', 0x00, }, // F2 + //{ PS2_KEY_F3, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ5665_ALL, 's', 0x00, }, // F3 + //{ PS2_KEY_F4, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ5665_ALL, 't', 0x00, }, // F4 + //{ PS2_KEY_F5, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ5665_ALL, 'u', 0x00, }, // F5 + //{ PS2_KEY_F6, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ5665_ALL, 0xEC, 0x00, }, // F6 + //{ PS2_KEY_F7, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ5665_ALL, 0xEB, 0x00, }, // F7 + //{ PS2_KEY_F8, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ5665_ALL, 0xE2, 0x00, }, // F8 + //{ PS2_KEY_F9, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ5665_ALL, 0xE1, 0x00, }, // F9 + //{ PS2_KEY_F10, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ5665_ALL, 0x00, 0x00, }, // XFER + //{ PS2_KEY_F11, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ5665_ALL, 0xFE, 0x00, }, // HELP + //{ PS2_KEY_F12, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ5665_ALL, 0x00, 0x00, }, // COPY + //{ PS2_KEY_TAB, PS2CTRL_NONE, KEYMAP_STANDARD, MZ5665_ALL, 0x09, 0x00, }, // TAB + // Numeric keys. + { PS2_KEY_0, PS2CTRL_NONE, KEYMAP_STANDARD, MZ5665_ALL, '0', 0x00, }, // 0 + { PS2_KEY_1, PS2CTRL_NONE, KEYMAP_STANDARD, MZ5665_ALL, '1', 0x00, }, // 1 + { PS2_KEY_2, PS2CTRL_NONE, KEYMAP_STANDARD, MZ5665_ALL, '2', 0x00, }, // 2 + { PS2_KEY_3, PS2CTRL_NONE, KEYMAP_STANDARD, MZ5665_ALL, '3', 0x00, }, // 3 + { PS2_KEY_4, PS2CTRL_NONE, KEYMAP_STANDARD, MZ5665_ALL, '4', 0x00, }, // 4 + { PS2_KEY_5, PS2CTRL_NONE, KEYMAP_STANDARD, MZ5665_ALL, '5', 0x00, }, // 5 + { PS2_KEY_6, PS2CTRL_NONE, KEYMAP_STANDARD, MZ5665_ALL, '6', 0x00, }, // 6 + { PS2_KEY_7, PS2CTRL_NONE, KEYMAP_STANDARD, MZ5665_ALL, '7', 0x00, }, // 7 + { PS2_KEY_8, PS2CTRL_NONE, KEYMAP_STANDARD, MZ5665_ALL, '8', 0x00, }, // 8 + { PS2_KEY_9, PS2CTRL_NONE, KEYMAP_STANDARD, MZ5665_ALL, '9', 0x00, }, // 9 + // Punctuation keys. + { PS2_KEY_0, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ5665_ALL, ')', 0x00, }, // Close Right Bracket ) + { PS2_KEY_1, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ5665_ALL, '!', 0x00, }, // Exclamation + { PS2_KEY_2, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ5665_ALL, '"', 0x00, }, // Double quote. + { PS2_KEY_3, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ5665_ALL, 0x23, 0x00, }, // Pound Sign -> Hash + { PS2_KEY_4, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ5665_ALL, '$', 0x00, }, // Dollar + { PS2_KEY_5, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ5665_ALL, '%', 0x00, }, // Percent + { PS2_KEY_6, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ5665_ALL, '^', 0x00, }, // Kappa + { PS2_KEY_7, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ5665_ALL, '&', 0x00, }, // Ampersand + { PS2_KEY_8, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ5665_ALL, '*', 0x00, }, // Star + { PS2_KEY_9, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ5665_ALL, '(', 0x00, }, // Open Left Bracket ( + // ALPHA keys, lower and uppercase. + //PS2 Code PS2 Ctrl (Flags to Match) Keyboard Model Machine MZ5665 Data MZ5665 Ctrl (Flags to Set). + { PS2_KEY_A, PS2CTRL_SHIFT | PS2CTRL_CAPS, KEYMAP_STANDARD, MZ5665_ALL, 'a', 0x00, }, // a + { PS2_KEY_A, PS2CTRL_CAPS, KEYMAP_STANDARD, MZ5665_ALL, 'A', 0x00, }, // A + { PS2_KEY_B, PS2CTRL_SHIFT | PS2CTRL_CAPS, KEYMAP_STANDARD, MZ5665_ALL, 'b', 0x00, }, // b + { PS2_KEY_B, PS2CTRL_CAPS, KEYMAP_STANDARD, MZ5665_ALL, 'B', 0x00, }, // B + { PS2_KEY_C, PS2CTRL_SHIFT | PS2CTRL_CAPS, KEYMAP_STANDARD, MZ5665_ALL, 'c', 0x00, }, // c + { PS2_KEY_C, PS2CTRL_CAPS, KEYMAP_STANDARD, MZ5665_ALL, 'C', 0x00, }, // C + { PS2_KEY_D, PS2CTRL_SHIFT | PS2CTRL_CAPS, KEYMAP_STANDARD, MZ5665_ALL, 'd', 0x00, }, // d + { PS2_KEY_D, PS2CTRL_CAPS, KEYMAP_STANDARD, MZ5665_ALL, 'D', 0x00, }, // D + { PS2_KEY_E, PS2CTRL_SHIFT | PS2CTRL_CAPS, KEYMAP_STANDARD, MZ5665_ALL, 'e', 0x00, }, // e + { PS2_KEY_E, PS2CTRL_CAPS, KEYMAP_STANDARD, MZ5665_ALL, 'E', 0x00, }, // E + { PS2_KEY_F, PS2CTRL_SHIFT | PS2CTRL_CAPS, KEYMAP_STANDARD, MZ5665_ALL, 'f', 0x00, }, // f + { PS2_KEY_F, PS2CTRL_CAPS, KEYMAP_STANDARD, MZ5665_ALL, 'F', 0x00, }, // F + { PS2_KEY_G, PS2CTRL_SHIFT | PS2CTRL_CAPS, KEYMAP_STANDARD, MZ5665_ALL, 'g', 0x00, }, // g + { PS2_KEY_G, PS2CTRL_CAPS, KEYMAP_STANDARD, MZ5665_ALL, 'G', 0x00, }, // G + { PS2_KEY_H, PS2CTRL_SHIFT | PS2CTRL_CAPS, KEYMAP_STANDARD, MZ5665_ALL, 'h', 0x00, }, // h + { PS2_KEY_H, PS2CTRL_CAPS, KEYMAP_STANDARD, MZ5665_ALL, 'H', 0x00, }, // H + { PS2_KEY_I, PS2CTRL_SHIFT | PS2CTRL_CAPS, KEYMAP_STANDARD, MZ5665_ALL, 'i', 0x00, }, // i + { PS2_KEY_I, PS2CTRL_CAPS, KEYMAP_STANDARD, MZ5665_ALL, 'I', 0x00, }, // I + { PS2_KEY_J, PS2CTRL_SHIFT | PS2CTRL_CAPS, KEYMAP_STANDARD, MZ5665_ALL, 'j', 0x00, }, // j + { PS2_KEY_J, PS2CTRL_CAPS, KEYMAP_STANDARD, MZ5665_ALL, 'J', 0x00, }, // J + { PS2_KEY_K, PS2CTRL_SHIFT | PS2CTRL_CAPS, KEYMAP_STANDARD, MZ5665_ALL, 'k', 0x00, }, // k + { PS2_KEY_K, PS2CTRL_CAPS, KEYMAP_STANDARD, MZ5665_ALL, 'K', 0x00, }, // K + { PS2_KEY_L, PS2CTRL_SHIFT | PS2CTRL_CAPS, KEYMAP_STANDARD, MZ5665_ALL, 'l', 0x00, }, // l + { PS2_KEY_L, PS2CTRL_CAPS, KEYMAP_STANDARD, MZ5665_ALL, 'L', 0x00, }, // L + { PS2_KEY_M, PS2CTRL_SHIFT | PS2CTRL_CAPS, KEYMAP_STANDARD, MZ5665_ALL, 'm', 0x00, }, // m + { PS2_KEY_M, PS2CTRL_CAPS, KEYMAP_STANDARD, MZ5665_ALL, 'M', 0x00, }, // M + { PS2_KEY_N, PS2CTRL_SHIFT | PS2CTRL_CAPS, KEYMAP_STANDARD, MZ5665_ALL, 'n', 0x00, }, // n + { PS2_KEY_N, PS2CTRL_CAPS, KEYMAP_STANDARD, MZ5665_ALL, 'N', 0x00, }, // N + { PS2_KEY_O, PS2CTRL_SHIFT | PS2CTRL_CAPS, KEYMAP_STANDARD, MZ5665_ALL, 'o', 0x00, }, // o + { PS2_KEY_O, PS2CTRL_CAPS, KEYMAP_STANDARD, MZ5665_ALL, 'O', 0x00, }, // O + { PS2_KEY_P, PS2CTRL_SHIFT | PS2CTRL_CAPS, KEYMAP_STANDARD, MZ5665_ALL, 'p', 0x00, }, // p + { PS2_KEY_P, PS2CTRL_CAPS, KEYMAP_STANDARD, MZ5665_ALL, 'P', 0x00, }, // P + { PS2_KEY_Q, PS2CTRL_SHIFT | PS2CTRL_CAPS, KEYMAP_STANDARD, MZ5665_ALL, 'q', 0x00, }, // q + { PS2_KEY_Q, PS2CTRL_CAPS, KEYMAP_STANDARD, MZ5665_ALL, 'Q', 0x00, }, // Q + { PS2_KEY_R, PS2CTRL_SHIFT | PS2CTRL_CAPS, KEYMAP_STANDARD, MZ5665_ALL, 'r', 0x00, }, // r + { PS2_KEY_R, PS2CTRL_CAPS, KEYMAP_STANDARD, MZ5665_ALL, 'R', 0x00, }, // R + { PS2_KEY_S, PS2CTRL_SHIFT | PS2CTRL_CAPS, KEYMAP_STANDARD, MZ5665_ALL, 's', 0x00, }, // s + { PS2_KEY_S, PS2CTRL_CAPS, KEYMAP_STANDARD, MZ5665_ALL, 'S', 0x00, }, // S + { PS2_KEY_T, PS2CTRL_SHIFT | PS2CTRL_CAPS, KEYMAP_STANDARD, MZ5665_ALL, 't', 0x00, }, // t + { PS2_KEY_T, PS2CTRL_CAPS, KEYMAP_STANDARD, MZ5665_ALL, 'T', 0x00, }, // T + { PS2_KEY_U, PS2CTRL_SHIFT | PS2CTRL_CAPS, KEYMAP_STANDARD, MZ5665_ALL, 'u', 0x00, }, // u + { PS2_KEY_U, PS2CTRL_CAPS, KEYMAP_STANDARD, MZ5665_ALL, 'U', 0x00, }, // U + { PS2_KEY_V, PS2CTRL_SHIFT | PS2CTRL_CAPS, KEYMAP_STANDARD, MZ5665_ALL, 'v', 0x00, }, // v + { PS2_KEY_V, PS2CTRL_CAPS, KEYMAP_STANDARD, MZ5665_ALL, 'V', 0x00, }, // V + { PS2_KEY_W, PS2CTRL_SHIFT | PS2CTRL_CAPS, KEYMAP_STANDARD, MZ5665_ALL, 'w', 0x00, }, // w + { PS2_KEY_W, PS2CTRL_CAPS, KEYMAP_STANDARD, MZ5665_ALL, 'W', 0x00, }, // W + { PS2_KEY_X, PS2CTRL_SHIFT | PS2CTRL_CAPS, KEYMAP_STANDARD, MZ5665_ALL, 'x', 0x00, }, // x + { PS2_KEY_X, PS2CTRL_CAPS, KEYMAP_STANDARD, MZ5665_ALL, 'X', 0x00, }, // X + { PS2_KEY_Y, PS2CTRL_SHIFT | PS2CTRL_CAPS, KEYMAP_STANDARD, MZ5665_ALL, 'y', 0x00, }, // y + { PS2_KEY_Y, PS2CTRL_CAPS, KEYMAP_STANDARD, MZ5665_ALL, 'Y', 0x00, }, // Y + { PS2_KEY_Z, PS2CTRL_SHIFT | PS2CTRL_CAPS, KEYMAP_STANDARD, MZ5665_ALL, 'z', 0x00, }, // z + { PS2_KEY_Z, PS2CTRL_CAPS, KEYMAP_STANDARD, MZ5665_ALL, 'Z', 0x00, }, // Z + + //PS2 Code PS2 Ctrl (Flags to Match) Keyboard Model Machine MZ5665 Data MZ5665 Ctrl (Flags to Set). + { PS2_KEY_SPACE, PS2CTRL_NONE, KEYMAP_STANDARD, MZ5665_ALL, ' ', 0x00, }, // Space + { PS2_KEY_COMMA, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ5665_ALL, '<', 0x00, }, // Less Than < + { PS2_KEY_COMMA, PS2CTRL_NONE, KEYMAP_STANDARD, MZ5665_ALL, ',', 0x00, }, // Comma , + { PS2_KEY_SEMI, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ5665_ALL, ':', 0x00, }, // Colon : + { PS2_KEY_SEMI, PS2CTRL_NONE, KEYMAP_STANDARD, MZ5665_ALL, ';', 0x00, }, // Semi-Colon ; + { PS2_KEY_DOT, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ5665_ALL, '>', 0x00, }, // Greater Than > + { PS2_KEY_DOT, PS2CTRL_NONE, KEYMAP_STANDARD, MZ5665_ALL, '.', 0x00, }, // Full stop . + { PS2_KEY_DIV, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ5665_ALL, '?', 0x00, }, // Question ? + { PS2_KEY_DIV, PS2CTRL_NONE, KEYMAP_STANDARD, MZ5665_ALL, '/', 0x00, }, // Divide / + { PS2_KEY_MINUS, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ5665_ALL, '_', 0x00, }, // Underscore + { PS2_KEY_MINUS, PS2CTRL_NONE, KEYMAP_STANDARD, MZ5665_ALL, '-', 0x00, }, + { PS2_KEY_APOS, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ5665_ALL, '@', 0x00, }, // At @ + { PS2_KEY_APOS, PS2CTRL_NONE, KEYMAP_STANDARD, MZ5665_ALL, '\'', 0x00, }, // Single quote ' + { PS2_KEY_OPEN_SQ, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ5665_ALL, '{', 0x00, }, // Open Left Brace { + { PS2_KEY_OPEN_SQ, PS2CTRL_NONE, KEYMAP_STANDARD, MZ5665_ALL, '[', 0x00, }, // Open Left Square Bracket [ + { PS2_KEY_EQUAL, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ5665_ALL, '+', 0x00, }, // Plus + + { PS2_KEY_EQUAL, PS2CTRL_NONE, KEYMAP_STANDARD, MZ5665_ALL, '=', 0x00, }, // Equal = + { PS2_KEY_CAPS, PS2CTRL_NONE, KEYMAP_STANDARD, MZ5665_ALL, ' ', 0x00, }, // LOCK + { PS2_KEY_ENTER, PS2CTRL_NONE, KEYMAP_STANDARD, MZ5665_ALL, 0x0D, 0x00, }, // ENTER/RETURN + { PS2_KEY_CLOSE_SQ, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ5665_ALL, '}', 0x00, }, // Close Right Brace } + { PS2_KEY_CLOSE_SQ, PS2CTRL_NONE, KEYMAP_STANDARD, MZ5665_ALL, ']', 0x00, }, // Close Right Square Bracket ] + { PS2_KEY_BACK, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ5665_ALL, '|', 0x00, }, // + { PS2_KEY_BACK, PS2CTRL_NONE, KEYMAP_STANDARD, MZ5665_ALL, '\\', 0x00, }, // Back slash maps to Yen + { PS2_KEY_BTICK, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ5665_ALL, '`', 0x00, }, // Pipe + { PS2_KEY_BTICK, PS2CTRL_NONE, KEYMAP_STANDARD, MZ5665_ALL, '|', 0x00, }, // Back tick ` + { PS2_KEY_HASH, PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ5665_ALL, '~', 0x00, }, // Tilde has no mapping. + { PS2_KEY_HASH, PS2CTRL_NONE, KEYMAP_STANDARD, MZ5665_ALL, '#', 0x00, }, // Hash + { PS2_KEY_BS, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ5665_ALL, 0x08, 0x00, }, // Backspace + { PS2_KEY_ESC, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ5665_ALL, 0x1B, 0x00, }, // ESCape + { PS2_KEY_SCROLL, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ5665_ALL, ' ', 0x00, }, // Not assigned. + { PS2_KEY_INSERT, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ5665_ALL, MZ5665_KEY_INS, 0x00, }, // INSERT + { PS2_KEY_HOME, PS2CTRL_FUNC | PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ5665_ALL, MZ5665_KEY_CLR, 0x00, }, // CLR + { PS2_KEY_HOME, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ5665_ALL, MZ5665_KEY_HOME, 0x00, }, // HOME + { PS2_KEY_DELETE, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ5665_ALL, MZ5665_KEY_DEL, 0x00, }, // DELETE + { PS2_KEY_END, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ5665_ALL, 0x11, 0x00, }, // END + { PS2_KEY_PGUP, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ5665_ALL, 0x0E, 0x00, }, // Roll Up. + { PS2_KEY_PGDN, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ5665_ALL, 0x0F, 0x00, }, // Roll Down + { PS2_KEY_UP_ARROW, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ5665_ALL, MZ5665_KEY_UP, 0x00, }, // Up Arrow + { PS2_KEY_L_ARROW, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ5665_ALL, MZ5665_KEY_LEFT, 0x00, }, // Left Arrow + { PS2_KEY_DN_ARROW, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ5665_ALL, MZ5665_KEY_DOWN, 0x00, }, // Down Arrow + { PS2_KEY_R_ARROW, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ5665_ALL, MZ5665_KEY_RIGHT, 0x00, }, // Right Arrow + { PS2_KEY_NUM, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ5665_ALL, 0x00, 0x00, }, // Not assigned. + // GRPH (Alt Gr) + //PS2 Code PS2 Ctrl (Flags to Match) Keyboard Model Machine MZ5665 Data MZ5665 Ctrl (Flags to Set). + { PS2_KEY_0, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0xFA, 0x00, }, // GRPH+0 + { PS2_KEY_1, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0xF1, 0x00, }, // GRPH+1 + { PS2_KEY_2, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0xF2, 0x00, }, // GRPH+2 + { PS2_KEY_3, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0xF3, 0x00, }, // GRPH+3 + { PS2_KEY_4, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0xF4, 0x00, }, // GRPH+4 + { PS2_KEY_5, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0xF5, 0x00, }, // GRPH+5 + { PS2_KEY_6, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0xF6, 0x00, }, // GRPH+6 + { PS2_KEY_7, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0xF7, 0x00, }, // GRPH+7 + { PS2_KEY_8, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0xF8, 0x00, }, // GRPH+8 + { PS2_KEY_9, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0xF9, 0x00, }, // GRPH+9 + { PS2_KEY_A, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0x7F, 0x00, }, // GRPH+A + { PS2_KEY_B, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0x84, 0x00, }, // GRPH+B + { PS2_KEY_C, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0x82, 0x00, }, // GRPH+C + { PS2_KEY_D, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0xEA, 0x00, }, // GRPH+D + { PS2_KEY_E, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0xE2, 0x00, }, // GRPH+E + { PS2_KEY_F, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0xEB, 0x00, }, // GRPH+F + { PS2_KEY_G, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0xEC, 0x00, }, // GRPH+G + { PS2_KEY_H, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0xED, 0x00, }, // GRPH+H + { PS2_KEY_I, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0xE7, 0x00, }, // GRPH+I + { PS2_KEY_J, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0xEE, 0x00, }, // GRPH+J + { PS2_KEY_K, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0xEF, 0x00, }, // GRPH+K + { PS2_KEY_L, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0x8E, 0x00, }, // GRPH+L + { PS2_KEY_M, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0x86, 0x00, }, // GRPH+M + { PS2_KEY_N, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0x85, 0x00, }, // GRPH+N + { PS2_KEY_O, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0xF0, 0x00, }, // GRPH+O + { PS2_KEY_P, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0x8D, 0x00, }, // GRPH+P + { PS2_KEY_Q, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0xE0, 0x00, }, // GRPH+Q + { PS2_KEY_R, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0xE3, 0x00, }, // GRPH+R + { PS2_KEY_S, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0xE9, 0x00, }, // GRPH+S + { PS2_KEY_T, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0xE4, 0x00, }, // GRPH+T + { PS2_KEY_U, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0xE6, 0x00, }, // GRPH+U + { PS2_KEY_V, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0x83, 0x00, }, // GRPH+V + { PS2_KEY_W, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0xE1, 0x00, }, // GRPH+W + { PS2_KEY_X, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0x81, 0x00, }, // GRPH+X + { PS2_KEY_Y, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0xE5, 0x00, }, // GRPH+Y + { PS2_KEY_Z, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0x80, 0x00, }, // GRPH+Z + { PS2_KEY_COMMA, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0x87, 0x00, }, // GRPH+, + { PS2_KEY_SEMI, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0x89, 0x00, }, // GRPH+; + { PS2_KEY_DOT, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0x88, 0x00, }, // GRPH+. + { PS2_KEY_DIV, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0xFE, 0x00, }, // GRPH+/ + { PS2_KEY_MINUS, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0x8C, 0x00, }, // GRPH+- + { PS2_KEY_APOS, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0x8A, 0x00, }, // GRPH+' + { PS2_KEY_OPEN_SQ, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0xFC, 0x00, }, // GRPH+[ + { PS2_KEY_CLOSE_SQ, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0xE8, 0x00, }, // GRPH+] + { PS2_KEY_BACK, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0x90, 0x00, }, // GRPH+Backslash + { PS2_KEY_KP0, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0x8F, 0x00, }, // GRPH+Keypad 0 + { PS2_KEY_KP1, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0x99, 0x00, }, // GRPH+Keypad 1 + { PS2_KEY_KP2, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0x92, 0x00, }, // GRPH+Keypad 2 + { PS2_KEY_KP3, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0x98, 0x00, }, // GRPH+Keypad 3 + { PS2_KEY_KP4, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0x95, 0x00, }, // GRPH+Keypad 4 + { PS2_KEY_KP5, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0x96, 0x00, }, // GRPH+Keypad 5 + { PS2_KEY_KP6, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0x94, 0x00, }, // GRPH+Keypad 6 + { PS2_KEY_KP7, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0x9A, 0x00, }, // GRPH+Keypad 7 + { PS2_KEY_KP8, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0x93, 0x00, }, // GRPH+Keypad 8 + { PS2_KEY_KP9, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0x97, 0x00, }, // GRPH+Keypad 9 + { PS2_KEY_KP_DOT, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0x91, 0x00, }, // GRPH+Keypad Full stop . + { PS2_KEY_KP_PLUS, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0x9D, 0x00, }, // GRPH+Keypad Plus + + { PS2_KEY_KP_MINUS, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0x9C, 0x00, }, // GRPH+Keypad Minus - + { PS2_KEY_KP_TIMES, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0x9B, 0x00, }, // GRPH+Keypad Times * + { PS2_KEY_KP_DIV, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0x9E, 0x00, }, // GRPH+Keypad Divide / + { PS2_KEY_KP_ENTER, PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0x90, 0x00, }, // GRPH+Keypad Ebter / + // KANA (Alt) + //PS2 Code PS2 Ctrl (Flags to Match) Keyboard Model Machine MZ5665 Data MZ5665 Ctrl (Flags to Set). + { PS2_KEY_0, PS2CTRL_KANA | PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ5665_ALL, 0xA6, 0x00, }, // KANA+SHIFT+0 + { PS2_KEY_0, PS2CTRL_KANA, KEYMAP_STANDARD, MZ5665_ALL, 0xDC, 0x00, }, // KANA+0 + { PS2_KEY_1, PS2CTRL_KANA, KEYMAP_STANDARD, MZ5665_ALL, 0xC7, 0x00, }, // KANA+1 + { PS2_KEY_2, PS2CTRL_KANA, KEYMAP_STANDARD, MZ5665_ALL, 0xCC, 0x00, }, // KANA+2 + { PS2_KEY_3, PS2CTRL_KANA | PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ5665_ALL, 0xA7, 0x00, }, // KANA+SHIFT+3 + { PS2_KEY_3, PS2CTRL_KANA, KEYMAP_STANDARD, MZ5665_ALL, 0xB1, 0x00, }, // KANA+3 + { PS2_KEY_4, PS2CTRL_KANA | PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ5665_ALL, 0xA9, 0x00, }, // KANA+SHIFT+4 + { PS2_KEY_4, PS2CTRL_KANA, KEYMAP_STANDARD, MZ5665_ALL, 0xB3, 0x00, }, // KANA+4 + { PS2_KEY_5, PS2CTRL_KANA | PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ5665_ALL, 0xAA, 0x00, }, // KANA+SHIFT+5 + { PS2_KEY_5, PS2CTRL_KANA, KEYMAP_STANDARD, MZ5665_ALL, 0xB4, 0x00, }, // KANA+5 + { PS2_KEY_6, PS2CTRL_KANA | PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ5665_ALL, 0xAB, 0x00, }, // KANA+SHIFT+6 + { PS2_KEY_6, PS2CTRL_KANA, KEYMAP_STANDARD, MZ5665_ALL, 0xB5, 0x00, }, // KANA+6 + { PS2_KEY_7, PS2CTRL_KANA | PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ5665_ALL, 0xAC, 0x00, }, // KANA+SHIFT+7 + { PS2_KEY_7, PS2CTRL_KANA, KEYMAP_STANDARD, MZ5665_ALL, 0xD4, 0x00, }, // KANA+7 + { PS2_KEY_8, PS2CTRL_KANA | PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ5665_ALL, 0xAD, 0x00, }, // KANA+SHIFT+8 + { PS2_KEY_8, PS2CTRL_KANA, KEYMAP_STANDARD, MZ5665_ALL, 0xD5, 0x00, }, // KANA+8 + { PS2_KEY_9, PS2CTRL_KANA | PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ5665_ALL, 0xAE, 0x00, }, // KANA+SHIFT+9 + { PS2_KEY_9, PS2CTRL_KANA, KEYMAP_STANDARD, MZ5665_ALL, 0xD6, 0x00, }, // KANA+9 + { PS2_KEY_A, PS2CTRL_KANA, KEYMAP_STANDARD, MZ5665_ALL, 0xC1, 0x00, }, // KANA+A + { PS2_KEY_B, PS2CTRL_KANA, KEYMAP_STANDARD, MZ5665_ALL, 0xBA, 0x00, }, // KANA+B + { PS2_KEY_C, PS2CTRL_KANA, KEYMAP_STANDARD, MZ5665_ALL, 0xBF, 0x00, }, // KANA+C + { PS2_KEY_D, PS2CTRL_KANA, KEYMAP_STANDARD, MZ5665_ALL, 0xBC, 0x00, }, // KANA+D + { PS2_KEY_E, PS2CTRL_KANA | PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ5665_ALL, 0xA8, 0x00, }, // KANA+SHIFT+E + { PS2_KEY_E, PS2CTRL_KANA, KEYMAP_STANDARD, MZ5665_ALL, 0xB2, 0x00, }, // KANA+E + { PS2_KEY_F, PS2CTRL_KANA, KEYMAP_STANDARD, MZ5665_ALL, 0xCA, 0x00, }, // KANA+F + { PS2_KEY_G, PS2CTRL_KANA, KEYMAP_STANDARD, MZ5665_ALL, 0xB7, 0x00, }, // KANA+G + { PS2_KEY_H, PS2CTRL_KANA, KEYMAP_STANDARD, MZ5665_ALL, 0xB8, 0x00, }, // KANA+H + { PS2_KEY_I, PS2CTRL_KANA, KEYMAP_STANDARD, MZ5665_ALL, 0xC6, 0x00, }, // KANA+I + { PS2_KEY_J, PS2CTRL_KANA, KEYMAP_STANDARD, MZ5665_ALL, 0xCF, 0x00, }, // KANA+J + { PS2_KEY_K, PS2CTRL_KANA, KEYMAP_STANDARD, MZ5665_ALL, 0xC9, 0x00, }, // KANA+K + { PS2_KEY_L, PS2CTRL_KANA, KEYMAP_STANDARD, MZ5665_ALL, 0xD8, 0x00, }, // KANA+L + { PS2_KEY_M, PS2CTRL_KANA, KEYMAP_STANDARD, MZ5665_ALL, 0xD3, 0x00, }, // KANA+M + { PS2_KEY_N, PS2CTRL_KANA, KEYMAP_STANDARD, MZ5665_ALL, 0xD0, 0x00, }, // KANA+N + { PS2_KEY_O, PS2CTRL_KANA, KEYMAP_STANDARD, MZ5665_ALL, 0xD7, 0x00, }, // KANA+O + { PS2_KEY_P, PS2CTRL_KANA, KEYMAP_STANDARD, MZ5665_ALL, 0xBE, 0x00, }, // KANA+P + { PS2_KEY_Q, PS2CTRL_KANA, KEYMAP_STANDARD, MZ5665_ALL, 0xC0, 0x00, }, // KANA+Q + { PS2_KEY_R, PS2CTRL_KANA, KEYMAP_STANDARD, MZ5665_ALL, 0xBD, 0x00, }, // KANA+R + { PS2_KEY_S, PS2CTRL_KANA, KEYMAP_STANDARD, MZ5665_ALL, 0xC4, 0x00, }, // KANA+S + { PS2_KEY_T, PS2CTRL_KANA, KEYMAP_STANDARD, MZ5665_ALL, 0xB6, 0x00, }, // KANA+T + { PS2_KEY_U, PS2CTRL_KANA, KEYMAP_STANDARD, MZ5665_ALL, 0xC5, 0x00, }, // KANA+U + { PS2_KEY_V, PS2CTRL_KANA, KEYMAP_STANDARD, MZ5665_ALL, 0xCB, 0x00, }, // KANA+V + { PS2_KEY_W, PS2CTRL_KANA, KEYMAP_STANDARD, MZ5665_ALL, 0xC3, 0x00, }, // KANA+W + { PS2_KEY_X, PS2CTRL_KANA, KEYMAP_STANDARD, MZ5665_ALL, 0xBB, 0x00, }, // KANA+X + { PS2_KEY_Y, PS2CTRL_KANA, KEYMAP_STANDARD, MZ5665_ALL, 0xDD, 0x00, }, // KANA+Y + { PS2_KEY_Z, PS2CTRL_KANA | PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ5665_ALL, 0xAF, 0x00, }, // KANA+SHIFT+Z + { PS2_KEY_Z, PS2CTRL_KANA, KEYMAP_STANDARD, MZ5665_ALL, 0xC2, 0x00, }, // KANA+Z + { PS2_KEY_COMMA, PS2CTRL_KANA | PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ5665_ALL, 0xA4, 0x00, }, // KANA+SHIFT+, + { PS2_KEY_COMMA, PS2CTRL_KANA, KEYMAP_STANDARD, MZ5665_ALL, 0xC8, 0x00, }, // KANA+, + { PS2_KEY_SEMI, PS2CTRL_KANA, KEYMAP_STANDARD, MZ5665_ALL, 0xDA, 0x00, }, // KANA+; + { PS2_KEY_DOT, PS2CTRL_KANA | PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ5665_ALL, 0xA1, 0x00, }, // KANA+SHIFT+. + { PS2_KEY_DOT, PS2CTRL_KANA, KEYMAP_STANDARD, MZ5665_ALL, 0xD9, 0x00, }, // KANA+. + { PS2_KEY_DIV, PS2CTRL_KANA | PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ5665_ALL, 0xA5, 0x00, }, // KANA+SHIFT+/ + { PS2_KEY_DIV, PS2CTRL_KANA, KEYMAP_STANDARD, MZ5665_ALL, 0xD2, 0x00, }, // KANA+/ + { PS2_KEY_MINUS, PS2CTRL_KANA, KEYMAP_STANDARD, MZ5665_ALL, 0xCE, 0x00, }, // KANA+- + { PS2_KEY_APOS, PS2CTRL_KANA, KEYMAP_STANDARD, MZ5665_ALL, 0xDE, 0x00, }, // KANA+' + { PS2_KEY_OPEN_SQ, PS2CTRL_KANA | PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ5665_ALL, 0xA2, 0x00, }, // KANA+SHIFT+[ + { PS2_KEY_OPEN_SQ, PS2CTRL_KANA, KEYMAP_STANDARD, MZ5665_ALL, 0xDF, 0x00, }, // KANA+[ + { PS2_KEY_CLOSE_SQ, PS2CTRL_KANA | PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ5665_ALL, 0xA3, 0x00, }, // KANA+SHIFT+] + { PS2_KEY_CLOSE_SQ, PS2CTRL_KANA, KEYMAP_STANDARD, MZ5665_ALL, 0xD1, 0x00, }, // KANA+] + { PS2_KEY_BACK, PS2CTRL_KANA, KEYMAP_STANDARD, MZ5665_ALL, 0xDB, 0x00, }, // KANA+Backslash + { PS2_KEY_BS, PS2CTRL_KANA | PS2CTRL_SHIFT, KEYMAP_STANDARD, MZ5665_ALL, 0x12, 0x00, }, // KANA+SHIFT+Backspace + // Keypad. + { PS2_KEY_KP0, PS2CTRL_NONE, KEYMAP_STANDARD, MZ5665_ALL, '0', 0x00, }, // Keypad 0 + { PS2_KEY_KP1, PS2CTRL_NONE, KEYMAP_STANDARD, MZ5665_ALL, '1', 0x00, }, // Keypad 1 + { PS2_KEY_KP2, PS2CTRL_NONE, KEYMAP_STANDARD, MZ5665_ALL, '2', 0x00, }, // Keypad 2 + { PS2_KEY_KP3, PS2CTRL_NONE, KEYMAP_STANDARD, MZ5665_ALL, '3', 0x00, }, // Keypad 3 + { PS2_KEY_KP4, PS2CTRL_NONE, KEYMAP_STANDARD, MZ5665_ALL, '4', 0x00, }, // Keypad 4 + { PS2_KEY_KP5, PS2CTRL_NONE, KEYMAP_STANDARD, MZ5665_ALL, '5', 0x00, }, // Keypad 5 + { PS2_KEY_KP6, PS2CTRL_NONE, KEYMAP_STANDARD, MZ5665_ALL, '6', 0x00, }, // Keypad 6 + { PS2_KEY_KP7, PS2CTRL_NONE, KEYMAP_STANDARD, MZ5665_ALL, '7', 0x00, }, // Keypad 7 + { PS2_KEY_KP8, PS2CTRL_NONE, KEYMAP_STANDARD, MZ5665_ALL, '8', 0x00, }, // Keypad 8 + { PS2_KEY_KP9, PS2CTRL_NONE, KEYMAP_STANDARD, MZ5665_ALL, '9', 0x00, }, // Keypad 9 + { PS2_KEY_KP_COMMA, PS2CTRL_NONE, KEYMAP_STANDARD, MZ5665_ALL, ',', 0x00, }, // Keypad Comma , + { PS2_KEY_KP_DOT, PS2CTRL_NONE, KEYMAP_STANDARD, MZ5665_ALL, '.', 0x00, }, // Keypad Full stop . + { PS2_KEY_KP_PLUS, PS2CTRL_NONE, KEYMAP_STANDARD, MZ5665_ALL, '+', 0x00, }, // Keypad Plus + + { PS2_KEY_KP_MINUS, PS2CTRL_NONE, KEYMAP_STANDARD, MZ5665_ALL, '-', 0x00, }, // Keypad Minus - + { PS2_KEY_KP_TIMES, PS2CTRL_NONE, KEYMAP_STANDARD, MZ5665_ALL, '*', 0x00, }, // Keypad Times * + { PS2_KEY_KP_DIV, PS2CTRL_NONE, KEYMAP_STANDARD, MZ5665_ALL, '/', 0x00, }, // Keypad Divide / + { PS2_KEY_KP_ENTER, PS2CTRL_NONE, KEYMAP_STANDARD, MZ5665_ALL, 0x0D, 0x00, }, // Keypad Ebter / + //PS2 Code PS2 Ctrl (Flags to Match) Keyboard Model Machine MZ5665 Data MZ5665 Ctrl (Flags to Set). + // Special keys. + { PS2_KEY_PRTSCR, PS2CTRL_FUNC, KEYMAP_STANDARD, MZ5665_ALL, 0x00, 0x00, }, // ARGO KEY + { PS2_KEY_PAUSE, PS2CTRL_NONE, KEYMAP_STANDARD, MZ5665_ALL, 0x03, 0x00, }, // BREAK KEY + { PS2_KEY_L_GUI, PS2CTRL_FUNC | PS2CTRL_GUI, KEYMAP_STANDARD, MZ5665_ALL, 0x00, 0x00, }, // GRAPH KEY + //{ PS2_KEY_L_ALT, PS2CTRL_FUNC | PS2CTRL_KANA, KEYMAP_STANDARD, MZ5665_ALL, 0x00, 0x00, }, // KJ1 Sentence + //{ PS2_KEY_R_ALT, PS2CTRL_FUNC | PS2CTRL_GRAPH, KEYMAP_STANDARD, MZ5665_ALL, 0x00, 0x00, }, // KJ2 Transform + { PS2_KEY_R_GUI, PS2CTRL_FUNC | PS2CTRL_GUI, KEYMAP_STANDARD, MZ5665_ALL, 0x00, 0x00, }, // KANA KEY + { PS2_KEY_MENU, PS2CTRL_FUNC | PS2CTRL_GUI, KEYMAP_STANDARD, MZ5665_ALL, 0x00, 0x00, }, // Not assigned. + // Modifiers are last, only being selected if an earlier match isnt made. + { PS2_KEY_L_SHIFT, PS2CTRL_NONE, KEYMAP_STANDARD, MZ5665_ALL, 0x00, 0x00, }, + { PS2_KEY_R_SHIFT, PS2CTRL_NONE, KEYMAP_STANDARD, MZ5665_ALL, 0x00, 0x00, }, + { PS2_KEY_L_CTRL, PS2CTRL_NONE, KEYMAP_STANDARD, MZ5665_ALL, 0x00, 0x00, }, + { PS2_KEY_R_CTRL, PS2CTRL_NONE, KEYMAP_STANDARD, MZ5665_ALL, 0x00, 0x00, }, // Map to Control + { 0, PS2CTRL_NONE, KEYMAP_STANDARD, MZ5665_ALL, 0x00, 0x00, }, + }}; +}; + +#endif // MZ5665_H diff --git a/main/include/Mouse.h b/main/include/Mouse.h deleted file mode 120000 index 13f7c7c..0000000 --- a/main/include/Mouse.h +++ /dev/null @@ -1 +0,0 @@ -../../../sharpkey/main/include/Mouse.h \ No newline at end of file diff --git a/main/include/Mouse.h b/main/include/Mouse.h new file mode 100644 index 0000000..cf0f792 --- /dev/null +++ b/main/include/Mouse.h @@ -0,0 +1,151 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// Name: Mouse.h +// Created: Mar 2022 +// Version: v1.0 +// Author(s): Philip Smart +// Description: Header for the PS/2 Mouse to Sharp Host interface logic. +// 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 reflect changes realised in other modules due to addition of +// bluetooth and suspend logic due to NVS issues using both cores. +// Updates to reflect moving functionality into the HID and to support +// Bluetooth as a primary mouse or secondary mouse. +// +// 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 . +///////////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef MOUSE_H +#define MOUSE_H + +// Include the specification class. +#include "KeyInterface.h" +#include "NVS.h" +#include "HID.h" + +// NB: Macros definitions put inside class for clarity, they are still global scope. + +// Encapsulate the Mouse interface. +class Mouse : public KeyInterface { + + // Macros. + // + #define NUMELEM(a) (sizeof(a)/sizeof(a[0])) + + // Constants. + #define MOUSEIF_VERSION 1.02 + #define MAX_MOUSE_XMIT_KEY_BUF 128 + #define BITBANG_UART_BIT_TIME 208UL + + public: + + // Prototypes. + Mouse(void); + Mouse(uint32_t ifMode, NVS *hdlNVS, LED *hdlLED, HID *hdlHID); + Mouse(uint32_t ifMode, NVS *hdlNVS, LED *hdlLED, HID *hdlHID, bool secondaryIf); + Mouse(NVS *hdlNVShdlHID, HID *hdlHID); + ~Mouse(void); + void getMouseConfigTypes(std::vector& typeList); + bool getMouseSelectList(std::vector>& selectList, std::string option); + bool setMouseConfigValue(std::string paramName, std::string paramValue); + void mouseReceiveData(HID::t_mouseMessageElement mouseMessage); + bool persistConfig(void); + + // Method to return the class version number. + float version(void) + { + return(MOUSEIF_VERSION); + } + + protected: + + private: + // Prototypes. + IRAM_ATTR static void hostInterface( void * pvParameters ); + void init(uint32_t ifMode, NVS *hdlNVS, LED *hdlLED, HID *hdlHID); + void init(NVS *hdlNVS, HID *hdlHID); + + // Structure to maintain mouse interface configuration data. This data is persisted through powercycles as needed. + typedef struct { + struct { + // PS/2 Mouse data Adjustment and filtering options. + // + enum HID::HID_MOUSE_RESOLUTION resolution; + enum HID::HID_MOUSE_SCALING scaling; + enum HID::HID_MOUSE_SAMPLING sampleRate; + } mouse; + + struct { + // Host data for adjustment and configuration. + enum HID::HID_MOUSE_HOST_SCALING scaling; + } host; + + struct { + } params; + } t_mouseConfig; + + // Configuration data. + t_mouseConfig mouseConfig; + + // Structure to manage the Mouse control variables signifying the state of the Mouse. + typedef struct { + } t_msControl; + + // Mouse Control variables. + volatile t_msControl msCtrl; + + // Structure to manage the Sharp host control variables which define control and data mapping of the host interface and data sent. + // + typedef struct { + #ifdef CONFIG_HOST_HW_UART + int uartNum; + int uartBufferSize; + int uartQueueSize; + #endif + bool secondaryIf; // Mouse runs in tandem with a keyboard interface. + + // Data adjustment and processing options applied to the PS/2 data. + bool updated; + } t_hostControl; + + // Host Control variables. + volatile t_hostControl hostControl; + + // PS/2 to HOST serialiser buffer item. + typedef struct { + uint8_t xPos; + uint8_t yPos; + uint8_t status; + uint8_t wheel; + bool valid; + } t_xmitMessage; + + // Create an object for storing the data to be sent to the Host. This data has already been converted and adjusted from the incoming PS/2 message. + t_xmitMessage xmitMsg; + + // Thread handles - one per function, ie. ps/2 interface, host target interface, wifi interface. + TaskHandle_t TaskHostIF = NULL; + TaskHandle_t TaskHIDIF = NULL; + + // Spin lock mutex to hold a coresied to an uninterruptable method. This only works on dual core ESP32's. + portMUX_TYPE x1Mutex; +}; + +#endif // MOUSE_H diff --git a/main/include/NVS.h b/main/include/NVS.h deleted file mode 120000 index d2577cc..0000000 --- a/main/include/NVS.h +++ /dev/null @@ -1 +0,0 @@ -../../../sharpkey/main/include/NVS.h \ No newline at end of file diff --git a/main/include/NVS.h b/main/include/NVS.h new file mode 100644 index 0000000..a77553c --- /dev/null +++ b/main/include/NVS.h @@ -0,0 +1,162 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// Name: NVS.h +// Created: Mar 2022 +// Version: v1.0 +// Author(s): Philip Smart +// Description: Class definition to encapsulate the Espressif Non Volatile Storage into a thread safe +// object, The underlying API is supposed to be thread safe but experience has shown +// that two threads, each with there own handle can cause a lockup. +// Credits: +// Copyright: (c) 2019-2022 Philip Smart +// +// History: Mar 2022 - Initial write. +// v1.01 May 2022 - Initial release version. +// +// 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 . +///////////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef NVS_H +#define NVS_H + +#include +#include +#include +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/semphr.h" +#include "esp_log.h" +#include "esp_system.h" +#include "nvs_flash.h" +#include "nvs.h" +#include "soc/timer_group_struct.h" +#include "soc/timer_group_reg.h" +#include "driver/timer.h" + + +// NB: Macros definitions put inside class for clarity, they are still global scope. + +// Define a virtual class which acts as the base and specification of all super classes forming host +// interface objects. +class NVS { + + // Macros. + // + #define NUMELEM(a) (sizeof(a)/sizeof(a[0])) + + // Constants. + #define NVS_VERSION 1.01 + + public: + + // Prototypes. + NVS(void); + NVS(std::string keyName); + virtual ~NVS(void) {}; + void eraseAll(void); + void init(void); + bool takeMutex(void); + void giveMutex(void); + // Persistence. + bool open(std::string keyName); + bool persistData(const char *key, void *pData, uint32_t size); + bool retrieveData(const char *key, void *pData, uint32_t size); + bool commitData(void); + + // Helper method to identify the sub class, this is used in non volatile key management. + // Warning: This method wont work if optimisation for size is enabled on the compiler. + const char *getClassName(const std::string& prettyFunction) + { + // First find the CLASS :: METHOD seperation. + size_t colons = prettyFunction.find("::"); + + // None, then this is not a class. + if (colons == std::string::npos) + return "::"; + + // Split out the class name. + size_t begin = prettyFunction.substr(0,colons).rfind(" ") + 1; + size_t end = colons - begin; + + // Return the name. + return(prettyFunction.substr(begin,end).c_str()); + } + + // Helper method to change a file extension. + void replaceExt(std::string& fileName, const std::string& newExt) + { + // Locals. + std::string::size_type extPos = fileName.rfind('.', fileName.length()); + + if(extPos != std::string::npos) + { + fileName.replace(extPos+1, newExt.length(), newExt); + } + return; + } + + // Template to aid in conversion of an enum to integer. + template constexpr typename std::underlying_type::type to_underlying(E e) noexcept + { + return static_cast::type>(e); + } + + // Method to return the class version number. + virtual float version(void) + { + return(NVS_VERSION); + } + + // Method to return the name of the class. + virtual std::string ifName(void) + { + return(nvsCtrl.nvsClassName); + } + + // Method to return the name of the nvs key. + virtual std::string keyName(void) + { + return(nvsCtrl.nvsKeyName); + } + + protected: + + private: + + // Structure to maintain an active setting for the LED. The LED control thread uses these values to effect the required lighting of the LED. + typedef struct { + // Handle to the persistent storage api. + nvs_handle_t nvsHandle; + + // Name of the class for this instantiation. + std::string nvsClassName; + + // Name of the key under which NVS was opened. + std::string nvsKeyName; + + // Mutex to block access to limit one thread at a time. + SemaphoreHandle_t mutexInternal; + } t_nvsControl; + + // Var to store all NVS control variables. + t_nvsControl nvsCtrl; + +}; +#endif // NVS_H diff --git a/main/include/PC9801.h b/main/include/PC9801.h deleted file mode 120000 index cf20392..0000000 --- a/main/include/PC9801.h +++ /dev/null @@ -1 +0,0 @@ -../../../sharpkey/main/include/PC9801.h \ No newline at end of file diff --git a/main/include/PC9801.h b/main/include/PC9801.h new file mode 100644 index 0000000..aab4bc3 --- /dev/null +++ b/main/include/PC9801.h @@ -0,0 +1,529 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// Name: PC9801.h +// Created: Apr 2022 +// Version: v1.0 +// Author(s): Philip Smart +// Description: Header for the NEC PC-9801 to HID (PS/2, Bluetooth) interface logic. +// Credits: +// Copyright: (c) 2019-2022 Philip Smart +// +// History: Apr 2022 - Initial write. +// v1.01 Jun 2022 - Updates to reflect changes realised in other modules due to addition of +// bluetooth and suspend logic due to NVS issues using both cores. +// +// +// 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 . +///////////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef PC9801_H +#define PC9801_H + +// Include the specification class. +#include "KeyInterface.h" +#include "NVS.h" +#include "LED.h" +#include "HID.h" +#include +#include + +// NB: Macros definitions put inside class for clarity, they are still global scope. + +// Encapsulate the NEC PC-9801 interface. +class PC9801 : public KeyInterface { + // Macros. + // + #define NUMELEM(a) (sizeof(a)/sizeof(a[0])) + + // Constants. + #define PC9801IF_VERSION 1.00 + #define PC9801IF_KEYMAP_FILE "PC9801_KeyMap.BIN" + #define MAX_PC9801_XMIT_KEY_BUF 16 + #define MAX_PC9801_RCV_KEY_BUF 16 + + // NEC PC-9801 Key control bit mask. + #define PC9801_CTRL_SHIFT ((unsigned char) (1 << 5)) + #define PC9801_CTRL_RELEASESHIFT ((unsigned char) (1 << 4)) + #define PC9801_CTRL_CTRL ((unsigned char) (1 << 3)) + #define PC9801_CTRL_GRAPH ((unsigned char) (1 << 2)) + #define PC9801_CTRL_CAPS ((unsigned char) (1 << 1)) + #define PC9801_CTRL_KANA ((unsigned char) (1 << 0)) + #define PC9801_CTRL_NONE 0x00 + + // Special key definition. +// #define PC9801_KEY_UP 0x1E // ↑ +// #define PC9801_KEY_DOWN 0x1F // ↓ +// #define PC9801_KEY_LEFT 0x1D // ↠+// #define PC9801_KEY_RIGHT 0x1C // → → +// #define PC9801_KEY_INS 0x12 // INS +// #define PC9801_KEY_DEL 0x08 // DEL +// #define PC9801_KEY_CLR 0x0C // CLR +// #define PC9801_KEY_HOME 0x0B // HOME + + // PS2 Flag definitions. + #define PS2CTRL_NONE 0x00 // No keys active = 0 + #define PS2CTRL_SHIFT 0x01 // Shfit Key active = 1 + #define PS2CTRL_CTRL 0x02 // Ctrl Key active = 1 + #define PS2CTRL_CAPS 0x04 // CAPS active = 1 + #define PS2CTRL_KANA 0x08 // KANA active = 1 + #define PS2CTRL_GRAPH 0x10 // GRAPH active = 1 + #define PS2CTRL_GUI 0x20 // GUI Key active = 1 + #define PS2CTRL_FUNC 0x40 // Special Function Keys active = 1 + #define PS2CTRL_BREAK 0x80 // BREAK Key active = 1 + #define PS2CTRL_EXACT 0x80 // EXACT Match active = 1 + + // The initial mapping is made inside the PS2KeyAdvanced class from Scan Code Set 2 to ASCII + // for a selected keyboard. Special functions are detected and combined inside this module + // before mapping with the table below to extract the PC-9801 key code and control data. + // ie. PS/2 Scan Code -> ASCII + Flags -> PC-9801 Key Code + Ctrl Data + #define PS2TBL_PC9801_MAXCOLS 6 + #define PS2TBL_PC9801_MAXROWS 131 + + // The initial mapping is made inside the PS2KeyAdvanced class from Scan Code Set 2 to ASCII + // for a selected keyboard. Special functions are detected and combined inside this module + // before mapping with the table below to extract the NEC PC-9801 key code and control data. + // ie. PS/2 Scan Code -> ASCII + Flags -> NEC PC-9801 Key Code + Ctrl Data + + // Keyboard mapping table column names. + #define PS2TBL_PS2KEYCODE_NAME "PS/2 KeyCode" + #define PS2TBL_PS2CTRL_NAME "PS/2 Control Key" + #define PS2TBL_KEYBOARDMODEL_NAME "For Keyboard" + #define PS2TBL_MACHINE_NAME "For Host Model" + #define PS2TBL_PC9801_KEYCODE_NAME "PC9801 KeyCode" + #define PS2TBL_PC9801__CTRL_NAME "PC9801 Control Key" + + // Keyboard mapping table column types. + #define PS2TBL_PS2KEYCODE_TYPE "hex" + #define PS2TBL_PS2CTRL_TYPE "custom_cbp_ps2ctrl" + #define PS2TBL_KEYBOARDMODEL_TYPE "custom_cbp_keybmodel" + #define PS2TBL_MACHINE_TYPE "custom_cbp_machine" + #define PS2TBL_PC9801_KEYCODE_TYPE "hex" + #define PS2TBL_PC9801_CTRL_TYPE "custom_cbn_x1ctrl" + + // Keyboard mapping table select list for PS2CTRL. + #define PS2TBL_PS2CTRL_SEL_NONE "NONE" + #define PS2TBL_PS2CTRL_SEL_SHIFT "SHIFT" + #define PS2TBL_PS2CTRL_SEL_CTRL "CTRL" + #define PS2TBL_PS2CTRL_SEL_CAPS "CAPS" + #define PS2TBL_PS2CTRL_SEL_KANA "KANA" + #define PS2TBL_PS2CTRL_SEL_GRAPH "GRAPH" + #define PS2TBL_PS2CTRL_SEL_GUI "GUI" + #define PS2TBL_PS2CTRL_SEL_FUNC "FUNC" + #define PS2TBL_PS2CTRL_SEL_EXACT "EXACT" + + // Keyboard mapping table select list for target machine. + #define PC9801_SEL_ALL "ALL" + #define PC9801_SEL_ORIG "ORIGINAL" + + // Keyboard mapping table select list for Model of keyboard. + #define KEYMAP_SEL_STANDARD "ALL" + #define KEYMAP_SEL_UK_WYSE_KB3926 "UK_WYSE_KB3926" + #define KEYMAP_SEL_JAPAN_OADG109 "JAPAN_OADG109" + #define KEYMAP_SEL_JAPAN_SANWA_SKBL1 "JAPAN_SANWA_SKBL1" + #define KEYMAP_SEL_NOT_ASSIGNED_4 "KEYBOARD_4" + #define KEYMAP_SEL_NOT_ASSIGNED_5 "KEYBOARD_5" + #define KEYMAP_SEL_NOT_ASSIGNED_6 "KEYBOARD_6" + #define KEYMAP_SEL_UK_PERIBOARD_810 "UK_PERIBOARD_810" + #define KEYMAP_SEL_UK_OMOTON_K8508 "UK_OMOTON_K8508" + + // Keyboard mapping table select list for PC9801 Control codes. + #define PC9801_CTRL_SEL_GRAPH "GRAPH" + #define PC9801_CTRL_SEL_CAPS "CAPS" + #define PC9801_CTRL_SEL_KANA "KANA" + #define PC9801_CTRL_SEL_SHIFT "SHIFT" + #define PC9801_CTRL_SEL_CTRL "CTRL" + + // The NEC PC-9801 Series was released over a number of years and each iteration added changes/updates. In order to cater for differences, it is possible to assign a key mapping + // to a specific machine type(s) or all of the series by adding the flags below into the mapping table. + #define PC9801_ALL 0xFF + + // Keyboard models. The base on which this interface was created was a Wyse KB3926 PS/2 Keyboard and this is deemed STANDARD. Other models need to insert difference maps + // prior to the STANDARD entry along with the keyboard model so that it is processed first thus allowing differing keyboards with different maps. + #define KEYMAP_STANDARD 0xFF + #define KEYMAP_UK_WYSE_KB3926 0x01 + #define KEYMAP_JAPAN_OADG109 0x02 + #define KEYMAP_JAPAN_SANWA_SKBL1 0x04 + #define KEYMAP_NOT_ASSIGNED_4 0x08 + #define KEYMAP_NOT_ASSIGNED_5 0x10 + #define KEYMAP_NOT_ASSIGNED_6 0x20 + #define KEYMAP_UK_PERIBOARD_810 0x40 + #define KEYMAP_UK_OMOTON_K8508 0x80 + + // PC-9801 Scan codes - PS2 codes along with function keys (SHIFT, CTRL etc) are mapped to the X68000 scan codes below. + #define PC9801_KEY_ESC 0x00 + #define PC9801_KEY_0 0x0A + #define PC9801_KEY_1 0x01 + #define PC9801_KEY_2 0x02 + #define PC9801_KEY_3 0x03 + #define PC9801_KEY_4 0x04 + #define PC9801_KEY_5 0x05 + #define PC9801_KEY_6 0x06 + #define PC9801_KEY_7 0x07 + #define PC9801_KEY_8 0x08 + #define PC9801_KEY_9 0x09 + #define PC9801_KEY_A 0x1D + #define PC9801_KEY_B 0x2D + #define PC9801_KEY_C 0x2B + #define PC9801_KEY_D 0x1F + #define PC9801_KEY_E 0x12 + #define PC9801_KEY_F 0x20 + #define PC9801_KEY_G 0x21 + #define PC9801_KEY_H 0x22 + #define PC9801_KEY_I 0x17 + #define PC9801_KEY_J 0x23 + #define PC9801_KEY_K 0x24 + #define PC9801_KEY_L 0x25 + #define PC9801_KEY_M 0x2F + #define PC9801_KEY_N 0x2E + #define PC9801_KEY_O 0x18 + #define PC9801_KEY_P 0x19 + #define PC9801_KEY_Q 0x10 + #define PC9801_KEY_R 0x13 + #define PC9801_KEY_S 0x1E + #define PC9801_KEY_T 0x14 + #define PC9801_KEY_U 0x16 + #define PC9801_KEY_V 0x2C + #define PC9801_KEY_W 0x11 + #define PC9801_KEY_X 0x2A + #define PC9801_KEY_Y 0x15 + #define PC9801_KEY_Z 0x29 + #define PC9801_KEY_AT 0x1A // Requires SHIFT + #define PC9801_KEY_MINUS 0x0B + #define PC9801_KEY_CIRCUMFLEX 0x0C + #define PC9801_KEY_YEN 0x0D + #define PC9801_KEY_BS 0x0E + #define PC9801_KEY_TAB 0x0F + #define PC9801_KEY_OPEN_SQ 0x1A + #define PC9801_KEY_CLOSE_SQ 0x1B + #define PC9801_KEY_RETURN 0x1C + #define PC9801_KEY_SEMI 0x26 + #define PC9801_KEY_COLON 0x27 + #define PC9801_KEY_COMMA 0x30 + #define PC9801_KEY_DOT 0x31 + #define PC9801_KEY_DIV 0x32 + #define PC9801_KEY_UNDERLINE 0x0D // Requires SHIFT + #define PC9801_KEY_SPACE 0x34 + #define PC9801_KEY_HOME 0x3E + #define PC9801_KEY_ROLLUP 0x36 + #define PC9801_KEY_ROLLDN 0x37 + #define PC9801_KEY_UNDO 0x3 // Not known3 + #define PC9801_KEY_L_ARROW 0x3B + #define PC9801_KEY_UP_ARROW 0x3A + #define PC9801_KEY_R_ARROW 0x3C + #define PC9801_KEY_DN_ARROW 0x3D + #define PC9801_KEY_CLR 0x3F // Not known + #define PC9801_KEY_KP0 0x4E + #define PC9801_KEY_KP1 0x4A + #define PC9801_KEY_KP2 0x4B + #define PC9801_KEY_KP3 0x4C + #define PC9801_KEY_KP4 0x46 + #define PC9801_KEY_KP5 0x47 + #define PC9801_KEY_KP6 0x48 + #define PC9801_KEY_KP7 0x42 + #define PC9801_KEY_KP8 0x43 + #define PC9801_KEY_KP9 0x44 + #define PC9801_KEY_KP_DIV 0x41 + #define PC9801_KEY_KP_TIMES 0x45 + #define PC9801_KEY_KP_MINUS 0x4D + #define PC9801_KEY_KP_PLUS 0x49 + #define PC9801_KEY_KP_EQUAL 0x4D + #define PC9801_KEY_KP_ENTER 0x1C + #define PC9801_KEY_KP_COMMA 0x4F + #define PC9801_KEY_KP_DOT 0x50 + #define PC9801_KEY_SYMBOL 0x52 // Not known + #define PC9801_KEY_HELP 0x3F + #define PC9801_KEY_CAPS 0x71 + #define PC9801_KEY_INS 0x38 + #define PC9801_KEY_DEL 0x39 + #define PC9801_KEY_BREAK 0x60 // Stop + #define PC9801_KEY_COPY 0x61 + #define PC9801_KEY_SHIFT 0x70 + #define PC9801_KEY_R_SHIFT 0x7D + #define PC9801_KEY_CTRL 0x74 + #define PC9801_KEY_GRAPH 0x73 + #define PC9801_KEY_XFER 0x35 + #define PC9801_KEY_NFER 0x51 + #define PC9801_KEY_KATAKANA 0x72 + #define PC9801_KEY_ROMAJI 0x33 + #define PC9801_KEY_F1 0x62 + #define PC9801_KEY_F2 0x63 + #define PC9801_KEY_F3 0x64 + #define PC9801_KEY_F4 0x65 + #define PC9801_KEY_F5 0x66 + #define PC9801_KEY_F6 0x67 + #define PC9801_KEY_F7 0x68 + #define PC9801_KEY_F8 0x69 + #define PC9801_KEY_F9 0x6A + #define PC9801_KEY_F10 0x6B + #define PC9801_KEY_F11 0x52 + #define PC9801_KEY_F12 0x53 + #define PC9801_KEY_F13 0x54 + #define PC9801_KEY_F14 0x55 + #define PC9801_KEY_F15 0x56 + #define PC9801_KEY_NULL 0xFF + + public: + // Prototypes. + PC9801(void); + PC9801(uint32_t ifMode, NVS *hdlNVS, LED *hdlLED, HID *hdlHID, const char *fsPath); + PC9801(NVS *hdlNVS, HID *hdlHID, const char *fsPath); + ~PC9801(void); + bool createKeyMapFile(std::fstream &outFile); + bool storeDataToKeyMapFile(std::fstream &outFile, char *data, int size); + bool storeDataToKeyMapFile(std::fstream & outFile, std::vector& dataArray); + bool closeAndCommitKeyMapFile(std::fstream &outFile, bool cleanupOnly); + std::string getKeyMapFileName(void) { return(PC9801IF_KEYMAP_FILE); }; + void getKeyMapHeaders(std::vector& headerList); + void getKeyMapTypes(std::vector& typeList); + bool getKeyMapSelectList(std::vector>& selectList, std::string option); + bool getKeyMapData(std::vector& dataArray, int *row, bool start); + + // Method to return the class version number. + float version(void) + { + return(PC9801IF_VERSION); + } + + protected: + + private: + // Prototypes. + IRAM_ATTR void pushKeyToQueue(uint32_t key); + IRAM_ATTR void pushHostCmdToQueue(uint8_t cmd); + IRAM_ATTR static void pcInterface( void * pvParameters ); + IRAM_ATTR static void hidInterface( void * pvParameters ); + void selectOption(uint8_t optionCode); + uint32_t mapKey(uint16_t scanCode); + bool loadKeyMap(); + bool saveKeyMap(void); + void init(uint32_t ifMode, NVS *hdlNVS, LED *hdlLED, HID *hdlHID); + void init(NVS *hdlNVS, HID *hdlHID); + + // Structure to encapsulate a single key map from PS/2 to NEC PC-9801 + typedef struct { + uint8_t ps2KeyCode; + uint8_t ps2Ctrl; + uint8_t keyboardModel; + uint8_t machine; + uint8_t pcKey; + uint8_t pcCtrl; + } t_keyMapEntry; + + // Structure to encapsulate the entire static keyboard mapping table. + typedef struct { + t_keyMapEntry kme[PS2TBL_PC9801_MAXROWS]; + } t_keyMap; + + // Structure to maintain the NEC PC-9801 interface configuration data. This data is persisted through powercycles as needed. + typedef struct { + struct { + uint8_t activeKeyboardMap; // Model of keyboard a keymap entry is applicable to. + uint8_t activeMachineModel; // Machine model a keymap entry is applicable to. + bool useOnlyPersisted; // Flag to indicate wether the inbuilt keymap array should be combined with persisted values or the inbuilt array is ignored and only persisted values used. + } params; + } t_pcConfig; + + // Configuration data. + t_pcConfig pcConfig; + + // Structure to manage the control signals signifying the state of the NEC PC-9801 keyboard. + typedef struct { + uint8_t keyCtrl; // Keyboard state flag control. + bool optionSelect; // Flag to indicate a user requested keyboard configuration option is being selected. + int uartNum; + int uartBufferSize; + int uartQueueSize; + + std::string fsPath; // Path on the underlying filesystem where storage is mounted and accessible. + t_keyMapEntry *kme; // Pointer to an array in memory to contain PS2 to NEC PC-9801 mapping values. + int kmeRows; // Number of rows in the kme table. + std::string keyMapFileName; // Name of file where extension or replacement key map entries are stored. + bool persistConfig; // Flag to request saving of the config into NVS storage. + } t_pcControl; + + // Transmit buffer queue item. + typedef struct { + uint32_t keyCode; // Key data to be sent to PC-9801, 4 bytes to allow for extended sequences.. + } t_xmitQueueMessage; + + // Receive buffer queue item. + typedef struct { + uint8_t hostCmd; // Keyboard configuration command received from X68000. + } t_rcvQueueMessage; + + // Thread handles - one per function, ie. HID interface and host target interface. + TaskHandle_t TaskHostIF = NULL; + TaskHandle_t TaskHIDIF = NULL; + + // Control structure to control interaction and mapping of keys for the host. + t_pcControl pcCtrl; + + // Spin lock mutex to hold a coresied to an uninterruptable method. This only works on dual core ESP32's. + portMUX_TYPE pcMutex; + + // + // This mapping is for the UK Wyse KB-3926 PS/2 keyboard + // + t_keyMap PS2toPC9801 = { + { + //PS2 Code PS2 Ctrl (Flags to Match) Keyboard Model Machine PC-9801 Data PC-9801 Ctrl (Flags to Set). + // Function keys + // { PS2_KEY_F1, PS2CTRL_FUNC | PS2CTRL_CTRL, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_HIRAGANA, PC9801_CTRL_NONE, }, // CTRL + F1 = Hiragana + // { PS2_KEY_F2, PS2CTRL_FUNC | PS2CTRL_CTRL, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_FULLWIDTH, PC9801_CTRL_NONE, }, // CTRL + F2 = Full Width + // { PS2_KEY_F3, PS2CTRL_FUNC | PS2CTRL_CTRL, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_KATAKANA, PC9801_CTRL_NONE, }, // CTRL + F3 = Katakana + // { PS2_KEY_F4, PS2CTRL_FUNC | PS2CTRL_CTRL, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_ROMAJI, PC9801_CTRL_NONE, }, // CTRL + F4 = Romaji + // { PS2_KEY_F5, PS2CTRL_FUNC | PS2CTRL_CTRL, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_TRANSPOSE, PC9801_CTRL_NONE, }, // CTRL + F5 = Tranpose + // { PS2_KEY_F6, PS2CTRL_FUNC | PS2CTRL_CTRL, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_SYMBOL, PC9801_CTRL_NONE, }, // CTRL + F6 = Symbol + // { PS2_KEY_F7, PS2CTRL_FUNC | PS2CTRL_CTRL, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_REGISTRATION, PC9801_CTRL_NONE, }, // CTRL + F7 = Registration - maybe a poor translation, needs better one! + // { PS2_KEY_F9, PS2CTRL_FUNC | PS2CTRL_CTRL, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_COPY, PC9801_CTRL_NONE, }, // CTRL + F9 = Copy + // { PS2_KEY_F10, PS2CTRL_FUNC | PS2CTRL_CTRL, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_HELP, PC9801_CTRL_NONE, }, // CTRL + F10 = Help + { PS2_KEY_F1, PS2CTRL_FUNC, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_F1, PC9801_CTRL_NONE, }, // F1 + { PS2_KEY_F2, PS2CTRL_FUNC, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_F2, PC9801_CTRL_NONE, }, // F2 + { PS2_KEY_F3, PS2CTRL_FUNC, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_F3, PC9801_CTRL_NONE, }, // F3 + { PS2_KEY_F4, PS2CTRL_FUNC, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_F4, PC9801_CTRL_NONE, }, // F4 + { PS2_KEY_F5, PS2CTRL_FUNC, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_F5, PC9801_CTRL_NONE, }, // F5 + { PS2_KEY_F6, PS2CTRL_FUNC, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_F6, PC9801_CTRL_NONE, }, // F6 + { PS2_KEY_F7, PS2CTRL_FUNC, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_F7, PC9801_CTRL_NONE, }, // F7 + { PS2_KEY_F8, PS2CTRL_FUNC, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_F8, PC9801_CTRL_NONE, }, // F8 + { PS2_KEY_F9, PS2CTRL_FUNC, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_F9, PC9801_CTRL_NONE, }, // F9 + { PS2_KEY_F10, PS2CTRL_FUNC, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_F10, PC9801_CTRL_NONE, }, // F10 + // { PS2_KEY_F11, PS2CTRL_FUNC, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_OPT_1, PC9801_CTRL_NONE, }, // F11 - OPT.1 + // { PS2_KEY_F12, PS2CTRL_FUNC, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_OPT_2, PC9801_CTRL_NONE, }, // F12 - OPT.2 + //PS2 Code PS2 Ctrl (Flags to Match) Machine PC-9801 Data PC-9801 Ctrl (Flags to Set). + // ALPHA keys, case is maaped in the PC-9801 via the SHIFT key event or CAPS key. + { PS2_KEY_A, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_A, PC9801_CTRL_NONE, }, // A + { PS2_KEY_B, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_B, PC9801_CTRL_NONE, }, // B + { PS2_KEY_C, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_C, PC9801_CTRL_NONE, }, // C + { PS2_KEY_D, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_D, PC9801_CTRL_NONE, }, // D + { PS2_KEY_E, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_E, PC9801_CTRL_NONE, }, // E + { PS2_KEY_F, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_F, PC9801_CTRL_NONE, }, // F + { PS2_KEY_G, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_G, PC9801_CTRL_NONE, }, // G + { PS2_KEY_H, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_H, PC9801_CTRL_NONE, }, // H + { PS2_KEY_I, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_I, PC9801_CTRL_NONE, }, // I + { PS2_KEY_J, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_J, PC9801_CTRL_NONE, }, // J + { PS2_KEY_K, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_K, PC9801_CTRL_NONE, }, // K + { PS2_KEY_L, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_L, PC9801_CTRL_NONE, }, // L + { PS2_KEY_M, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_M, PC9801_CTRL_NONE, }, // M + { PS2_KEY_N, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_N, PC9801_CTRL_NONE, }, // N + { PS2_KEY_O, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_O, PC9801_CTRL_NONE, }, // O + { PS2_KEY_P, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_P, PC9801_CTRL_NONE, }, // P + { PS2_KEY_Q, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_Q, PC9801_CTRL_NONE, }, // Q + { PS2_KEY_R, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_R, PC9801_CTRL_NONE, }, // R + { PS2_KEY_S, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_S, PC9801_CTRL_NONE, }, // S + { PS2_KEY_T, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_T, PC9801_CTRL_NONE, }, // T + { PS2_KEY_U, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_U, PC9801_CTRL_NONE, }, // U + { PS2_KEY_V, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_V, PC9801_CTRL_NONE, }, // V + { PS2_KEY_W, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_W, PC9801_CTRL_NONE, }, // W + { PS2_KEY_X, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_X, PC9801_CTRL_NONE, }, // X + { PS2_KEY_Y, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_Y, PC9801_CTRL_NONE, }, // Y + { PS2_KEY_Z, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_Z, PC9801_CTRL_NONE, }, // Z + // Numeric keys. + { PS2_KEY_0, PS2CTRL_SHIFT, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_9, PC9801_CTRL_NONE, }, // Close Bracket ) + { PS2_KEY_0, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_0, PC9801_CTRL_NONE, }, // 0 + { PS2_KEY_1, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_1, PC9801_CTRL_NONE, }, // 1 + { PS2_KEY_2, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_2, PC9801_CTRL_NONE, }, // 2 + { PS2_KEY_3, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_3, PC9801_CTRL_NONE, }, // 3 + { PS2_KEY_4, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_4, PC9801_CTRL_NONE, }, // 4 + { PS2_KEY_5, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_5, PC9801_CTRL_NONE, }, // 5 + { PS2_KEY_6, PS2CTRL_SHIFT, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_CIRCUMFLEX, PC9801_CTRL_RELEASESHIFT, }, // Circumflex ^ + { PS2_KEY_6, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_6, PC9801_CTRL_NONE, }, // 6 + { PS2_KEY_7, PS2CTRL_SHIFT, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_6, PC9801_CTRL_NONE, }, // & + { PS2_KEY_7, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_7, PC9801_CTRL_NONE, }, // 7 + { PS2_KEY_8, PS2CTRL_SHIFT, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_COLON, PC9801_CTRL_NONE, }, // Start * + { PS2_KEY_8, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_8, PC9801_CTRL_NONE, }, // 8 + { PS2_KEY_9, PS2CTRL_SHIFT, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_8, PC9801_CTRL_NONE, }, // Open Bracket ( + { PS2_KEY_9, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_9, PC9801_CTRL_NONE, }, // 9 + //PS2 Code PS2 Ctrl (Flags to Match) Machine PC-9801 Data PC-9801 Ctrl (Flags to Set). + // Punctuation keys. + { PS2_KEY_SPACE, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_SPACE, PC9801_CTRL_NONE, }, // Space + { PS2_KEY_MINUS, PS2CTRL_SHIFT, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_CIRCUMFLEX, PC9801_CTRL_NONE, }, // Upper Bar + { PS2_KEY_MINUS, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_MINUS, PC9801_CTRL_NONE, }, // Minus - + { PS2_KEY_EQUAL, PS2CTRL_SHIFT, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_SEMI, PC9801_CTRL_SHIFT, }, // Plus + + { PS2_KEY_EQUAL, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_MINUS, PC9801_CTRL_SHIFT, }, // Equal = + { PS2_KEY_DOT, PS2CTRL_SHIFT, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_DOT, PC9801_CTRL_NONE, }, // Greater Than > + { PS2_KEY_DOT, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_DOT, PC9801_CTRL_NONE, }, // Dot + { PS2_KEY_DIV, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_DIV, PC9801_CTRL_NONE, }, // Divide / + { PS2_KEY_SEMI, PS2CTRL_SHIFT, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_COLON, PC9801_CTRL_RELEASESHIFT, }, // Colon : + { PS2_KEY_SEMI, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_SEMI, PC9801_CTRL_NONE, }, // Semi-Colon ; + { PS2_KEY_OPEN_SQ, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_OPEN_SQ, PC9801_CTRL_NONE, }, // [ + { PS2_KEY_CLOSE_SQ, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_CLOSE_SQ, PC9801_CTRL_NONE, }, // ] + { PS2_KEY_APOS, PS2CTRL_SHIFT, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_AT, PC9801_CTRL_RELEASESHIFT, }, // @ + { PS2_KEY_APOS, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_7, PC9801_CTRL_SHIFT, }, // ' + { PS2_KEY_BACK, PS2CTRL_SHIFT, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_YEN, PC9801_CTRL_NONE, }, // Back slash maps to Yen + { PS2_KEY_BACK, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_YEN, PC9801_CTRL_NONE, }, // Back slash maps to Yen + { PS2_KEY_HASH, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_3, PC9801_CTRL_SHIFT, }, // Hash + { PS2_KEY_COMMA, PS2CTRL_SHIFT, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_COMMA, PC9801_CTRL_NONE, }, // Less Than < + { PS2_KEY_COMMA, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_COMMA, PC9801_CTRL_NONE, }, // Comma , + { PS2_KEY_BTICK, PS2CTRL_FUNC | PS2CTRL_SHIFT, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_UNDERLINE, PC9801_CTRL_SHIFT, }, // Underline + { PS2_KEY_BTICK, PS2CTRL_FUNC, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_AT, PC9801_CTRL_SHIFT, }, // Back Tick ` + // Control keys. + { PS2_KEY_TAB, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_TAB, PC9801_CTRL_NONE, }, // TAB + { PS2_KEY_BS, PS2CTRL_FUNC, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_BS, PC9801_CTRL_NONE, }, // Backspace + { PS2_KEY_ESC, PS2CTRL_FUNC, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_ESC, PC9801_CTRL_NONE, }, // ESCape + { PS2_KEY_INSERT, PS2CTRL_FUNC, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_INS, PC9801_CTRL_NONE, }, // INSERT + { PS2_KEY_HOME, PS2CTRL_FUNC | PS2CTRL_SHIFT, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_CLR, PC9801_CTRL_NONE, }, // CLR + { PS2_KEY_HOME, PS2CTRL_FUNC, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_HOME, PC9801_CTRL_NONE, }, // HOME + { PS2_KEY_DELETE, PS2CTRL_FUNC, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_DEL, PC9801_CTRL_NONE, }, // DELETE + { PS2_KEY_UP_ARROW, PS2CTRL_FUNC, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_UP_ARROW, PC9801_CTRL_NONE, }, // Up Arrow + { PS2_KEY_L_ARROW, PS2CTRL_FUNC, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_L_ARROW, PC9801_CTRL_NONE, }, // Left Arrow + { PS2_KEY_DN_ARROW, PS2CTRL_FUNC, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_DN_ARROW, PC9801_CTRL_NONE, }, // Down Arrow + { PS2_KEY_R_ARROW, PS2CTRL_FUNC, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_R_ARROW, PC9801_CTRL_NONE, }, // Right Arrow + { PS2_KEY_PGUP, PS2CTRL_FUNC, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_ROLLUP, PC9801_CTRL_NONE, }, // Roll Up. + { PS2_KEY_PGDN, PS2CTRL_FUNC, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_ROLLDN, PC9801_CTRL_NONE, }, // Roll Down + { PS2_KEY_SCROLL, PS2CTRL_FUNC, KEYMAP_STANDARD, PC9801_ALL, ' ', PC9801_CTRL_NONE, }, // Not assigned. + { PS2_KEY_ENTER, PS2CTRL_FUNC, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_RETURN, PC9801_CTRL_NONE, }, // Not assigned. + { PS2_KEY_CAPS, PS2CTRL_CAPS, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_CAPS, PC9801_CTRL_NONE, }, // CAPS + { PS2_KEY_END, PS2CTRL_FUNC, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_UNDO, PC9801_CTRL_NONE, }, // UNDO + // Keypad. + { PS2_KEY_KP0, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_KP0, PC9801_CTRL_NONE, }, // Keypad 0 + { PS2_KEY_KP1, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_KP1, PC9801_CTRL_NONE, }, // Keypad 1 + { PS2_KEY_KP2, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_KP2, PC9801_CTRL_NONE, }, // Keypad 2 + { PS2_KEY_KP3, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_KP3, PC9801_CTRL_NONE, }, // Keypad 3 + { PS2_KEY_KP4, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_KP4, PC9801_CTRL_NONE, }, // Keypad 4 + { PS2_KEY_KP5, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_KP5, PC9801_CTRL_NONE, }, // Keypad 5 + { PS2_KEY_KP6, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_KP6, PC9801_CTRL_NONE, }, // Keypad 6 + { PS2_KEY_KP7, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_KP7, PC9801_CTRL_NONE, }, // Keypad 7 + { PS2_KEY_KP8, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_KP8, PC9801_CTRL_NONE, }, // Keypad 8 + { PS2_KEY_KP9, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_KP9, PC9801_CTRL_NONE, }, // Keypad 9 + { PS2_KEY_KP_COMMA, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_KP_COMMA, PC9801_CTRL_NONE, }, // Keypad Comma , + { PS2_KEY_KP_DOT, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_KP_DOT, PC9801_CTRL_NONE, }, // Keypad Full stop . + { PS2_KEY_KP_PLUS, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_KP_PLUS, PC9801_CTRL_NONE, }, // Keypad Plus + + { PS2_KEY_KP_MINUS, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_KP_MINUS, PC9801_CTRL_NONE, }, // Keypad Minus - + { PS2_KEY_KP_TIMES, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_KP_TIMES, PC9801_CTRL_NONE, }, // Keypad Times * + { PS2_KEY_KP_DIV, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_KP_DIV, PC9801_CTRL_NONE, }, // Keypad Divide / + { PS2_KEY_KP_EQUAL, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_MINUS, PC9801_CTRL_SHIFT, }, // Keypad Equal = + { PS2_KEY_KP_ENTER, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_KP_ENTER, PC9801_CTRL_NONE, }, // Keypad Ebter / + { PS2_KEY_KP_ENTER, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_KP_EQUAL, PC9801_CTRL_NONE, }, // Keypad Ebter / + //PS2 Code PS2 Ctrl (Flags to Match) Machine PC-9801 Data PC-9801 Ctrl (Flags to Set). + // Special keys. + { PS2_KEY_PRTSCR, PS2CTRL_FUNC, KEYMAP_STANDARD, PC9801_ALL, 0x00, PC9801_CTRL_NONE, }, // + { PS2_KEY_PAUSE, PS2CTRL_FUNC | PS2CTRL_SHIFT, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_BREAK, PC9801_CTRL_RELEASESHIFT, }, // BREAK KEY + // { PS2_KEY_L_GUI, PS2CTRL_FUNC, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_XF1, PC9801_CTRL_NONE, }, // XF1 + // { PS2_KEY_L_ALT, PS2CTRL_FUNC, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_XF2, PC9801_CTRL_NONE, }, // XF2 + // { PS2_KEY_R_ALT, PS2CTRL_FUNC, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_XF3, PC9801_CTRL_NONE, }, // XF3 + // { PS2_KEY_R_GUI, PS2CTRL_FUNC, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_XF4, PC9801_CTRL_NONE, }, // XF4 + // { PS2_KEY_MENU, PS2CTRL_FUNC, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_XF5, PC9801_CTRL_NONE, }, // XF5 + // Modifiers are last, only being selected if an earlier match isnt made. + { PS2_KEY_L_SHIFT, PS2CTRL_FUNC, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_SHIFT, PC9801_CTRL_NONE, }, // + { PS2_KEY_R_SHIFT, PS2CTRL_FUNC, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_SHIFT, PC9801_CTRL_NONE, }, // + { PS2_KEY_L_CTRL, PS2CTRL_FUNC, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_CTRL, PC9801_CTRL_NONE, }, // Map to Control + { PS2_KEY_R_CTRL, PS2CTRL_FUNC, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_CTRL, PC9801_CTRL_NONE, }, // Map to Control + { 0, PS2CTRL_NONE, KEYMAP_STANDARD, PC9801_ALL, PC9801_KEY_NULL, PC9801_CTRL_NONE, }, // + }}; +}; + +#endif // PC9801_H diff --git a/main/include/PS2KeyAdvanced.h b/main/include/PS2KeyAdvanced.h deleted file mode 120000 index 1cd7d19..0000000 --- a/main/include/PS2KeyAdvanced.h +++ /dev/null @@ -1 +0,0 @@ -../../../sharpkey/main/include/PS2KeyAdvanced.h \ No newline at end of file diff --git a/main/include/PS2KeyAdvanced.h b/main/include/PS2KeyAdvanced.h new file mode 100644 index 0000000..034d3bc --- /dev/null +++ b/main/include/PS2KeyAdvanced.h @@ -0,0 +1,451 @@ +/* Version V1.0.9 + PS2KeyAdvanced.h - PS2KeyAdvanced library + Copyright (c) 2007 Free Software Foundation. All right reserved. + Written by Paul Carpenter, PC Services + Created September 2014 + Updated January 2016 - Paul Carpenter - add tested on Due and tidy ups for V1.5 Library Management + January 2020 Fix typos, correct keyboard reset status improve library.properties + and additional platform handling and some documentation + March 2020 Add SAMD1 as recognised support as has been tested by user + Improve different architecture handling + November 2020 Add support for STM32 from user Hiabuto-de + Tested on STM32Duino-Framework and PlatformIO on STM32F103C8T6 and an IBM Model M + July 2021 Add workaround for ESP32 issue with Silicon (hardware) from user submissions + + IMPORTANT WARNING + + If using a DUE or similar board with 3V3 I/O you MUST put a level translator + like a Texas Instruments TXS0102 or FET circuit as the signals are + Bi-directional (signals transmitted from both ends on same wire). + + Failure to do so may damage your Arduino Due or similar board. + + Test History + September 2014 Uno and Mega 2560 September 2014 using Arduino V1.6.0 + January 2016 Uno, Mega 2560 and Due using Arduino 1.6.7 and Due Board + Manager V1.6.6 + + ONLY use defines in this file others may disappear on updates. + + This is for a LATIN style keyboard using Scan code set 2. See various + websites on what different scan code sets use. Scan Code Set 2 is the + default scan code set for PS2 keyboards on power up. + + Will support most keyboards even ones with multimedia keys or even 24 function keys. + + Fully featured PS2 keyboard library to provide + All function and movement keys supported even multi-lingual + Parity checking of data sent/received on receive request keyboard resend + Resends data when needed handles keyboard protocol for RESEND and ECHO + Functions for get and set of + Scancode set in use READ only + LED and LOCK control + ReadID + Reset keyboard + Send ECHO + Ignore Break codes for keys + Ignore typematic repeat of CTRL, SHIFT, ALT, Num, Scroll, Caps + Handles NUM, CAPS and SCROLL lock keys to LEDs + Handles NUM/SCROLL internally + + Read function Returns an UNSIGNED INT containing + Make/Break status + Caps status + Shift, CTRL, ALT, ALT GR, GUI keys + Flag for function key not a displayable/printable character + 8 bit key code + + Code Ranges (bottom byte of unsigned int) + 0 invalid/error + 1-1F Functions (Caps, Shift, ALT, Enter, DEL... ) + 1A-1F Functions with ASCII control code + (DEL, BS, TAB, ESC, ENTER, SPACE) + 20-61 Printable characters noting + 0-9 = 0x30 to 0x39 as ASCII + A to Z = 0x41 to 0x5A as upper case ASCII type codes + 8B Extra European key + 61-A0 Function keys and other special keys (plus F2 and F1) + 61-78 F1 to F24 + 79-8A Multimedia + 8B NOT included + 8C-8E ACPI power + 91-A0 and F2 and F1 - Special multilingual + A8-FF Keyboard communications commands (note F2 and F1 are special + codes for special multi-lingual keyboards) + + By using these ranges it is possible to perform detection of any key and do + easy translation to ASCII/UTF-8 avoiding keys that do not have a valid code. + + Top Byte is 8 bits denoting as follows with defines for bit code + + Define name bit description + PS2_BREAK 15 1 = Break key code + (MSB) 0 = Make Key code + PS2_SHIFT 14 1 = Shift key pressed as well (either side) + 0 = NO shift key + PS2_CTRL 13 1 = Ctrl key pressed as well (either side) + 0 = NO Ctrl key + PS2_CAPS 12 1 = Caps Lock ON + 0 = Caps lock OFF + PS2_ALT 11 1 = Left Alt key pressed as well + 0 = NO Left Alt key + PS2_ALT_GR 10 1 = Right Alt (Alt GR) key pressed as well + 0 = NO Right Alt key + PS2_GUI 9 1 = GUI key pressed as well (either) + 0 = NO GUI key + PS2_FUNCTION 8 1 = FUNCTION key non-printable character (plus space, tab, enter) + 0 = standard character key + + Error Codes + Most functions return 0 or 0xFFFF as error, other codes to note and + handle appropriately + 0xAA keyboard has reset and passed power up tests + will happen if keyboard plugged in after code start + 0xFC Keyboard General error or power up fail + + It is responsibility of your programme to deal with converting special cases like + + sends a special code to something else. If you wish to do that make a + NEW library called SOMETHING different NOT a variant or revision of this one, as you + are changing base functionality + + See PS2KeyCode.h for codes from the keyboard this library uses to decode. + (may disappear in updates do not rely on that file or definitions) + + See this file for returned definitions of Keys + + Note defines starting + PS2_KC_* are internal defines for codes from the keyboard + PS2_KEY_* are the codes this library returns + PS2_* remaining defines for use in higher levels + + To get the key as ASCII/UTF-8 single byte character conversion requires use + of PS2KeyMap library AS WELL. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ +#ifndef PS2KeyAdvanced_h +#define PS2KeyAdvanced_h + +// Platform specific areas +// Harvard architecture settings for PROGMEM +// Add separate for EACH architecture as easier to maintain +// AVR (includes Teensy 2.0) +#if defined( ARDUINO_ARCH_AVR ) +#define PS2_SUPPORTED 1 +#define PS2_REQUIRES_PROGMEM 1 +#define PS2_CLEAR_PENDING_IRQ 1 +#endif +// SAM (Due) +#if defined( ARDUINO_ARCH_SAM ) +#define PS2_SUPPORTED 1 +#define PS2_CLEAR_PENDING_IRQ 1 +#endif +// SAMD1 +#if defined( ARDUINO_ARCH_SAMD1 ) +#define PS2_SUPPORTED 1 +#define PS2_CLEAR_PENDING_IRQ 1 +#endif +// STM32 +#if defined( ARDUINO_ARCH_STM32 ) +#define PS2_SUPPORTED 1 +#define PS2_CLEAR_PENDING_IRQ 1 +#endif +// ESP32 +#if defined( ARDUINO_ARCH_ESP32 ) +#define PS2_SUPPORTED 1 +#define PS2_ONLY_CHANGE_IRQ 1 +#endif + +// Invalid architecture +#if !( defined( PS2_SUPPORTED ) ) +#warning Library is NOT supported on this board Use at your OWN risk +#endif + +/* Flags/bit masks for status bits in returned unsigned int value */ +#define PS2_BREAK 0x8000 +#define PS2_SHIFT 0x4000 +#define PS2_CTRL 0x2000 +#define PS2_CAPS 0x1000 +#define PS2_ALT 0x800 +#define PS2_ALT_GR 0x400 +#define PS2_GUI 0x200 +#define PS2_FUNCTION 0x100 + +/* General defines of communications codes */ +/* Command or response */ +#define PS2_KEY_RESEND 0xFE +#define PS2_KEY_ACK 0xFA +#define PS2_KEY_ECHO 0xEE +/* Responses */ +#define PS2_KEY_BAT 0xAA +// Actually buffer overrun +#define PS2_KEY_OVERRUN 0xFF +// Below is general error code +#define PS2_KEY_ERROR 0xFC + +/* Command parameters for functions */ +/* LED codes OR together */ +#define PS2_LOCK_SCROLL 0x01 +#define PS2_LOCK_NUM 0x02 +#define PS2_LOCK_CAPS 0x04 +/* Only useful for very few keyboards */ +#define PS2_LOCK_EXTRA 0x08 + +/* Returned keycode definitions */ +/* Do NOT change these codings as you will break base + functionality use PS2KeyMap for that and internationalisation */ +#define PS2_KEY_NUM 0x01 +#define PS2_KEY_SCROLL 0x02 +#define PS2_KEY_CAPS 0x03 +#define PS2_KEY_PRTSCR 0x04 +#define PS2_KEY_PAUSE 0x05 +#define PS2_KEY_L_SHIFT 0x06 +#define PS2_KEY_R_SHIFT 0x07 +#define PS2_KEY_L_CTRL 0X08 +#define PS2_KEY_R_CTRL 0X09 +#define PS2_KEY_L_ALT 0x0A +#define PS2_KEY_R_ALT 0x0B +/* Sometimes called windows key */ +#define PS2_KEY_L_GUI 0x0C +#define PS2_KEY_R_GUI 0x0D +#define PS2_KEY_MENU 0x0E +/* Break is CTRL + PAUSE generated inside keyboard */ +#define PS2_KEY_BREAK 0x0F +/* Generated by some keyboards by ALT and PRTSCR */ +#define PS2_KEY_SYSRQ 0x10 +#define PS2_KEY_HOME 0x11 +#define PS2_KEY_END 0x12 +#define PS2_KEY_PGUP 0x13 +#define PS2_KEY_PGDN 0x14 +#define PS2_KEY_L_ARROW 0x15 +#define PS2_KEY_R_ARROW 0x16 +#define PS2_KEY_UP_ARROW 0x17 +#define PS2_KEY_DN_ARROW 0x18 +#define PS2_KEY_INSERT 0x19 +#define PS2_KEY_DELETE 0x1A +#define PS2_KEY_ESC 0x1B +#define PS2_KEY_BS 0x1C +#define PS2_KEY_TAB 0x1D +#define PS2_KEY_ENTER 0x1E +#define PS2_KEY_SPACE 0x1F +#define PS2_KEY_KP0 0x20 +#define PS2_KEY_KP1 0x21 +#define PS2_KEY_KP2 0x22 +#define PS2_KEY_KP3 0x23 +#define PS2_KEY_KP4 0x24 +#define PS2_KEY_KP5 0x25 +#define PS2_KEY_KP6 0x26 +#define PS2_KEY_KP7 0x27 +#define PS2_KEY_KP8 0x28 +#define PS2_KEY_KP9 0x29 +#define PS2_KEY_KP_DOT 0x2A +#define PS2_KEY_KP_ENTER 0x2B +#define PS2_KEY_KP_PLUS 0x2C +#define PS2_KEY_KP_MINUS 0x2D +#define PS2_KEY_KP_TIMES 0x2E +#define PS2_KEY_KP_DIV 0x2F +#define PS2_KEY_0 0X30 +#define PS2_KEY_1 0X31 +#define PS2_KEY_2 0X32 +#define PS2_KEY_3 0X33 +#define PS2_KEY_4 0X34 +#define PS2_KEY_5 0X35 +#define PS2_KEY_6 0X36 +#define PS2_KEY_7 0X37 +#define PS2_KEY_8 0X38 +#define PS2_KEY_9 0X39 +#define PS2_KEY_APOS 0X3A +#define PS2_KEY_COMMA 0X3B +#define PS2_KEY_MINUS 0X3C +#define PS2_KEY_DOT 0X3D +#define PS2_KEY_DIV 0X3E +/* Some Numeric keyboards have an '=' on right keypad */ +#define PS2_KEY_KP_EQUAL 0x3F +/* Single quote or back quote */ +#define PS2_KEY_SINGLE 0X40 +#define PS2_KEY_A 0X41 +#define PS2_KEY_B 0X42 +#define PS2_KEY_C 0X43 +#define PS2_KEY_D 0X44 +#define PS2_KEY_E 0X45 +#define PS2_KEY_F 0X46 +#define PS2_KEY_G 0X47 +#define PS2_KEY_H 0X48 +#define PS2_KEY_I 0X49 +#define PS2_KEY_J 0X4A +#define PS2_KEY_K 0X4B +#define PS2_KEY_L 0X4C +#define PS2_KEY_M 0X4D +#define PS2_KEY_N 0X4E +#define PS2_KEY_O 0X4F +#define PS2_KEY_P 0X50 +#define PS2_KEY_Q 0X51 +#define PS2_KEY_R 0X52 +#define PS2_KEY_S 0X53 +#define PS2_KEY_T 0X54 +#define PS2_KEY_U 0X55 +#define PS2_KEY_V 0X56 +#define PS2_KEY_W 0X57 +#define PS2_KEY_X 0X58 +#define PS2_KEY_Y 0X59 +#define PS2_KEY_Z 0X5A +#define PS2_KEY_SEMI 0X5B +#define PS2_KEY_BACK 0X5C +#define PS2_KEY_OPEN_SQ 0X5D +#define PS2_KEY_CLOSE_SQ 0X5E +#define PS2_KEY_EQUAL 0X5F +/* Some Numeric keypads have a comma key */ +#define PS2_KEY_KP_COMMA 0x60 +#define PS2_KEY_F1 0X61 +#define PS2_KEY_F2 0X62 +#define PS2_KEY_F3 0X63 +#define PS2_KEY_F4 0X64 +#define PS2_KEY_F5 0X65 +#define PS2_KEY_F6 0X66 +#define PS2_KEY_F7 0X67 +#define PS2_KEY_F8 0X68 +#define PS2_KEY_F9 0X69 +#define PS2_KEY_F10 0X6A +#define PS2_KEY_F11 0X6B +#define PS2_KEY_F12 0X6C +#define PS2_KEY_F13 0X6D +#define PS2_KEY_F14 0X6E +#define PS2_KEY_F15 0X6F +#define PS2_KEY_F16 0X70 +#define PS2_KEY_F17 0X71 +#define PS2_KEY_F18 0X72 +#define PS2_KEY_F19 0X73 +#define PS2_KEY_F20 0X74 +#define PS2_KEY_F21 0X75 +#define PS2_KEY_F22 0X76 +#define PS2_KEY_F23 0X77 +#define PS2_KEY_F24 0X78 +#define PS2_KEY_NEXT_TR 0X79 +#define PS2_KEY_PREV_TR 0X7A +#define PS2_KEY_STOP 0X7B +#define PS2_KEY_PLAY 0X7C +#define PS2_KEY_MUTE 0X7D +#define PS2_KEY_VOL_UP 0X7E +#define PS2_KEY_VOL_DN 0X7F +#define PS2_KEY_MEDIA 0X80 +#define PS2_KEY_EMAIL 0X81 +#define PS2_KEY_CALC 0X82 +#define PS2_KEY_COMPUTER 0X83 +#define PS2_KEY_WEB_SEARCH 0X84 +#define PS2_KEY_WEB_HOME 0X85 +#define PS2_KEY_WEB_BACK 0X86 +#define PS2_KEY_WEB_FORWARD 0X87 +#define PS2_KEY_WEB_STOP 0X88 +#define PS2_KEY_WEB_REFRESH 0X89 +#define PS2_KEY_WEB_FAVOR 0X8A +#define PS2_KEY_EUROPE2 0X8B +#define PS2_KEY_POWER 0X8C +#define PS2_KEY_SLEEP 0X8D +#define PS2_KEY_WAKE 0X90 +#define PS2_KEY_INTL1 0X91 +#define PS2_KEY_INTL2 0X92 +#define PS2_KEY_INTL3 0X93 +#define PS2_KEY_INTL4 0X94 +#define PS2_KEY_INTL5 0X95 +#define PS2_KEY_LANG1 0X96 +#define PS2_KEY_LANG2 0X97 +#define PS2_KEY_LANG3 0X98 +#define PS2_KEY_LANG4 0X99 +#define PS2_KEY_LANG5 0xA0 +#define PS2_KEY_BTICK 0X9A +#define PS2_KEY_HASH 0X9B + +/* + Purpose: Provides advanced access to PS2 keyboards + Public class definitions + + See standard error codes for error code returns + */ +class PS2KeyAdvanced { + public: + /* This constructor does basically nothing. Please call the begin(int,int) + method before using any other method of this class. */ + PS2KeyAdvanced( ); + + // Destructor - disable and detach interrupts and free up resources. + ~PS2KeyAdvanced( ); + + /* Starts the keyboard "service" by registering the external interrupt. + setting the pin modes correctly and driving those needed to high. + Sets default LOCK status (LEDs) to passed in value or default of all off + The best place to call this method is in the setup routine. */ + void begin( uint8_t, uint8_t ); + + // Additional key available check which doesnt affect the queue. + uint8_t keyAvailable(void); + + /* Returns number of codes available or 0 for none */ + uint8_t available( ); + + /* Returns the key last read from the keyboard. + If there is no key available, 0 is returned. */ + uint16_t read( ); + + /* Returns the current status of Locks + Use Macro to mask out bits from + PS2_LOCK_NUM PS2_LOCK_CAPS PS2_LOCK_SCROLL */ + uint8_t getLock( ); + + /* Sets the current status of Locks and LEDs + Use macro defines added together from + PS2_LOCK_NUM PS2_LOCK_CAPS PS2_LOCK_SCROLL */ + void setLock( uint8_t ); + + /* Set library to not send break key codes + 1 = no break codes + 0 = send break codes */ + void setNoBreak( uint8_t ); + + /* Set library to not repeat make codes for CTRL, ALT, GUI, SHIFT + 1 = no repeat codes + 0 = send repeat codes */ + void setNoRepeat( uint8_t ); + + /* Resets keyboard when reset has completed + keyboard sends AA - Pass or FC for fail + Read from keyboard data buffer */ + void resetKey( ); + + /* Get the current Scancode Set used in keyboard + returned data in keyboard buffer read as keys */ + void getScanCodeSet( void ); + + /* Get the current Scancode Set used in keyboard + returned data in keyboard buffer read as keys */ + void readID( void ); + + /* Send Echo command to keyboard + returned data in keyboard buffer read as keys */ + void echo( void ); + + // Method to suspend PS2 activity, primarily by disabling the interrupts. + void suspend(bool suspend); + + /* Send Typematic rate/delay command to keyboard + First Parameter rate is 0 - 0x1F (31) + 0 = 30 CPS + 0x1F = 2 CPS + default in keyboard is 0xB (10.9 CPS) + Second Parameter delay is 0 - 3 for 0.25s to 1s in 0.25 increments + default in keyboard is 1 = 0.5 second delay + Returned data in keyboard buffer read as keys */ + int typematic( uint8_t , uint8_t ); +}; +#endif diff --git a/main/include/PS2KeyCode.h b/main/include/PS2KeyCode.h deleted file mode 120000 index cbee121..0000000 --- a/main/include/PS2KeyCode.h +++ /dev/null @@ -1 +0,0 @@ -../../../sharpkey/main/include/PS2KeyCode.h \ No newline at end of file diff --git a/main/include/PS2KeyCode.h b/main/include/PS2KeyCode.h new file mode 100644 index 0000000..ec4da88 --- /dev/null +++ b/main/include/PS2KeyCode.h @@ -0,0 +1,276 @@ +/* Version V1.0.8 + PS2KeyCode.h - PS2KeyAdvanced library Internal actual PS2 key code sequences + Copyright (c) 2007 Free Software Foundation. All right reserved. + Written by Paul Carpenter, PC Services + Created September 2014 + Updated January 2016 - Paul Carpenter - add tested on Due and tidy ups for V1.5 Library Management + Updated December 2019 - Paul Carpenter - Fix typo in code for Multimedia STOP + + PRIVATE to library + + Test History + September 2014 Uno and Mega 2560 September 2014 using Arduino V1.6.0 + January 2016 Uno, Mega 2560 and Due using Arduino 1.6.7 and Due Board + Manager V1.6.6 + + This is for a LATIN style keyboard. Will support most keyboards even ones + with multimedia keys or even 24 function keys. + + Definitions used for key codes from a PS2 keyboard, do not use in your + code these are to be handled INTERNALLY by the library. + (may disappear in updates do not rely on this file or definitions) + + See PS2KeyAdvanced.h for codes returned from library and flag settings + + Defines are in three groups + + Special codes definition of communications bytes + + Single Byte codes returned as key codes + + Two byte Codes preceded by E0 code returned as keycodes + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef PS2KeyCode_h +#define PS2KeyCode_h + +/* Ignore code for key code translation */ +#define PS2_KEY_IGNORE 0xBB + +// buffer sizes keyboard RX and TX, then key reading buffer +// Minimum size 8 can be larger +#define _RX_BUFFER_SIZE 8 +// Minimum size 6 can be larger +#define _TX_BUFFER_SIZE 6 +// Output Buffer of unsigned int values. Minimum size 4 can be larger +#define _KEY_BUFF_SIZE 4 + +/* private defines for library files not global */ +/* _ps2mode status flags */ +#define _PS2_BUSY 0x80 +#define _TX_MODE 0x40 +#define _BREAK_KEY 0x20 +#define _WAIT_RESPONSE 0x10 +#define _E0_MODE 0x08 +#define _E1_MODE 0x04 +#define _LAST_VALID 0x02 + +/* _tx_ready flags */ +#define _HANDSHAKE 0x80 +#define _COMMAND 0x01 + +/* Key Repeat defines */ +#define _NO_BREAKS 0x08 +#define _NO_REPEATS 0x80 + +/* PS2_keystatus byte masks (from 16 bit int masks) */ +#define _BREAK ( PS2_BREAK >> 8 ) +#define _SHIFT ( PS2_SHIFT >> 8 ) +#define _CTRL ( PS2_CTRL >> 8 ) +#define _CAPS ( PS2_CAPS >> 8 ) +#define _ALT ( PS2_ALT >> 8 ) +#define _ALT_GR ( PS2_ALT_GR >> 8 ) +#define _GUI ( PS2_GUI >> 8 ) +#define _FUNCTION ( PS2_FUNCTION >> 8 ) + +/* General defines of comms codes */ +/* Command or response */ +#define PS2_KC_RESEND 0xFE +#define PS2_KC_ACK 0xFA +#define PS2_KC_ECHO 0xEE +/* Responses */ +#define PS2_KC_BAT 0xAA +// Actually buffer overrun +#define PS2_KC_OVERRUN 0xFF +// Below is general error code +#define PS2_KC_ERROR 0xFC +#define PS2_KC_KEYBREAK 0xF0 +#define PS2_KC_EXTEND1 0xE1 +#define PS2_KC_EXTEND 0xE0 +/* Commands */ +#define PS2_KC_RESET 0xFF +#define PS2_KC_DEFAULTS 0xF6 +#define PS2_KC_DISABLE 0xF5 +#define PS2_KC_ENABLE 0xF4 +#define PS2_KC_RATE 0xF3 +#define PS2_KC_READID 0xF2 +#define PS2_KC_SCANCODE 0xF0 +#define PS2_KC_LOCK 0xED + +/* Single Byte Key Codes */ +#define PS2_KC_NUM 0x77 +#define PS2_KC_SCROLL 0x7E +#define PS2_KC_CAPS 0x58 +#define PS2_KC_L_SHIFT 0x12 +#define PS2_KC_R_SHIFT 0x59 +/* This is Left CTRL and ALT but Right version is in E0 with same code */ +#define PS2_KC_CTRL 0X14 +#define PS2_KC_ALT 0x11 +/* Generated by some keyboards by ALT and PRTSCR */ +#define PS2_KC_SYSRQ 0x84 +#define PS2_KC_ESC 0x76 +#define PS2_KC_BS 0x66 +#define PS2_KC_TAB 0x0D +#define PS2_KC_ENTER 0x5A +#define PS2_KC_SPACE 0x29 +#define PS2_KC_KP0 0x70 +#define PS2_KC_KP1 0x69 +#define PS2_KC_KP2 0x72 +#define PS2_KC_KP3 0x7A +#define PS2_KC_KP4 0x6B +#define PS2_KC_KP5 0x73 +#define PS2_KC_KP6 0x74 +#define PS2_KC_KP7 0x6C +#define PS2_KC_KP8 0x75 +#define PS2_KC_KP9 0x7D +#define PS2_KC_KP_DOT 0x71 +#define PS2_KC_KP_PLUS 0x79 +#define PS2_KC_KP_MINUS 0x7B +#define PS2_KC_KP_TIMES 0x7C +/* Some keyboards have an '=' on right keypad */ +#define PS2_KC_KP_EQUAL 0x0F +#define PS2_KC_0 0X45 +#define PS2_KC_1 0X16 +#define PS2_KC_2 0X1E +#define PS2_KC_3 0X26 +#define PS2_KC_4 0X25 +#define PS2_KC_5 0X2E +#define PS2_KC_6 0X36 +#define PS2_KC_7 0X3D +#define PS2_KC_8 0X3E +#define PS2_KC_9 0X46 +#define PS2_KC_APOS 0X52 +#define PS2_KC_COMMA 0X41 +#define PS2_KC_MINUS 0X4E +#define PS2_KC_DOT 0X49 +#define PS2_KC_DIV 0X4A +/* Single quote or back apostrophe */ +#define PS2_KC_BTICK 0X0E +#define PS2_KC_A 0X1C +#define PS2_KC_B 0X32 +#define PS2_KC_C 0X21 +#define PS2_KC_D 0X23 +#define PS2_KC_E 0X24 +#define PS2_KC_F 0X2B +#define PS2_KC_G 0X34 +#define PS2_KC_H 0X33 +#define PS2_KC_I 0X43 +#define PS2_KC_J 0X3B +#define PS2_KC_K 0X42 +#define PS2_KC_L 0X4B +#define PS2_KC_M 0X3A +#define PS2_KC_N 0X31 +#define PS2_KC_O 0X44 +#define PS2_KC_P 0X4D +#define PS2_KC_Q 0X15 +#define PS2_KC_R 0X2D +#define PS2_KC_S 0X1B +#define PS2_KC_T 0X2C +#define PS2_KC_U 0X3C +#define PS2_KC_V 0X2A +#define PS2_KC_W 0X1D +#define PS2_KC_X 0X22 +#define PS2_KC_Y 0X35 +#define PS2_KC_Z 0X1A +#define PS2_KC_SEMI 0X4C +#define PS2_KC_BACK 0X5D +// Extra key left of Z on 102 keyboards +#define PS2_KC_EUROPE2 0x61 +#define PS2_KC_OPEN_SQ 0X54 +#define PS2_KC_CLOSE_SQ 0X5B +#define PS2_KC_EQUAL 0X55 +#define PS2_KC_F1 0X05 +#define PS2_KC_F2 0X06 +#define PS2_KC_F3 0X04 +#define PS2_KC_F4 0X0C +#define PS2_KC_F5 0X03 +#define PS2_KC_F6 0X0B +#define PS2_KC_F7 0X83 +#define PS2_KC_F8 0X0A +#define PS2_KC_F9 0X01 +#define PS2_KC_F10 0X09 +#define PS2_KC_F11 0X78 +#define PS2_KC_F12 0X07 +#define PS2_KC_F13 0X08 +#define PS2_KC_F14 0X10 +#define PS2_KC_F15 0X18 +#define PS2_KC_F16 0X20 +#define PS2_KC_F17 0X28 +#define PS2_KC_F18 0X30 +#define PS2_KC_F19 0X38 +#define PS2_KC_F20 0X40 +#define PS2_KC_F21 0X48 +#define PS2_KC_F22 0X50 +#define PS2_KC_F23 0X57 +#define PS2_KC_F24 0X5F +#define PS2_KC_KP_COMMA 0X6D +#define PS2_KC_INTL1 0X51 +#define PS2_KC_INTL2 0X13 +#define PS2_KC_INTL3 0X6A +#define PS2_KC_INTL4 0X64 +#define PS2_KC_INTL5 0X67 +#define PS2_KC_LANG1 0XF2 +#define PS2_KC_LANG2 0XF1 +#define PS2_KC_LANG3 0X63 +#define PS2_KC_LANG4 0X62 +#define PS2_KC_LANG5 0X5F + +/* Extended key codes E0 table for two byte codes */ +/* PS2_CTRL and PS2_ALT Need using in any table for the right keys */ +/* first is special case for PRTSCR not always used so ignored by decoding */ +#define PS2_KC_IGNORE 0x12 +#define PS2_KC_PRTSCR 0x7C +/* Sometimes called windows key */ +#define PS2_KC_L_GUI 0x1F +#define PS2_KC_R_GUI 0x27 +#define PS2_KC_MENU 0x2F +/* Break is CTRL + PAUSE generated inside keyboard */ +#define PS2_KC_BREAK 0x7E +#define PS2_KC_HOME 0x6C +#define PS2_KC_END 0x69 +#define PS2_KC_PGUP 0x7D +#define PS2_KC_PGDN 0x7A +#define PS2_KC_L_ARROW 0x6B +#define PS2_KC_R_ARROW 0x74 +#define PS2_KC_UP_ARROW 0x75 +#define PS2_KC_DN_ARROW 0x72 +#define PS2_KC_INSERT 0x70 +#define PS2_KC_DELETE 0x71 +#define PS2_KC_KP_ENTER 0x5A +#define PS2_KC_KP_DIV 0x4A +#define PS2_KC_NEXT_TR 0X4D +#define PS2_KC_PREV_TR 0X15 +#define PS2_KC_STOP 0X3B +#define PS2_KC_PLAY 0X34 +#define PS2_KC_MUTE 0X23 +#define PS2_KC_VOL_UP 0X32 +#define PS2_KC_VOL_DN 0X21 +#define PS2_KC_MEDIA 0X50 +#define PS2_KC_EMAIL 0X48 +#define PS2_KC_CALC 0X2B +#define PS2_KC_COMPUTER 0X40 +#define PS2_KC_WEB_SEARCH 0X10 +#define PS2_KC_WEB_HOME 0X3A +#define PS2_KC_WEB_BACK 0X38 +#define PS2_KC_WEB_FORWARD 0X30 +#define PS2_KC_WEB_STOP 0X28 +#define PS2_KC_WEB_REFRESH 0X20 +#define PS2_KC_WEB_FAVOR 0X18 +#define PS2_KC_POWER 0X37 +#define PS2_KC_SLEEP 0X3F +#define PS2_KC_WAKE 0X5E +#endif diff --git a/main/include/PS2KeyTable.h b/main/include/PS2KeyTable.h deleted file mode 120000 index 6ab62d1..0000000 --- a/main/include/PS2KeyTable.h +++ /dev/null @@ -1 +0,0 @@ -../../../sharpkey/main/include/PS2KeyTable.h \ No newline at end of file diff --git a/main/include/PS2KeyTable.h b/main/include/PS2KeyTable.h new file mode 100644 index 0000000..c0a304e --- /dev/null +++ b/main/include/PS2KeyTable.h @@ -0,0 +1,393 @@ +/* Version V1.0.8 + PS2KeyTable.h - PS2KeyAdvanced library keycode values to return values + Copyright (c) 2007 Free Software Foundation. All right reserved. + Written by Paul Carpenter, PC Services + Created September 2014 + V1.0.2 Updated January 2016 - Paul Carpenter - add tested on Due and tidy ups for V1.5 Library Management + + PRIVATE to library + + Test History + September 2014 Uno and Mega 2560 September 2014 using Arduino V1.6.0 + January 2016 Uno, Mega 2560 and Due using Arduino 1.6.7 and Due Board + Manager V1.6.6 + + Internal to library private tables + (may disappear in updates do not rely on this file or definitions) + + This is for a LATIN style keyboard. Will support most keyboards even ones + with multimedia keys or even 24 function keys. + + Definitions used for key codes from a PS2 keyboard, do not use in your + code these are handled by the library. + + See PS2KeyAdvanced.h for codes returned from library and flag settings + + Two sets of tables + + Single Byte codes returned as key codes + + Two byte Codes preceded by E0 code returned as keycodes + + Same tables used for make and break decode + + Special cases are - + + PRTSCR that ignores one of the sequences (E0,12) as this is not always sent + especially with modifier keys or some keyboards when typematic repeat comes on. + + PAUSE as this is an 8 byte sequence only one starting E1 so main code gets E1 + and waits for 7 more valid bytes to make the coding. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ +#ifndef PS2KeyTable_h +#define PS2KeyTable_h + +/* Table contents are pairs of numbers + first code from keyboard + second is either PS2_KEY_IGNOPRE code or key code to return + + Single byte Key table + In codes can only be 1 - 0x9F, plus 0xF2 and 0xF1 + Out Codes in range 1 to 0x9F +*/ +const uint8_t single_key[][ 2 ] = { + { PS2_KC_NUM, PS2_KEY_NUM }, + { PS2_KC_SCROLL, PS2_KEY_SCROLL }, + { PS2_KC_CAPS, PS2_KEY_CAPS }, + { PS2_KC_L_SHIFT, PS2_KEY_L_SHIFT }, + { PS2_KC_R_SHIFT, PS2_KEY_R_SHIFT }, + { PS2_KC_CTRL, PS2_KEY_L_CTRL }, + { PS2_KC_ALT, PS2_KEY_L_ALT }, + { PS2_KC_SYSRQ, PS2_KEY_SYSRQ }, + { PS2_KC_ESC, PS2_KEY_ESC }, + { PS2_KC_BS, PS2_KEY_BS }, + { PS2_KC_TAB, PS2_KEY_TAB }, + { PS2_KC_ENTER, PS2_KEY_ENTER }, + { PS2_KC_SPACE, PS2_KEY_SPACE }, + { PS2_KC_KP0, PS2_KEY_KP0 }, + { PS2_KC_KP1, PS2_KEY_KP1 }, + { PS2_KC_KP2, PS2_KEY_KP2 }, + { PS2_KC_KP3, PS2_KEY_KP3 }, + { PS2_KC_KP4, PS2_KEY_KP4 }, + { PS2_KC_KP5, PS2_KEY_KP5 }, + { PS2_KC_KP6, PS2_KEY_KP6 }, + { PS2_KC_KP7, PS2_KEY_KP7 }, + { PS2_KC_KP8, PS2_KEY_KP8 }, + { PS2_KC_KP9, PS2_KEY_KP9 }, + { PS2_KC_KP_DOT, PS2_KEY_KP_DOT }, + { PS2_KC_KP_PLUS, PS2_KEY_KP_PLUS }, + { PS2_KC_KP_MINUS, PS2_KEY_KP_MINUS }, + { PS2_KC_KP_TIMES, PS2_KEY_KP_TIMES }, + { PS2_KC_KP_EQUAL, PS2_KEY_KP_EQUAL }, + { PS2_KC_0, PS2_KEY_0 }, + { PS2_KC_1, PS2_KEY_1 }, + { PS2_KC_2, PS2_KEY_2 }, + { PS2_KC_3, PS2_KEY_3 }, + { PS2_KC_4, PS2_KEY_4 }, + { PS2_KC_5, PS2_KEY_5 }, + { PS2_KC_6, PS2_KEY_6 }, + { PS2_KC_7, PS2_KEY_7 }, + { PS2_KC_8, PS2_KEY_8 }, + { PS2_KC_9, PS2_KEY_9 }, + { PS2_KC_APOS, PS2_KEY_APOS }, + { PS2_KC_COMMA, PS2_KEY_COMMA }, + { PS2_KC_MINUS, PS2_KEY_MINUS }, + { PS2_KC_DOT, PS2_KEY_DOT }, + { PS2_KC_DIV, PS2_KEY_DIV }, + { PS2_KC_BTICK, PS2_KEY_BTICK }, + { PS2_KC_A, PS2_KEY_A }, + { PS2_KC_B, PS2_KEY_B }, + { PS2_KC_C, PS2_KEY_C }, + { PS2_KC_D, PS2_KEY_D }, + { PS2_KC_E, PS2_KEY_E }, + { PS2_KC_F, PS2_KEY_F }, + { PS2_KC_G, PS2_KEY_G }, + { PS2_KC_H, PS2_KEY_H }, + { PS2_KC_I, PS2_KEY_I }, + { PS2_KC_J, PS2_KEY_J }, + { PS2_KC_K, PS2_KEY_K }, + { PS2_KC_L, PS2_KEY_L }, + { PS2_KC_M, PS2_KEY_M }, + { PS2_KC_N, PS2_KEY_N }, + { PS2_KC_O, PS2_KEY_O }, + { PS2_KC_P, PS2_KEY_P }, + { PS2_KC_Q, PS2_KEY_Q }, + { PS2_KC_R, PS2_KEY_R }, + { PS2_KC_S, PS2_KEY_S }, + { PS2_KC_T, PS2_KEY_T }, + { PS2_KC_U, PS2_KEY_U }, + { PS2_KC_V, PS2_KEY_V }, + { PS2_KC_W, PS2_KEY_W }, + { PS2_KC_X, PS2_KEY_X }, + { PS2_KC_Y, PS2_KEY_Y }, + { PS2_KC_Z, PS2_KEY_Z }, + { PS2_KC_SEMI, PS2_KEY_SEMI }, + { PS2_KC_BACK, PS2_KEY_HASH }, + { PS2_KC_OPEN_SQ, PS2_KEY_OPEN_SQ }, + { PS2_KC_CLOSE_SQ, PS2_KEY_CLOSE_SQ }, + { PS2_KC_EQUAL, PS2_KEY_EQUAL }, + { PS2_KC_EUROPE2, PS2_KEY_BACK }, + { PS2_KC_F1, PS2_KEY_F1 }, + { PS2_KC_F2, PS2_KEY_F2 }, + { PS2_KC_F3, PS2_KEY_F3 }, + { PS2_KC_F4, PS2_KEY_F4 }, + { PS2_KC_F5, PS2_KEY_F5 }, + { PS2_KC_F6, PS2_KEY_F6 }, + { PS2_KC_F7, PS2_KEY_F7 }, + { PS2_KC_F8, PS2_KEY_F8 }, + { PS2_KC_F9, PS2_KEY_F9 }, + { PS2_KC_F10, PS2_KEY_F10 }, + { PS2_KC_F11, PS2_KEY_F11 }, + { PS2_KC_F12, PS2_KEY_F12 }, + { PS2_KC_KP_COMMA, PS2_KEY_KP_COMMA }, + { PS2_KC_INTL1, PS2_KEY_INTL1 }, + { PS2_KC_INTL2, PS2_KEY_INTL2 }, + { PS2_KC_INTL3, PS2_KEY_INTL3 }, + { PS2_KC_INTL4, PS2_KEY_INTL4 }, + { PS2_KC_INTL5, PS2_KEY_INTL5 }, + { PS2_KC_LANG1, PS2_KEY_LANG1 }, + { PS2_KC_LANG2, PS2_KEY_LANG2 }, + { PS2_KC_LANG3, PS2_KEY_LANG3 }, + { PS2_KC_LANG4, PS2_KEY_LANG4 }, + { PS2_KC_LANG5, PS2_KEY_LANG5 } + }; + +/* Two byte Key table after an E0 byte received */ +const uint8_t extended_key[][ 2 ] = { + { PS2_KC_IGNORE, PS2_KEY_IGNORE }, + { PS2_KC_PRTSCR, PS2_KEY_PRTSCR }, + { PS2_KC_CTRL, PS2_KEY_R_CTRL }, + { PS2_KC_ALT, PS2_KEY_R_ALT }, + { PS2_KC_L_GUI, PS2_KEY_L_GUI }, + { PS2_KC_R_GUI, PS2_KEY_R_GUI }, + { PS2_KC_MENU, PS2_KEY_MENU }, + { PS2_KC_BREAK, PS2_KEY_BREAK }, + { PS2_KC_HOME, PS2_KEY_HOME }, + { PS2_KC_END, PS2_KEY_END }, + { PS2_KC_PGUP, PS2_KEY_PGUP }, + { PS2_KC_PGDN, PS2_KEY_PGDN }, + { PS2_KC_L_ARROW, PS2_KEY_L_ARROW }, + { PS2_KC_R_ARROW, PS2_KEY_R_ARROW }, + { PS2_KC_UP_ARROW, PS2_KEY_UP_ARROW }, + { PS2_KC_DN_ARROW, PS2_KEY_DN_ARROW }, + { PS2_KC_INSERT, PS2_KEY_INSERT }, + { PS2_KC_DELETE, PS2_KEY_DELETE }, + { PS2_KC_KP_ENTER, PS2_KEY_KP_ENTER }, + { PS2_KC_KP_DIV, PS2_KEY_KP_DIV }, + { PS2_KC_NEXT_TR, PS2_KEY_NEXT_TR }, + { PS2_KC_PREV_TR, PS2_KEY_PREV_TR }, + { PS2_KC_STOP, PS2_KEY_STOP }, + { PS2_KC_PLAY, PS2_KEY_PLAY }, + { PS2_KC_MUTE, PS2_KEY_MUTE }, + { PS2_KC_VOL_UP, PS2_KEY_VOL_UP }, + { PS2_KC_VOL_DN, PS2_KEY_VOL_DN }, + { PS2_KC_MEDIA, PS2_KEY_MEDIA }, + { PS2_KC_EMAIL, PS2_KEY_EMAIL }, + { PS2_KC_CALC, PS2_KEY_CALC }, + { PS2_KC_COMPUTER, PS2_KEY_COMPUTER }, + { PS2_KC_WEB_SEARCH, PS2_KEY_WEB_SEARCH }, + { PS2_KC_WEB_HOME, PS2_KEY_WEB_HOME }, + { PS2_KC_WEB_BACK, PS2_KEY_WEB_BACK }, + { PS2_KC_WEB_FORWARD, PS2_KEY_WEB_FORWARD }, + { PS2_KC_WEB_STOP, PS2_KEY_WEB_STOP }, + { PS2_KC_WEB_REFRESH, PS2_KEY_WEB_REFRESH }, + { PS2_KC_WEB_FAVOR, PS2_KEY_WEB_FAVOR }, + { PS2_KC_POWER, PS2_KEY_POWER }, + { PS2_KC_SLEEP, PS2_KEY_SLEEP }, + { PS2_KC_WAKE, PS2_KEY_WAKE } + }; + +/* Scroll lock numeric keypad re-mappings for NOT NUMLOCK */ +/* in translated code order order is important */ +const uint8_t scroll_remap[] = { + PS2_KEY_INSERT, // PS2_KEY_KP0 + PS2_KEY_END, // PS2_KEY_KP1 + PS2_KEY_DN_ARROW, // PS2_KEY_KP2 + PS2_KEY_PGDN, // PS2_KEY_KP3 + PS2_KEY_L_ARROW, // PS2_KEY_KP4 + PS2_KEY_IGNORE, // PS2_KEY_KP5 + PS2_KEY_R_ARROW, // PS2_KEY_KP6 + PS2_KEY_HOME, // PS2_KEY_KP7 + PS2_KEY_UP_ARROW, // PS2_KEY_KP8 + PS2_KEY_PGUP, // PS2_KEY_KP9 + PS2_KEY_DELETE // PS2_KEY_KP_DOT + }; + +//const uint8_t single_key[][ 2 ] = { +// { PS2_KC_NUM, PS2_KEY_NUM }, +// { PS2_KC_SCROLL, PS2_KEY_SCROLL }, +// { PS2_KC_CAPS, PS2_KEY_CAPS }, +// { PS2_KC_L_SHIFT, PS2_KEY_L_SHIFT }, +// { PS2_KC_R_SHIFT, PS2_KEY_R_SHIFT }, +// { PS2_KC_CTRL, PS2_KEY_L_CTRL }, +// { PS2_KC_ALT, PS2_KEY_L_ALT }, +// { PS2_KC_SYSRQ, PS2_KEY_SYSRQ }, +// { PS2_KC_ESC, PS2_KEY_ESC }, +// { PS2_KC_BS, PS2_KEY_BS }, +// { PS2_KC_TAB, PS2_KEY_TAB }, +// { PS2_KC_ENTER, PS2_KEY_ENTER }, +// { PS2_KC_SPACE, PS2_KEY_SPACE }, +// { PS2_KC_KP0, PS2_KEY_KP0 }, +// { PS2_KC_KP1, PS2_KEY_KP1 }, +// { PS2_KC_KP2, PS2_KEY_KP2 }, +// { PS2_KC_KP3, PS2_KEY_KP3 }, +// { PS2_KC_KP4, PS2_KEY_KP4 }, +// { PS2_KC_KP5, PS2_KEY_KP5 }, +// { PS2_KC_KP6, PS2_KEY_KP6 }, +// { PS2_KC_KP7, PS2_KEY_KP7 }, +// { PS2_KC_KP8, PS2_KEY_KP8 }, +// { PS2_KC_KP9, PS2_KEY_KP9 }, +// { PS2_KC_KP_DOT, PS2_KEY_KP_DOT }, +// { PS2_KC_KP_PLUS, PS2_KEY_KP_PLUS }, +// { PS2_KC_KP_MINUS, PS2_KEY_KP_MINUS }, +// { PS2_KC_KP_TIMES, PS2_KEY_KP_TIMES }, +// { PS2_KC_KP_EQUAL, PS2_KEY_KP_EQUAL }, +// { PS2_KC_0, PS2_KEY_0 }, +// { PS2_KC_1, PS2_KEY_1 }, +// { PS2_KC_2, PS2_KEY_2 }, +// { PS2_KC_3, PS2_KEY_3 }, +// { PS2_KC_4, PS2_KEY_4 }, +// { PS2_KC_5, PS2_KEY_5 }, +// { PS2_KC_6, PS2_KEY_6 }, +// { PS2_KC_7, PS2_KEY_7 }, +// { PS2_KC_8, PS2_KEY_8 }, +// { PS2_KC_9, PS2_KEY_9 }, +// { PS2_KC_APOS, PS2_KEY_APOS }, +// { PS2_KC_COMMA, PS2_KEY_COMMA }, +// { PS2_KC_MINUS, PS2_KEY_MINUS }, +// { PS2_KC_DOT, PS2_KEY_DOT }, +// { PS2_KC_DIV, PS2_KEY_DIV }, +// { PS2_KC_BTICK, PS2_KEY_BTICK }, +// { PS2_KC_A, PS2_KEY_A }, +// { PS2_KC_B, PS2_KEY_B }, +// { PS2_KC_C, PS2_KEY_C }, +// { PS2_KC_D, PS2_KEY_D }, +// { PS2_KC_E, PS2_KEY_E }, +// { PS2_KC_F, PS2_KEY_F }, +// { PS2_KC_G, PS2_KEY_G }, +// { PS2_KC_H, PS2_KEY_H }, +// { PS2_KC_I, PS2_KEY_I }, +// { PS2_KC_J, PS2_KEY_J }, +// { PS2_KC_K, PS2_KEY_K }, +// { PS2_KC_L, PS2_KEY_L }, +// { PS2_KC_M, PS2_KEY_M }, +// { PS2_KC_N, PS2_KEY_N }, +// { PS2_KC_O, PS2_KEY_O }, +// { PS2_KC_P, PS2_KEY_P }, +// { PS2_KC_Q, PS2_KEY_Q }, +// { PS2_KC_R, PS2_KEY_R }, +// { PS2_KC_S, PS2_KEY_S }, +// { PS2_KC_T, PS2_KEY_T }, +// { PS2_KC_U, PS2_KEY_U }, +// { PS2_KC_V, PS2_KEY_V }, +// { PS2_KC_W, PS2_KEY_W }, +// { PS2_KC_X, PS2_KEY_X }, +// { PS2_KC_Y, PS2_KEY_Y }, +// { PS2_KC_Z, PS2_KEY_Z }, +// { PS2_KC_SEMI, PS2_KEY_SEMI }, +// { PS2_KC_BACK, PS2_KEY_BACK }, +// { PS2_KC_OPEN_SQ, PS2_KEY_OPEN_SQ }, +// { PS2_KC_CLOSE_SQ, PS2_KEY_CLOSE_SQ }, +// { PS2_KC_EQUAL, PS2_KEY_EQUAL }, +// { PS2_KC_EUROPE2, PS2_KEY_BACK }, +// { PS2_KC_F1, PS2_KEY_F1 }, +// { PS2_KC_F2, PS2_KEY_F2 }, +// { PS2_KC_F3, PS2_KEY_F3 }, +// { PS2_KC_F4, PS2_KEY_F4 }, +// { PS2_KC_F5, PS2_KEY_F5 }, +// { PS2_KC_F6, PS2_KEY_F6 }, +// { PS2_KC_F7, PS2_KEY_F7 }, +// { PS2_KC_F8, PS2_KEY_F8 }, +// { PS2_KC_F9, PS2_KEY_F9 }, +// { PS2_KC_F10, PS2_KEY_F10 }, +// { PS2_KC_F11, PS2_KEY_F11 }, +// { PS2_KC_F12, PS2_KEY_F12 }, +// { PS2_KC_KP_COMMA, PS2_KEY_KP_COMMA }, +// { PS2_KC_INTL1, PS2_KEY_INTL1 }, +// { PS2_KC_INTL2, PS2_KEY_INTL2 }, +// { PS2_KC_INTL3, PS2_KEY_INTL3 }, +// { PS2_KC_INTL4, PS2_KEY_INTL4 }, +// { PS2_KC_INTL5, PS2_KEY_INTL5 }, +// { PS2_KC_LANG1, PS2_KEY_LANG1 }, +// { PS2_KC_LANG2, PS2_KEY_LANG2 }, +// { PS2_KC_LANG3, PS2_KEY_LANG3 }, +// { PS2_KC_LANG4, PS2_KEY_LANG4 }, +// { PS2_KC_LANG5, PS2_KEY_LANG5 } +// }; +// +///* Two byte Key table after an E0 byte received */ +//const uint8_t extended_key[][ 2 ] = { +// { PS2_KC_IGNORE, PS2_KEY_IGNORE }, +// { PS2_KC_PRTSCR, PS2_KEY_PRTSCR }, +// { PS2_KC_CTRL, PS2_KEY_R_CTRL }, +// { PS2_KC_ALT, PS2_KEY_R_ALT }, +// { PS2_KC_L_GUI, PS2_KEY_L_GUI }, +// { PS2_KC_R_GUI, PS2_KEY_R_GUI }, +// { PS2_KC_MENU, PS2_KEY_MENU }, +// { PS2_KC_BREAK, PS2_KEY_BREAK }, +// { PS2_KC_HOME, PS2_KEY_HOME }, +// { PS2_KC_END, PS2_KEY_END }, +// { PS2_KC_PGUP, PS2_KEY_PGUP }, +// { PS2_KC_PGDN, PS2_KEY_PGDN }, +// { PS2_KC_L_ARROW, PS2_KEY_L_ARROW }, +// { PS2_KC_R_ARROW, PS2_KEY_R_ARROW }, +// { PS2_KC_UP_ARROW, PS2_KEY_UP_ARROW }, +// { PS2_KC_DN_ARROW, PS2_KEY_DN_ARROW }, +// { PS2_KC_INSERT, PS2_KEY_INSERT }, +// { PS2_KC_DELETE, PS2_KEY_DELETE }, +// { PS2_KC_KP_ENTER, PS2_KEY_KP_ENTER }, +// { PS2_KC_KP_DIV, PS2_KEY_KP_DIV }, +// { PS2_KC_NEXT_TR, PS2_KEY_NEXT_TR }, +// { PS2_KC_PREV_TR, PS2_KEY_PREV_TR }, +// { PS2_KC_STOP, PS2_KEY_STOP }, +// { PS2_KC_PLAY, PS2_KEY_PLAY }, +// { PS2_KC_MUTE, PS2_KEY_MUTE }, +// { PS2_KC_VOL_UP, PS2_KEY_VOL_UP }, +// { PS2_KC_VOL_DN, PS2_KEY_VOL_DN }, +// { PS2_KC_MEDIA, PS2_KEY_MEDIA }, +// { PS2_KC_EMAIL, PS2_KEY_EMAIL }, +// { PS2_KC_CALC, PS2_KEY_CALC }, +// { PS2_KC_COMPUTER, PS2_KEY_COMPUTER }, +// { PS2_KC_WEB_SEARCH, PS2_KEY_WEB_SEARCH }, +// { PS2_KC_WEB_HOME, PS2_KEY_WEB_HOME }, +// { PS2_KC_WEB_BACK, PS2_KEY_WEB_BACK }, +// { PS2_KC_WEB_FORWARD, PS2_KEY_WEB_FORWARD }, +// { PS2_KC_WEB_STOP, PS2_KEY_WEB_STOP }, +// { PS2_KC_WEB_REFRESH, PS2_KEY_WEB_REFRESH }, +// { PS2_KC_WEB_FAVOR, PS2_KEY_WEB_FAVOR }, +// { PS2_KC_POWER, PS2_KEY_POWER }, +// { PS2_KC_SLEEP, PS2_KEY_SLEEP }, +// { PS2_KC_WAKE, PS2_KEY_WAKE } +// }; +// +///* Scroll lock numeric keypad re-mappings for NOT NUMLOCK */ +///* in translated code order order is important */ +//const uint8_t scroll_remap[] = { +// PS2_KEY_INSERT, // PS2_KEY_KP0 +// PS2_KEY_END, // PS2_KEY_KP1 +// PS2_KEY_DN_ARROW, // PS2_KEY_KP2 +// PS2_KEY_PGDN, // PS2_KEY_KP3 +// PS2_KEY_L_ARROW, // PS2_KEY_KP4 +// PS2_KEY_IGNORE, // PS2_KEY_KP5 +// PS2_KEY_R_ARROW, // PS2_KEY_KP6 +// PS2_KEY_HOME, // PS2_KEY_KP7 +// PS2_KEY_UP_ARROW, // PS2_KEY_KP8 +// PS2_KEY_PGUP, // PS2_KEY_KP9 +// PS2_KEY_DELETE // PS2_KEY_KP_DOT +// }; +// +#endif diff --git a/main/include/PS2Mouse.h b/main/include/PS2Mouse.h deleted file mode 120000 index d32271c..0000000 --- a/main/include/PS2Mouse.h +++ /dev/null @@ -1 +0,0 @@ -../../../sharpkey/main/include/PS2Mouse.h \ No newline at end of file diff --git a/main/include/PS2Mouse.h b/main/include/PS2Mouse.h new file mode 100644 index 0000000..5134567 --- /dev/null +++ b/main/include/PS2Mouse.h @@ -0,0 +1,187 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// Name: PS2Mouse.h +// Created: Jan 2022 +// Version: v1.0 +// Author(s): Philip Smart +// Description: Header file for the PS/2 Mouse Class. +// +// Credits: +// Copyright: (c) 2022 Philip Smart +// +// History: Mar 2022 - Initial write. +// +// 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 . +///////////////////////////////////////////////////////////////////////////////////////////////////////// +#ifndef MOUSE_H_ +#define MOUSE_H_ + +#include +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_log.h" +#include "Arduino.h" +#include "driver/gpio.h" +#include "soc/timer_group_struct.h" +#include "soc/timer_group_reg.h" + +// PS/2 Mouse Class. +class PS2Mouse { + + // mode status flags + #define _PS2_BUSY 0x04 + #define _TX_MODE 0x02 + #define _WAIT_RESPONSE 0x01 + + // Constants. + #define MAX_PS2_XMIT_KEY_BUF 16 + #define MAX_PS2_RCV_KEY_BUF 16 + #define INTELLI_MOUSE 3 + #define SCALING_1_TO_1 0xE6 + #define DEFAULT_MOUSE_TIMEOUT 100 + + public: + // Public structures used for containment of mouse movement and control data. These are used by the insantiating + // object to request and process mouse data. + // Postional data - X/Y co-ordinates of mouse movement. + typedef struct { + int x, y; + } Position; + + // Mouse data, containing positional data, status, wheel data and validity. + typedef struct { + bool valid; + bool overrun; + int status; + Position position; + int wheel; + } MouseData; + + // Enumeration of all processed mouse responses. + enum Responses { + MOUSE_RESP_ACK = 0xFA, + }; + + // Enumeration of all processed mouse commands. + enum Commands { + MOUSE_CMD_SET_SCALING_1_1 = 0xE6, + MOUSE_CMD_SET_SCALING_2_1 = 0xE7, + MOUSE_CMD_SET_RESOLUTION = 0xE8, + MOUSE_CMD_GET_STATUS = 0xE9, + MOUSE_CMD_SET_STREAM_MODE = 0xEA, + MOUSE_CMD_REQUEST_DATA = 0xEB, + MOUSE_CMD_SET_REMOTE_MODE = 0xF0, + MOUSE_CMD_GET_DEVICE_ID = 0xF2, + MOUSE_CMD_SET_SAMPLE_RATE = 0xF3, + MOUSE_CMD_ENABLE_STREAMING = 0xF4, + MOUSE_CMD_DISABLE_STREAMING = 0xF5, + MOUSE_CMD_RESEND = 0xFE, + MOUSE_CMD_RESET = 0xFF, + }; + + // Resolution - the PS/2 mouse can digitize movement from 1mm to 1/8mm, the default being 1/4 (ie. 1mm = 4 counts). This allows configuration for a finer or rougher + // tracking digitisation. + enum PS2_RESOLUTION { + PS2_MOUSE_RESOLUTION_1_1 = 0x00, + PS2_MOUSE_RESOLUTION_1_2 = 0x01, + PS2_MOUSE_RESOLUTION_1_4 = 0x02, + PS2_MOUSE_RESOLUTION_1_8 = 0x03, + }; + + // Scaling - the PS/2 mouse can provide linear (1:1 no scaling) or non liner (2:1 scaling) adaptation of the digitised data. This allows configuration for amplification of movements. + enum PS2_SCALING { + PS2_MOUSE_SCALING_1_1 = 0x00, + PS2_MOUSE_SCALING_2_1 = 0x01, + }; + + // Sampling rate - the PS/2 mouse, in streaming mode, the mouse sends with movement updates. This allows for finer or rougher digitisation of movements. The default is 100 samples per + // second and the X68000 is fixed at 100 samples per second. Adjusting the ps/2 sample rate will affect tracking granularity on the X68000. + enum PS2_SAMPLING { + PS2_MOUSE_SAMPLE_RATE_10 = 10, + PS2_MOUSE_SAMPLE_RATE_20 = 20, + PS2_MOUSE_SAMPLE_RATE_40 = 40, + PS2_MOUSE_SAMPLE_RATE_60 = 60, + PS2_MOUSE_SAMPLE_RATE_80 = 80, + PS2_MOUSE_SAMPLE_RATE_100 = 100, + PS2_MOUSE_SAMPLE_RATE_200 = 200, + }; + + // Public accessible prototypes. + PS2Mouse(int clockPin, int dataPin); + ~PS2Mouse(); + void writeByte(uint8_t); + bool setResolution(enum PS2_RESOLUTION resolution); + bool setStreamMode(void); + bool setRemoteMode(void); + bool setScaling(enum PS2_SCALING scaling); + char getDeviceId(void); + bool checkIntelliMouseExtensions(void); + bool setSampleRate(enum PS2_SAMPLING rate); + bool enableStreaming(void); + bool disableStreaming(void); + bool getStatus(uint8_t *respBuf); + bool reset(void); + MouseData readData(void); + void initialize(void); + + // Method to register an object method for callback with context. + template + void setMouseDataCallback(A func_ptr, B obj_ptr) + { + ps2Ctrl.mouseDataCallback = bind(func_ptr, obj_ptr, 0, std::placeholders::_1); + } + + private: + // PS/2 Control structure - maintains all data and variables relevant to forming a connection with a PS/2 mouse, interaction and processing of its data. + struct { + int clkPin; // Hardware clock pin - bidirectional. + int dataPin; // Hardware data pin - bidirectional. + volatile uint8_t mode; // mode contains _PS2_BUSY bit 2 = busy until all expected bytes RX/TX + // _TX_MODE bit 1 = direction 1 = TX, 0 = RX (default) + // _WAIT_RESPONSE bit 0 = expecting data response + bool supportsIntelliMouseExtensions; // Intellimouse extensions supported. + bool streamingEnabled; // Streaming mode has been enabled. + volatile uint8_t bitCount; // Main state variable and bit count for interrupts + volatile uint8_t shiftReg; // Incoming/Outgoing data shift register. + volatile uint8_t parity; // Parity flag for data being sent/received. + uint16_t rxBuf[16]; // RX buffer - assembled bytes are stored in this buffer awaiting processing. + int rxPos; // Position in buffer to store next byte. + + // Callback for streaming processed mouse data to HID handler. + std::function mouseDataCallback; + } ps2Ctrl; + + // Structure to store incoming streamed mouse data along with validity flags. + struct { + MouseData mouseData; + bool newData; // An update has occurred since the last query. + bool overrun; // A data overrun has occurred since the last query. + } streaming; + + // Interrupt handler - needs to be declared static and assigned to internal RAM (within the ESP32) to function correctly. + IRAM_ATTR static void ps2interrupt( void ); + + // Prototypes. + bool requestData(uint8_t expectedBytes, uint8_t *respBuf, uint32_t timeout); + bool sendCmd(uint8_t cmd, uint8_t expectedBytes, uint8_t *respBuf, uint32_t timeout); + +}; + +#endif // MOUSE_H_ diff --git a/main/include/SWITCH.h b/main/include/SWITCH.h deleted file mode 120000 index ee1ef71..0000000 --- a/main/include/SWITCH.h +++ /dev/null @@ -1 +0,0 @@ -../../../sharpkey/main/include/SWITCH.h \ No newline at end of file diff --git a/main/include/SWITCH.h b/main/include/SWITCH.h new file mode 100644 index 0000000..5c84761 --- /dev/null +++ b/main/include/SWITCH.h @@ -0,0 +1,232 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// Name: SWITCH.h +// Created: May 2022 +// Version: v1.0 +// Author(s): Philip Smart +// Description: Class definition to encapsulate the SharpKey WiFi/Config Switch. +// +// Credits: +// Copyright: (c) 2019-2022 Philip Smart +// +// History: May 2022 - Initial write. +// v1.00 Jun 2022 - Updates to add additional callbacks for RESET and CLEARNVS +// +// 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 . +///////////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef SWITCH_H +#define SWITCH_H + +#include +#include +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/semphr.h" +#include "esp_log.h" +#include "esp_system.h" +#include "soc/timer_group_struct.h" +#include "soc/timer_group_reg.h" +#include "driver/timer.h" +#include "LED.h" + + +// NB: Macros definitions put inside class for clarity, they are still global scope. + +// Switch class. +class SWITCH { + + // Macros. + // + #define NUMELEM(a) (sizeof(a)/sizeof(a[0])) + + // Constants. + #define SWITCH_VERSION 1.00 + + public: + + // Prototypes. + SWITCH(LED *led); + SWITCH(void); + virtual ~SWITCH(void); + + // Method to register an object method for callback with context. + template + void setCancelEventCallback(A func_ptr, B obj_ptr) + { + swCtrl.cancelEventCallback = std::bind(func_ptr, obj_ptr); + } + template + void setCancelEventCallback(A func_ptr) + { + swCtrl.cancelEventCallback = std::bind(func_ptr); + } + // Wifi enable (configured mode). + template + void setWifiEnEventCallback(A func_ptr, B obj_ptr) + { + swCtrl.wifiEnEventCallback = std::bind(func_ptr, obj_ptr); + } + template + void setWifiEnEventCallback(A func_ptr) + { + swCtrl.wifiEnEventCallback = std::bind(func_ptr); + } + // Wifi default mode enable. + template + void setWifiDefEventCallback(A func_ptr, B obj_ptr) + { + swCtrl.wifiDefEventCallback = std::bind(func_ptr, obj_ptr); + } + template + void setWifiDefEventCallback(A func_ptr) + { + swCtrl.wifiDefEventCallback = std::bind(func_ptr); + } + // Bluetooth start pairing event. + template + void setBTPairingEventCallback(A func_ptr, B obj_ptr) + { + swCtrl.btPairingEventCallback = std::bind(func_ptr, obj_ptr); + } + template + void setBTPairingEventCallback(A func_ptr) + { + swCtrl.btPairingEventCallback = std::bind(func_ptr); + } + // RESET event - callback is executed prior to issuing an esp_restart(). + template + void setResetEventCallback(A func_ptr, B obj_ptr) + { + swCtrl.resetEventCallback = std::bind(func_ptr, obj_ptr); + } + template + void setResetEventCallback(A func_ptr) + { + swCtrl.resetEventCallback = std::bind(func_ptr); + } + // CLEARNVS event - callback when user requests all NVS settings are erased. + template + void setClearNVSEventCallback(A func_ptr, B obj_ptr) + { + swCtrl.clearNVSEventCallback = std::bind(func_ptr, obj_ptr); + } + template + void setClearNVSEventCallback(A func_ptr) + { + swCtrl.clearNVSEventCallback = std::bind(func_ptr); + } + + // Helper method to identify the sub class, this is used in non volatile key management. + // Warning: This method wont work if optimisation for size is enabled on the compiler. + const char *getClassName(const std::string& prettyFunction) + { + // First find the CLASS :: METHOD seperation. + size_t colons = prettyFunction.find("::"); + + // None, then this is not a class. + if (colons == std::string::npos) + return "::"; + + // Split out the class name. + size_t begin = prettyFunction.substr(0,colons).rfind(" ") + 1; + size_t end = colons - begin; + + // Return the name. + return(prettyFunction.substr(begin,end).c_str()); + } + + // Helper method to change a file extension. + void replaceExt(std::string& fileName, const std::string& newExt) + { + // Locals. + std::string::size_type extPos = fileName.rfind('.', fileName.length()); + + if(extPos != std::string::npos) + { + fileName.replace(extPos+1, newExt.length(), newExt); + } + return; + } + + // Template to aid in conversion of an enum to integer. + template constexpr typename std::underlying_type::type to_underlying(E e) noexcept + { + return static_cast::type>(e); + } + + // Method to return the class version number. + virtual float version(void) + { + return(SWITCH_VERSION); + } + + // Method to return the name of the class. + virtual std::string ifName(void) + { + return(swCtrl.swClassName); + } + + protected: + + private: + + // Prototypes. + void init(void); + IRAM_ATTR static void swInterface( void * pvParameters ); + inline uint32_t milliSeconds(void) + { + return( (uint32_t) clock() ); + } + + // Structure to maintain an active setting for the LED. The LED control thread uses these values to effect the required lighting of the LED. + typedef struct { + // Name of the class for this instantiation. + std::string swClassName; + + // Thread handles - Switch interface. + TaskHandle_t TaskSWIF = NULL; + + // Callback for Cancel Event. + std::function cancelEventCallback; + + // Callback for WiFi Enable Event. + std::function wifiEnEventCallback; + + // Callback for WiFi Default Event. + std::function wifiDefEventCallback; + + // Callback for Bluetooth Pairing Event. + std::function btPairingEventCallback; + + // Callback is executed prior to issuing an esp_restart(). + std::function resetEventCallback; + + // Callback when user requests all NVS settings are erased. + std::function clearNVSEventCallback; + } t_swControl; + + // Var to store all SWITCH control variables. + t_swControl swCtrl; + + // LED activity object handle. + LED *led; +}; +#endif // SWITCH_H diff --git a/main/include/WiFi.h b/main/include/WiFi.h deleted file mode 120000 index 2456c65..0000000 --- a/main/include/WiFi.h +++ /dev/null @@ -1 +0,0 @@ -../../../sharpkey/main/include/WiFi.h \ No newline at end of file diff --git a/main/include/WiFi.h b/main/include/WiFi.h new file mode 100644 index 0000000..9fdb2cc --- /dev/null +++ b/main/include/WiFi.h @@ -0,0 +1,400 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// Name: WiFi.h +// Created: Mar 2022 +// Version: v1.0 +// Author(s): Philip Smart +// Description: Header for the WiFi AP/Client logic. +// Credits: +// Copyright: (c) 2019-2022 Philip Smart +// +// History: Mar 2022 - Initial write. +// v1.01 May 2022 - Initial release version. +// v1.02 Jun 2022 - Seperated out the WiFi Enable switch and made the WiFi module active/ +// via a reboot process. This is necessary now that Bluetooth is inbuilt +// as the ESP32 shares an antenna and both operating together electrically +// is difficult but also the IDF stack conflicts as well. +// +// 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 . +///////////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef WIFI_H +#define WIFI_H + +#if defined(CONFIG_IF_WIFI_ENABLED) + #include "freertos/event_groups.h" + #include "esp_system.h" + #include "esp_wifi.h" + #include "esp_event.h" + #include "nvs_flash.h" + #include "lwip/err.h" + #include "lwip/sys.h" + #include + #include "esp_littlefs.h" + #include + #include + #include + #include + #include "NVS.h" + #include "LED.h" + #include "HID.h" + + // Include the specification class. + #include "KeyInterface.h" + + // Encapsulate the WiFi functionality. + class WiFi { + // Constants. + #define WIFI_VERSION 1.02 + #define OBJECT_VERSION_LIST_MAX 18 + #define FILEPACK_VERSION_FILE "version.txt" + #define WIFI_AP_DEFAULT_IP "192.168.4.1" + #define WIFI_AP_DEFAULT_GW "192.168.4.1" + #define WIFI_AP_DEFAULT_NETMASK "255.255.255.0" + + // The event group allows multiple bits for each event, but we only care about two events: + // - we are connected to the AP with an IP + // - we failed to connect after the maximum amount of retries + #define WIFI_CONNECTED_BIT BIT0 + #define WIFI_FAIL_BIT BIT1 + + // Tag for ESP WiFi logging. + #define WIFITAG "WiFi" + + // Menu selection types. + enum WIFIMODES { + WIFI_OFF = 0x00, // WiFi is disabled. + WIFI_ON = 0x01, // WiFi is enabled. + WIFI_CONFIG_AP = 0x02, // WiFi is set to enable Access Point to configure WiFi settings. + WIFI_CONFIG_CLIENT = 0x03 // WiFi is set to enable Client mode using persisted settings. + }; + + // Default WiFi parameters. + #define MAX_WIFI_SSID_LEN 31 + #define MAX_WIFI_PWD_LEN 63 + #define MAX_WIFI_IP_LEN 15 + #define MAX_WIFI_NETMASK_LEN 15 + #define MAX_WIFI_GATEWAY_LEN 15 + + // Buffer size for sending file data in chunks to the browser. + #define MAX_CHUNK_SIZE 4096 + + // Max length a file path can have on the embedded storage device. + #define FILE_PATH_MAX (15 + CONFIG_LITTLEFS_OBJ_NAME_LEN) + + public: + // Types for holding and maintaining a class/object to version number array. + typedef struct { + std::string object; + float version; + } t_versionItem; + typedef struct { + int elements; + t_versionItem *item[OBJECT_VERSION_LIST_MAX]; + } t_versionList; + + // Prototypes. + WiFi(KeyInterface *hdlKeyIf, KeyInterface *hdlMouseIf, bool defaultMode, NVS *nvs, LED *led, const char *fsPath, t_versionList *versionList); + WiFi(void); + ~WiFi(void); + void run(void); + + // Primary encapsulated interface object handle. + KeyInterface *keyIf; + + // Secondary encapsulated interface object handle. + KeyInterface *mouseIf; + + // Non Volatile Storage handle. + NVS *nvs; + + // LED activity handle. + LED *led; + + // Method to return the class version number. + float version(void) + { + return(WIFI_VERSION); + } + + protected: + + private: + + // Type for key:value pairs. + typedef struct { + std::string name; + std::string value; + } t_kvPair; + + // Structure to maintain wifi configuration data. This data is persisted through powercycles as needed. + typedef struct { + // Client access parameters, these, when valid, are used for binding to a known wifi access point. + struct { + bool valid; + char ssid[MAX_WIFI_SSID_LEN+1]; + char pwd[MAX_WIFI_PWD_LEN+1]; + bool useDHCP; + char ip[MAX_WIFI_IP_LEN+1]; + char netmask[MAX_WIFI_NETMASK_LEN+1]; + char gateway[MAX_WIFI_GATEWAY_LEN+1]; + } clientParams; + + // Structure to maintain Access Point parameters. These are configurable to allow possibility of changing them. + struct { + char ssid[MAX_WIFI_SSID_LEN+1]; + char pwd[MAX_WIFI_PWD_LEN+1]; + char ip[MAX_WIFI_IP_LEN+1]; + char netmask[MAX_WIFI_NETMASK_LEN+1]; + char gateway[MAX_WIFI_GATEWAY_LEN+1]; + } apParams; + + // General runtime control parameters. + struct { + // Configured mode of the Wifi: Access Point or Client. + enum WIFIMODES wifiMode; + } params; + } t_wifiConfig; + + // Configuration data. + t_wifiConfig wifiConfig; + + // Structure to manage the WiFi control variables, signifying the state of the Client or Access Point, runtime dependent, and + // necessary dedicated run variables (as opposed to locals). + typedef struct { + // Client mode variables, active when in client mode. + struct { + int clientRetryCnt; + bool connected; + char ssid[MAX_WIFI_SSID_LEN+1]; + char pwd[MAX_WIFI_PWD_LEN+1]; + char ip[MAX_WIFI_IP_LEN+1]; + char netmask[MAX_WIFI_NETMASK_LEN+1]; + char gateway[MAX_WIFI_GATEWAY_LEN+1]; + } client; + + // Access Point mode variabls, active when in AP mode. + struct { + char ssid[MAX_WIFI_SSID_LEN+1]; + char pwd[MAX_WIFI_PWD_LEN+1]; + char ip[MAX_WIFI_IP_LEN+1]; + char netmask[MAX_WIFI_NETMASK_LEN+1]; + char gateway[MAX_WIFI_GATEWAY_LEN+1]; + } ap; + + // HTTP session variables, parsed out of incoming connections. The sessions are synchronous so only maintain + // one copy. + struct { + std::string host; + std::string queryStr; + std::string fileName; + std::string filePath; + bool gzip; + bool deflate; + } session; + + // Runtime variables, used for global control of the WiFi module. + // + struct { + // Default path on the underlying filesystem. This is where the NVS/SD partition is mounted and all files under this directory are accessible. + const char * fsPath; + + // Version list of all objects used to build the SharpKey interface along with their version numbers. + t_versionList *versionList; + + // Run mode of the Wifi: Off, On or Access Point. + enum WIFIMODES wifiMode; + + // Handle to http server. + httpd_handle_t server; + + // Class name, used for NVS keys. + std::string thisClass; + + // Flag to raise a reboot button on the displayed page. + bool rebootButton; + + // Flag to indicate a hard reboot needed. + bool reboot; + + // Base path of file storag. + char basePath[FILE_PATH_MAX]; + + // String to hold any response error message. + std::string errorMsg; + } run; + } t_wifiControl; + + // Control data. + t_wifiControl wifiCtrl; + + // Prototypes. + bool setupWifiClient(void); + bool setupWifiAP(void); + bool stopWifi(void); + bool startWebserver(void); + void stopWebserver(void); + float getVersionNumber(std::string name); + esp_err_t expandAndSendFile(httpd_req_t *req, const char *basePath, std::string fileName); + esp_err_t expandVarsAndSend(httpd_req_t *req, std::string str); + esp_err_t sendKeyMapHeaders(httpd_req_t *req); + esp_err_t sendKeyMapTypes(httpd_req_t *req); + esp_err_t sendKeyMapCustomTypeFields(httpd_req_t *req); + esp_err_t sendKeyMapData(httpd_req_t *req); + esp_err_t sendKeyMapPopovers(httpd_req_t *req); + esp_err_t sendMouseRadioChoice(httpd_req_t *req, const char *option); + + + + esp_err_t wifiDataPOSTHandler(httpd_req_t *req, std::vector pairs, std::string& resp); + esp_err_t mouseDataPOSTHandler(httpd_req_t *req, std::vector pairs, std::string& resp); + static esp_err_t defaultDataPOSTHandler(httpd_req_t *req); + static esp_err_t defaultDataGETHandler(httpd_req_t *req); + IRAM_ATTR static esp_err_t otaFirmwareUpdatePOSTHandler(httpd_req_t *req); + IRAM_ATTR static esp_err_t otaFilepackUpdatePOSTHandler(httpd_req_t *req); + static esp_err_t keymapUploadPOSTHandler(httpd_req_t *req); + static esp_err_t keymapTablePOSTHandler(httpd_req_t *req); + + static esp_err_t defaultRebootHandler(httpd_req_t *req); + esp_err_t getPOSTData(httpd_req_t *req, std::vector *pairs); + + bool isFileExt(std::string fileName, std::string extension); + esp_err_t setContentTypeFromFileType(httpd_req_t *req, std::string fileName); + esp_err_t getPathFromURI(std::string& destPath, std::string& destFile, const char *basePath, const char *uri); + esp_err_t getPathFromURI(std::string& destPath, const char *basePath, const char *uri); + static esp_err_t defaultFileHandler(httpd_req_t *req); + std::string esp32PartitionType(esp_partition_type_t type); + std::string esp32PartitionSubType(esp_partition_subtype_t subtype); + + + IRAM_ATTR static void pairBluetoothDevice(void *pvParameters); + IRAM_ATTR static void wifiAPHandler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data); + IRAM_ATTR static void wifiClientHandler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data); + + // Method to split a string based on a delimiter and store in a vector. + std::vector split(std::string s, std::string delimiter) + { + // Locals. + size_t pos_start = 0; + size_t pos_end; + size_t delim_len = delimiter.length(); + std::string token; + std::vector res; + + // Loop through the string locating delimiters and split on each occurrence. + while((pos_end = s.find (delimiter, pos_start)) != std::string::npos) + { + token = s.substr (pos_start, pos_end - pos_start); + pos_start = pos_end + delim_len; + // Push each occurrence onto Vector. + res.push_back (token); + } + + // Store last item in vector. + res.push_back (s.substr (pos_start)); + return res; + } + + // check if a given string is a numeric string or not + bool isNumber(const std::string &str) + { + // `std::find_first_not_of` searches the string for the first character + // that does not match any of the characters specified in its arguments + return !str.empty() && + (str.find_first_not_of("[0123456789]") == std::string::npos); + } + + // Function to split string `str` using a given delimiter + std::vector split(const std::string &str, char delim) + { + auto i = 0; + std::vector list; + + auto pos = str.find(delim); + + while (pos != std::string::npos) + { + list.push_back(str.substr(i, pos - i)); + i = ++pos; + pos = str.find(delim, pos); + } + + list.push_back(str.substr(i, str.length())); + + return list; + } + + // Function to validate an IP address + bool validateIP(std::string ip) + { + // split the string into tokens + std::vector list = split(ip, '.'); + + // if the token size is not equal to four + if (list.size() != 4) { + return false; + } + + // validate each token + for (std::string str: list) + { + // verify that the string is a number or not, and the numbers + // are in the valid range + if (!isNumber(str) || std::stoi(str) > 255 || std::stoi(str) < 0) { + return false; + } + } + + return true; + } + + // Method to split an IP4 address into its components, checking each for validity. + bool splitIP(std::string ip, int *a, int *b, int *c, int *d) + { + // Init. + *a = *b = *c = *d = 0; + + // split the string into tokens + std::vector list = split(ip, '.'); + + // if the token size is not equal to four + if (list.size() != 4) { +printf("Size:%d\n", list.size()); + return false; + } + // Loop through vector and check each number for validity before assigning. + for(int idx=0; idx < 4; idx++) + { + // verify that the string is a number or not, and the numbers + // are in the valid range + if (!isNumber(list.at(idx)) || std::stoi(list.at(idx)) > 255 || std::stoi(list.at(idx)) < 0) { +printf("Item:%d, %s\n", idx, list.at(idx).c_str()); + return false; + } + int frag = std::stoi(list.at(idx)); + if(idx == 0) *a = frag; + if(idx == 1) *b = frag; + if(idx == 2) *c = frag; + if(idx == 3) *d = frag; + } + return true; + } + + }; +#endif + +#endif // WIFI_H diff --git a/main/include/X1.h b/main/include/X1.h deleted file mode 120000 index 9c4fcb6..0000000 --- a/main/include/X1.h +++ /dev/null @@ -1 +0,0 @@ -../../../sharpkey/main/include/X1.h \ No newline at end of file diff --git a/main/include/X1.h b/main/include/X1.h new file mode 100644 index 0000000..51b15fd --- /dev/null +++ b/main/include/X1.h @@ -0,0 +1,687 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// Name: X1.h +// Created: Mar 2022 +// Version: v1.0 +// Author(s): Philip Smart +// Description: Header for the Sharp X1 to PS/2 interface logic. +// 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 reflect changes realised in other modules due to addition of +// bluetooth and suspend logic due to NVS issues using both cores. +// v1.03 Jun 2022 - Further updates adding in keymaps for UK BT and Japan OADG109. +// +// 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 . +///////////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef X1_H +#define X1_H + +// Include the specification class. +#include "KeyInterface.h" +#include "NVS.h" +#include "LED.h" +#include "HID.h" +#include +#include + +// NB: Macros definitions put inside class for clarity, they are still global scope. + +// Encapsulate the Sharp X1 interface. +class X1 : public KeyInterface { + // Macros. + // + #define NUMELEM(a) (sizeof(a)/sizeof(a[0])) + + // Constants. + #define X1IF_VERSION 1.03 + #define X1IF_KEYMAP_FILE "X1_KeyMap.BIN" + #define MAX_X1_XMIT_KEY_BUF 16 + #define PS2TBL_X1_MAXROWS 349 + + // X1 Key control bit mask. + #define X1_CTRL_TENKEY ((unsigned char) (1 << 7)) + #define X1_CTRL_PRESS ((unsigned char) (1 << 6)) + #define X1_CTRL_REPEAT ((unsigned char) (1 << 5)) + #define X1_CTRL_GRAPH ((unsigned char) (1 << 4)) + #define X1_CTRL_CAPS ((unsigned char) (1 << 3)) + #define X1_CTRL_KANA ((unsigned char) (1 << 2)) + #define X1_CTRL_SHIFT ((unsigned char) (1 << 1)) + #define X1_CTRL_CTRL ((unsigned char) (1 << 0)) + + // X1 Special Key definitions. + #define X1KEY_UP 0x1E // ↑ + #define X1KEY_DOWN 0x1F // ↓ + #define X1KEY_LEFT 0x1D // ↠+ #define X1KEY_RIGHT 0x1C // → → + #define X1KEY_INS 0x12 // INS + #define X1KEY_DEL 0x08 // DEL + #define X1KEY_CLR 0x0C // CLR + #define X1KEY_HOME 0x0B // HOME + + // PS2 Flag definitions. + #define PS2CTRL_NONE 0x00 // No keys active = 0 + #define PS2CTRL_SHIFT 0x01 // Shfit Key active = 1 + #define PS2CTRL_CTRL 0x02 // Ctrl Key active = 1 + #define PS2CTRL_CAPS 0x04 // CAPS active = 1 + #define PS2CTRL_KANA 0x08 // KANA active = 1 + #define PS2CTRL_GRAPH 0x10 // GRAPH active = 1 + #define PS2CTRL_GUI 0x20 // GUI Key active = 1 + #define PS2CTRL_FUNC 0x40 // Special Function Keys active = 1 + #define PS2CTRL_BREAK 0x80 // BREAK Key active = 1 + #define PS2CTRL_EXACT 0x80 // EXACT Match active = 1 + + // The initial mapping is made inside the PS2KeyAdvanced class from Scan Code Set 2 to ASCII + // for a selected keyboard. Special functions are detected and combined inside this module + // before mapping with the table below to extract the X1 key code and control data. + // ie. PS/2 Scan Code -> ASCII + Flags -> X1 Key Code + Ctrl Data + + // Keyboard mapping table column names. + #define PS2TBL_PS2KEYCODE_NAME "PS/2 KeyCode" + #define PS2TBL_PS2CTRL_NAME "PS/2 Control Key" + #define PS2TBL_KEYBOARDMODEL_NAME "For Keyboard" + #define PS2TBL_MACHINE_NAME "For Host Model" + #define PS2TBL_X1MODE_NAME "X1 Mode" + #define PS2TBL_X1KEYCODE_NAME "X1 KeyCode 1" + #define PS2TBL_X1KEYCODE_BYTE2_NAME "X1 KeyCode 2" + #define PS2TBL_X1_CTRL_NAME "X1 Control Key" + + // Keyboard mapping table column types. + #define PS2TBL_PS2KEYCODE_TYPE "hex" + #define PS2TBL_PS2CTRL_TYPE "custom_cbp_ps2ctrl" + #define PS2TBL_KEYBOARDMODEL_TYPE "custom_cbp_keybmodel" + #define PS2TBL_MACHINE_TYPE "custom_cbp_machine" + #define PS2TBL_X1MODE_TYPE "custom_cbp_x1mode" + #define PS2TBL_X1KEYCODE_TYPE "hex" + #define PS2TBL_X1KEYCODE_BYTE2_TYPE "hex" + #define PS2TBL_X1CTRL_TYPE "custom_cbn_x1ctrl" + + // Keyboard mapping table select list for PS2CTRL. + #define PS2TBL_PS2CTRL_SEL_NONE "NONE" + #define PS2TBL_PS2CTRL_SEL_SHIFT "SHIFT" + #define PS2TBL_PS2CTRL_SEL_CTRL "CTRL" + #define PS2TBL_PS2CTRL_SEL_CAPS "CAPS" + #define PS2TBL_PS2CTRL_SEL_KANA "KANA" + #define PS2TBL_PS2CTRL_SEL_GRAPH "GRAPH" + #define PS2TBL_PS2CTRL_SEL_GUI "GUI" + #define PS2TBL_PS2CTRL_SEL_FUNC "FUNC" + #define PS2TBL_PS2CTRL_SEL_EXACT "EXACT" + + // Keyboard mapping table select list for Model of keyboard. + #define KEYMAP_SEL_STANDARD "ALL" + #define KEYMAP_SEL_UK_WYSE_KB3926 "UK_WYSE_KB3926" + #define KEYMAP_SEL_JAPAN_OADG109 "JAPAN_OADG109" + #define KEYMAP_SEL_JAPAN_SANWA_SKBL1 "JAPAN_SANWA_SKBL1" + #define KEYMAP_SEL_NOT_ASSIGNED_4 "KEYBOARD_4" + #define KEYMAP_SEL_NOT_ASSIGNED_5 "KEYBOARD_5" + #define KEYMAP_SEL_NOT_ASSIGNED_6 "KEYBOARD_6" + #define KEYMAP_SEL_UK_PERIBOARD_810 "UK_PERIBOARD_810" + #define KEYMAP_SEL_UK_OMOTON_K8508 "UK_OMOTON_K8508" + + // Keyboard mapping table select list for keyboard mode. + #define X1_SEL_MODE_A "Mode_A" + #define X1_SEL_MODE_B "Mode_B" + + // Keyboard mapping table select list for target machine. + #define X1_SEL_ALL "ALL" + #define X1_SEL_ORIG "ORIGINAL" + #define X1_SEL_TURBO "TURBO" + #define X1_SEL_TURBOZ "TURBOZ" + + // Keyboard mapping table select list for X1 Control codes. + #define X1_CTRL_SEL_TENKEY "TENKEY" + #define X1_CTRL_SEL_PRESS "PRESS" + #define X1_CTRL_SEL_REPEAT "REPEAT" + #define X1_CTRL_SEL_GRAPH "GRAPH" + #define X1_CTRL_SEL_CAPS "CAPS" + #define X1_CTRL_SEL_KANA "KANA" + #define X1_CTRL_SEL_SHIFT "SHIFT" + #define X1_CTRL_SEL_CTRL "CTRL" + + // The Sharp X1 Series was released over a number of years and each iteration added changes/updates. In order to cater for differences, it is possible to assign a key mapping + // to a specific machine type(s) or all of the series by adding the flags below into the mapping table. + #define X1_ALL 0xFF + #define X1_ORIG 0x01 + #define X1_TURBO 0x02 + #define X1_TURBOZ 0x04 + + // The X1 Turbo onwards had a mode switch on the keyboard, Mode A was normal use, Mode B was for games, speeding up the key press by shortening the timing and setting common game keys pressed in a 24bit bit map. + // The mapping table caters for both, OR'ing data in Mode B so that multiple key presses are sent across as a bit map. + #define X1_MODE_A 0x01 + #define X1_MODE_B 0x02 + + // Keyboard models. The base on which this interface was created was a Wyse KB3926 PS/2 Keyboard and this is deemed STANDARD. Other models need to insert difference maps + // prior to the STANDARD entry along with the keyboard model so that it is processed first thus allowing differing keyboards with different maps. + #define KEYMAP_STANDARD 0xFF + #define KEYMAP_UK_WYSE_KB3926 0x01 + #define KEYMAP_JAPAN_OADG109 0x02 + #define KEYMAP_JAPAN_SANWA_SKBL1 0x04 + #define KEYMAP_NOT_ASSIGNED_4 0x08 + #define KEYMAP_NOT_ASSIGNED_5 0x10 + #define KEYMAP_NOT_ASSIGNED_6 0x20 + #define KEYMAP_UK_PERIBOARD_810 0x40 + #define KEYMAP_UK_OMOTON_K8508 0x80 + + public: + // Prototypes. + X1(void); + X1(uint32_t ifMode, NVS *hdlNVS, LED *hdlLED, HID *hdlHID, const char *fsPath); + X1(NVS *hdlNVS, HID *hdlHID, const char *fsPath); + ~X1(void); + bool createKeyMapFile(std::fstream &outFile); + bool storeDataToKeyMapFile(std::fstream &outFile, char *data, int size); + bool storeDataToKeyMapFile(std::fstream & outFile, std::vector& dataArray); + bool closeAndCommitKeyMapFile(std::fstream &outFile, bool cleanupOnly); + std::string getKeyMapFileName(void) { return(X1IF_KEYMAP_FILE); }; + void getKeyMapHeaders(std::vector& headerList); + void getKeyMapTypes(std::vector& typeList); + bool getKeyMapSelectList(std::vector>& selectList, std::string option); + bool getKeyMapData(std::vector& dataArray, int *row, bool start); + + // Method to return the class version number. + float version(void) + { + return(X1IF_VERSION); + } + + protected: + + private: + // Prototypes. + void pushKeyToQueue(bool keybMode, uint32_t key); + IRAM_ATTR static void x1Interface( void * pvParameters ); + IRAM_ATTR static void hidInterface( void * pvParameters ); + void selectOption(uint8_t optionCode); + uint32_t mapKey(uint16_t scanCode); + bool loadKeyMap(); + bool saveKeyMap(void); + void init(uint32_t ifMode, NVS *hdlNVS, LED *hdlLED, HID *hdlHID); + void init(NVS *hdlNVS, HID *hdlHID); + +// // Overload the base yield method to include suspension of the PS/2 Keyboard interface. This interface uses interrupts which are not mutex protected and clash with the +// // WiFi API methods. +// inline void yield(uint32_t delay) +// { +// // If suspended, go into a permanent loop until the suspend flag is reset. +// if(this->suspend) +// { +// // Suspend the keyboard interface. +// Keyboard->suspend(true); +// +// // Use the base method logic. +// KeyInterface::yield(delay); +// +// // Release the keyboard interface. +// Keyboard->suspend(false); +// } else +// // Otherwise just delay by the required amount for timing and to give other threads a time slice. +// { +// KeyInterface::yield(delay); +// } +// return; +// } + + // Structure to encapsulate a single key map from PS/2 to X1. + typedef struct { + uint8_t ps2KeyCode; + uint8_t ps2Ctrl; + uint8_t keyboardModel; + uint8_t machine; + uint8_t x1Mode; + uint8_t x1Key; + uint8_t x1Key2; + uint8_t x1Ctrl; + } t_keyMapEntry; + + // Structure to encapsulate the entire static keyboard mapping table. + typedef struct { + t_keyMapEntry kme[PS2TBL_X1_MAXROWS]; + } t_keyMap; + + // Structure to maintain the X1 interface configuration data. This data is persisted through powercycles as needed. + typedef struct { + struct { + uint8_t activeKeyboardMap; // Model of keyboard a keymap entry is applicable to. + uint8_t activeMachineModel; // Machine model a keymap entry is applicable to. + } params; + } t_x1Config; + + // Configuration data. + t_x1Config x1Config; + + // Structure to manage the control signals signifying the state of the X1 keyboard. + typedef struct { + bool optionSelect; // Flag to indicate a user requested keyboard configuration option is being selected. + bool modeB; // Mode B (Game mode) flag. If set, Mode B active, clear, Mode A active (normal keyboard). + uint8_t keyCtrl; // Keyboard state flag control. + + std::string fsPath; // Path on the underlying filesystem where storage is mounted and accessible. + t_keyMapEntry *kme; // Pointer to an array in memory to contain PS2 to X1 mapping values. + int kmeRows; // Number of rows in the kme table. + std::string keyMapFileName; // Name of file where extension or replacement key map entries are stored. + bool persistConfig; // Flag to request saving of the config into NVS storage. + } t_x1Control; + + // Transmit buffer queue item. + typedef struct { + uint32_t keyCode; // 32bit because normal mode A is 16bit, game mode B is 24bit + bool modeB; // True if in game mode B. + } t_xmitQueueMessage; + + // Thread handles - one per function, ie. HID interface and host target interface. + TaskHandle_t TaskHostIF = NULL; + TaskHandle_t TaskHIDIF = NULL; + + // Control structure to control interaction and mapping of keys for the host. + t_x1Control x1Control; + + // Spin lock mutex to hold a coresied to an uninterruptable method. This only works on dual core ESP32's. + portMUX_TYPE x1Mutex; + + // Lookup table to match PS/2 codes to X1 Key and Control Data. + // + // Given that the X1 had many variants, with potential differences between them, the mapping table allows for ALL or variant specific entries, the first entry matching is selected. + // + + // + // This mapping is for the UK Wyse KB-3926 PS/2 keyboard + // + t_keyMap PS2toX1 = { + { + // HELP + // COPY + // ModeB Byte1 ModeB Byte2 ModeB Byte3 + //PS2 Code PS2 Ctrl (Flags to Match) Keyboard Model Machine X1 Keyb Mode X1 Data X1 Ctrl (Flags to Set). + { PS2_KEY_F1, PS2CTRL_FUNC | PS2CTRL_SHIFT, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'v', 0x00, 0xFF & ~(X1_CTRL_PRESS | X1_CTRL_TENKEY), }, // SHIFT+F1 + { PS2_KEY_F2, PS2CTRL_FUNC | PS2CTRL_SHIFT, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'w', 0x00, 0xFF & ~(X1_CTRL_PRESS | X1_CTRL_TENKEY), }, // SHIFT+F2 + { PS2_KEY_F3, PS2CTRL_FUNC | PS2CTRL_SHIFT, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'x', 0x00, 0xFF & ~(X1_CTRL_PRESS | X1_CTRL_TENKEY), }, // SHIFT+F3 + { PS2_KEY_F4, PS2CTRL_FUNC | PS2CTRL_SHIFT, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'y', 0x00, 0xFF & ~(X1_CTRL_PRESS | X1_CTRL_TENKEY), }, // SHIFT+F4 + { PS2_KEY_F5, PS2CTRL_FUNC | PS2CTRL_SHIFT, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'z', 0x00, 0xFF & ~(X1_CTRL_PRESS | X1_CTRL_TENKEY), }, // SHIFT+F5 + { PS2_KEY_F1, PS2CTRL_FUNC, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'q', 0x00, 0xFF & ~(X1_CTRL_PRESS | X1_CTRL_TENKEY), }, // F1 + { PS2_KEY_F2, PS2CTRL_FUNC, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'r', 0x00, 0xFF & ~(X1_CTRL_PRESS | X1_CTRL_TENKEY), }, // F2 + { PS2_KEY_F3, PS2CTRL_FUNC, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 's', 0x00, 0xFF & ~(X1_CTRL_PRESS | X1_CTRL_TENKEY), }, // F3 + { PS2_KEY_F4, PS2CTRL_FUNC, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 't', 0x00, 0xFF & ~(X1_CTRL_PRESS | X1_CTRL_TENKEY), }, // F4 + { PS2_KEY_F5, PS2CTRL_FUNC, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'u', 0x00, 0xFF & ~(X1_CTRL_PRESS | X1_CTRL_TENKEY), }, // F5 + { PS2_KEY_F6, PS2CTRL_FUNC, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xEC, 0x00, 0xFF & ~(X1_CTRL_PRESS | X1_CTRL_TENKEY), }, // F6 + { PS2_KEY_F7, PS2CTRL_FUNC, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xEB, 0x00, 0xFF & ~(X1_CTRL_PRESS | X1_CTRL_TENKEY), }, // F7 + { PS2_KEY_F8, PS2CTRL_FUNC, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xE2, 0x00, 0xFF & ~(X1_CTRL_PRESS | X1_CTRL_TENKEY), }, // F8 + { PS2_KEY_F9, PS2CTRL_FUNC, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xE1, 0x00, 0xFF & ~(X1_CTRL_PRESS | X1_CTRL_TENKEY), }, // F9 + { PS2_KEY_F10, PS2CTRL_FUNC, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x00, 0x00, 0xFF & ~(X1_CTRL_PRESS | X1_CTRL_TENKEY), }, // XFER + { PS2_KEY_F11, PS2CTRL_FUNC, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xFE, 0x00, 0xFF & ~(X1_CTRL_PRESS | X1_CTRL_TENKEY), }, // HELP + { PS2_KEY_F12, PS2CTRL_FUNC, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x00, 0x00, 0xFF & ~(X1_CTRL_PRESS | X1_CTRL_TENKEY), }, // COPY + { PS2_KEY_TAB, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x09, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // TAB + // Control keys. + { PS2_KEY_APOS, PS2CTRL_CTRL, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x00, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // CTRL-@ + { PS2_KEY_A, PS2CTRL_CTRL, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x01, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // CTRL-A + { PS2_KEY_B, PS2CTRL_CTRL, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x02, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // CTRL-B + { PS2_KEY_C, PS2CTRL_CTRL, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x03, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // CTRL-C + { PS2_KEY_D, PS2CTRL_CTRL, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x04, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // CTRL-D + { PS2_KEY_E, PS2CTRL_CTRL, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x05, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // CTRL-E + { PS2_KEY_F, PS2CTRL_CTRL, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x06, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // CTRL-F + { PS2_KEY_G, PS2CTRL_CTRL, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x07, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // CTRL-G + { PS2_KEY_H, PS2CTRL_CTRL, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x08, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // CTRL-H + { PS2_KEY_I, PS2CTRL_CTRL, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x09, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // CTRL-I + { PS2_KEY_J, PS2CTRL_CTRL, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x0A, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // CTRL-J + { PS2_KEY_K, PS2CTRL_CTRL, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x0B, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // CTRL-K + { PS2_KEY_L, PS2CTRL_CTRL, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x0C, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // CTRL-L + { PS2_KEY_M, PS2CTRL_CTRL, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x0D, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // CTRL-M + { PS2_KEY_N, PS2CTRL_CTRL, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x0E, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // CTRL-N + { PS2_KEY_O, PS2CTRL_CTRL, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x0F, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // CTRL-O + { PS2_KEY_P, PS2CTRL_CTRL, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x10, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // CTRL-P + { PS2_KEY_Q, PS2CTRL_CTRL, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x11, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // CTRL-Q + { PS2_KEY_R, PS2CTRL_CTRL, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x12, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // CTRL-R + { PS2_KEY_S, PS2CTRL_CTRL, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x13, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // CTRL-S + { PS2_KEY_T, PS2CTRL_CTRL, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x14, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // CTRL-T + { PS2_KEY_U, PS2CTRL_CTRL, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x15, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // CTRL-U + { PS2_KEY_V, PS2CTRL_CTRL, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x16, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // CTRL-V + { PS2_KEY_W, PS2CTRL_CTRL, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x17, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // CTRL-W + { PS2_KEY_X, PS2CTRL_CTRL, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x18, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // CTRL-X + { PS2_KEY_Y, PS2CTRL_CTRL, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x19, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // CTRL-Y + { PS2_KEY_Z, PS2CTRL_CTRL, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x1A, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // CTRL-Z + { PS2_KEY_OPEN_SQ, PS2CTRL_CTRL, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x1B, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // CTRL-[ + { PS2_KEY_BACK, PS2CTRL_CTRL, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x1C, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // CTRL-BACKSLASH + { PS2_KEY_CLOSE_SQ, PS2CTRL_CTRL, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x1D, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // CTRL-] + { PS2_KEY_6, PS2CTRL_CTRL, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x1E, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // CTRL-^ + { PS2_KEY_MINUS, PS2CTRL_CTRL, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x1F, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // CTRL-_ + // Numeric keys. + { PS2_KEY_0, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '0', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // 0 + { PS2_KEY_1, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '1', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // 1 + { PS2_KEY_2, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '2', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // 2 + { PS2_KEY_3, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '3', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // 3 + { PS2_KEY_4, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '4', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // 4 + { PS2_KEY_5, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '5', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // 5 + { PS2_KEY_6, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '6', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // 6 + { PS2_KEY_7, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '7', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // 7 + { PS2_KEY_8, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '8', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // 8 + { PS2_KEY_9, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '9', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // 9 + // Punctuation keys. + { PS2_KEY_0, PS2CTRL_SHIFT, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, ')', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // Close Right Bracket ) + { PS2_KEY_1, PS2CTRL_SHIFT, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '!', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // Exclamation + { PS2_KEY_2, PS2CTRL_SHIFT, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '"', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // Double quote. + { PS2_KEY_3, PS2CTRL_SHIFT, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x23, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // Pound Sign -> Hash + { PS2_KEY_4, PS2CTRL_SHIFT, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '$', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // Dollar + { PS2_KEY_5, PS2CTRL_SHIFT, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '%', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // Percent + { PS2_KEY_6, PS2CTRL_SHIFT, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '^', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // Kappa + { PS2_KEY_7, PS2CTRL_SHIFT, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '&', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // Ampersand + { PS2_KEY_8, PS2CTRL_SHIFT, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '*', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // Star + { PS2_KEY_9, PS2CTRL_SHIFT, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '(', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // Open Left Bracket ( + // ALPHA keys, lower and uppercase. + // ModeB Byte1 ModeB Byte2 ModeB Byte3 + //PS2 Code PS2 Ctrl (Flags to Match) Machine X1 Keyb Mode X1 Data X1 Ctrl (Flags to Set). + { PS2_KEY_A, PS2CTRL_SHIFT | PS2CTRL_CAPS, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'a', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // a + { PS2_KEY_A, PS2CTRL_CAPS, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'A', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // A + { PS2_KEY_B, PS2CTRL_SHIFT | PS2CTRL_CAPS, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'b', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // b + { PS2_KEY_B, PS2CTRL_CAPS, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'B', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // B + { PS2_KEY_C, PS2CTRL_SHIFT | PS2CTRL_CAPS, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'c', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // c + { PS2_KEY_C, PS2CTRL_CAPS, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'C', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // C + { PS2_KEY_D, PS2CTRL_SHIFT | PS2CTRL_CAPS, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'd', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // d + { PS2_KEY_D, PS2CTRL_CAPS, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'D', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // D + { PS2_KEY_E, PS2CTRL_SHIFT | PS2CTRL_CAPS, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'e', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // e + { PS2_KEY_E, PS2CTRL_CAPS, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'E', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // E + { PS2_KEY_F, PS2CTRL_SHIFT | PS2CTRL_CAPS, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'f', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // f + { PS2_KEY_F, PS2CTRL_CAPS, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'F', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // F + { PS2_KEY_G, PS2CTRL_SHIFT | PS2CTRL_CAPS, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'g', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // g + { PS2_KEY_G, PS2CTRL_CAPS, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'G', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // G + { PS2_KEY_H, PS2CTRL_SHIFT | PS2CTRL_CAPS, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'h', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // h + { PS2_KEY_H, PS2CTRL_CAPS, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'H', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // H + { PS2_KEY_I, PS2CTRL_SHIFT | PS2CTRL_CAPS, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'i', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // i + { PS2_KEY_I, PS2CTRL_CAPS, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'I', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // I + { PS2_KEY_J, PS2CTRL_SHIFT | PS2CTRL_CAPS, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'j', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // j + { PS2_KEY_J, PS2CTRL_CAPS, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'J', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // J + { PS2_KEY_K, PS2CTRL_SHIFT | PS2CTRL_CAPS, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'k', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // k + { PS2_KEY_K, PS2CTRL_CAPS, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'K', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // K + { PS2_KEY_L, PS2CTRL_SHIFT | PS2CTRL_CAPS, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'l', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // l + { PS2_KEY_L, PS2CTRL_CAPS, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'L', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // L + { PS2_KEY_M, PS2CTRL_SHIFT | PS2CTRL_CAPS, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'm', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // m + { PS2_KEY_M, PS2CTRL_CAPS, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'M', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // M + { PS2_KEY_N, PS2CTRL_SHIFT | PS2CTRL_CAPS, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'n', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // n + { PS2_KEY_N, PS2CTRL_CAPS, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'N', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // N + { PS2_KEY_O, PS2CTRL_SHIFT | PS2CTRL_CAPS, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'o', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // o + { PS2_KEY_O, PS2CTRL_CAPS, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'O', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // O + { PS2_KEY_P, PS2CTRL_SHIFT | PS2CTRL_CAPS, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'p', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // p + { PS2_KEY_P, PS2CTRL_CAPS, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'P', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // P + { PS2_KEY_Q, PS2CTRL_SHIFT | PS2CTRL_CAPS, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'q', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // q + { PS2_KEY_Q, PS2CTRL_CAPS, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'Q', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // Q + { PS2_KEY_R, PS2CTRL_SHIFT | PS2CTRL_CAPS, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'r', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // r + { PS2_KEY_R, PS2CTRL_CAPS, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'R', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // R + { PS2_KEY_S, PS2CTRL_SHIFT | PS2CTRL_CAPS, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 's', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // s + { PS2_KEY_S, PS2CTRL_CAPS, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'S', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // S + { PS2_KEY_T, PS2CTRL_SHIFT | PS2CTRL_CAPS, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 't', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // t + { PS2_KEY_T, PS2CTRL_CAPS, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'T', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // T + { PS2_KEY_U, PS2CTRL_SHIFT | PS2CTRL_CAPS, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'u', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // u + { PS2_KEY_U, PS2CTRL_CAPS, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'U', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // U + { PS2_KEY_V, PS2CTRL_SHIFT | PS2CTRL_CAPS, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'v', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // v + { PS2_KEY_V, PS2CTRL_CAPS, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'V', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // V + { PS2_KEY_W, PS2CTRL_SHIFT | PS2CTRL_CAPS, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'w', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // w + { PS2_KEY_W, PS2CTRL_CAPS, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'W', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // W + { PS2_KEY_X, PS2CTRL_SHIFT | PS2CTRL_CAPS, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'x', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // x + { PS2_KEY_X, PS2CTRL_CAPS, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'X', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // X + { PS2_KEY_Y, PS2CTRL_SHIFT | PS2CTRL_CAPS, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'y', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // y + { PS2_KEY_Y, PS2CTRL_CAPS, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'Y', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // Y + { PS2_KEY_Z, PS2CTRL_SHIFT | PS2CTRL_CAPS, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'z', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // z + { PS2_KEY_Z, PS2CTRL_CAPS, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 'Z', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // Z + // Mode B Mappings. + { PS2_KEY_Q, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_B, 0b10000000, 0b00000000, 0b00000000, }, // MODE B - Q + { PS2_KEY_W, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_B, 0b01000000, 0b00000000, 0b00000000, }, // MODE B - W + { PS2_KEY_E, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_B, 0b00100000, 0b00000000, 0b00000000, }, // MODE B - E + { PS2_KEY_A, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_B, 0b00010000, 0b00000000, 0b00000000, }, // MODE B - A + { PS2_KEY_D, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_B, 0b00001000, 0b00000000, 0b00000000, }, // MODE B - D + { PS2_KEY_Z, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_B, 0b00000100, 0b00000000, 0b00000000, }, // MODE B - Z + { PS2_KEY_X, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_B, 0b00000010, 0b00000000, 0b00000000, }, // MODE B - X + { PS2_KEY_C, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_B, 0b00000001, 0b00000000, 0b00000000, }, // MODE B - C + { PS2_KEY_I, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_B, 0b00000000, 0b00000000, 0b01000000, }, // MODE B - I - this is not 100%, the specs arent clear. + { PS2_KEY_1, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_B, 0b00000000, 0b00100000, 0b00000000, }, // MODE B - 1 + { PS2_KEY_2, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_B, 0b00000000, 0b00001000, 0b00000000, }, // MODE B - 2 + { PS2_KEY_3, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_B, 0b00000000, 0b00000001, 0b00000000, }, // MODE B - 3 + { PS2_KEY_4, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_B, 0b00000000, 0b01000000, 0b00000000, }, // MODE B - 4 + { PS2_KEY_6, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_B, 0b00000000, 0b00000010, 0b00000000, }, // MODE B - 6 + { PS2_KEY_7, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_B, 0b00000000, 0b10000000, 0b00000000, }, // MODE B - 7 + { PS2_KEY_8, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_B, 0b00000000, 0b00010000, 0b00000000, }, // MODE B - 8 + { PS2_KEY_9, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_B, 0b00000000, 0b00000100, 0b00000000, }, // MODE B - 9 + { PS2_KEY_ESC, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_B, 0b00000000, 0b00000000, 0b10000000, }, // MODE B - ESC + { PS2_KEY_MINUS, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_B, 0b00000000, 0b00000000, 0b00100000, }, // MODE B - MINUS + { PS2_KEY_EQUAL, PS2CTRL_SHIFT, KEYMAP_STANDARD, X1_ALL, X1_MODE_B, 0b00000000, 0b00000000, 0b00010000, }, // MODE B - PLUS + { PS2_KEY_8, PS2CTRL_SHIFT, KEYMAP_STANDARD, X1_ALL, X1_MODE_B, 0b00000000, 0b00000000, 0b00001000, }, // MODE B - TIMES + { PS2_KEY_TAB, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_B, 0b00000000, 0b00000000, 0b00000100, }, // MODE B - TAB + { PS2_KEY_SPACE, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_B, 0b00000000, 0b00000000, 0b00000010, }, // MODE B - SPACE + { PS2_KEY_ENTER, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_B, 0b00000000, 0b00000000, 0b00000001, }, // MODE B - RET + { PS2_KEY_KP1, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_B, 0b00000000, 0b00100000, 0b00000000, }, // MODE B - KeyPad 1 + { PS2_KEY_KP2, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_B, 0b00000000, 0b00001000, 0b00000000, }, // MODE B - KeyPad 2 + { PS2_KEY_KP3, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_B, 0b00000000, 0b00000001, 0b00000000, }, // MODE B - KeyPad 3 + { PS2_KEY_KP4, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_B, 0b00000000, 0b01000000, 0b00000000, }, // MODE B - KeyPad 4 + { PS2_KEY_KP6, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_B, 0b00000000, 0b00000010, 0b00000000, }, // MODE B - KeyPad 6 + { PS2_KEY_KP7, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_B, 0b00000000, 0b10000000, 0b00000000, }, // MODE B - KeyPad 7 + { PS2_KEY_KP8, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_B, 0b00000000, 0b00010000, 0b00000000, }, // MODE B - KeyPad 8 + { PS2_KEY_KP9, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_B, 0b00000000, 0b00000100, 0b00000000, }, // MODE B - KeyPad 9 + { PS2_KEY_KP_MINUS, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_B, 0b00000000, 0b00000000, 0b00100000, }, // MODE B - KeyPad MINUS + { PS2_KEY_KP_PLUS, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_B, 0b00000000, 0b00000000, 0b00010000, }, // MODE B - KeyPad PLUS + { PS2_KEY_KP_TIMES, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_B, 0b00000000, 0b00000000, 0b00001000, }, // MODE B - KeyPad TIMES + + // ModeB Byte1 ModeB Byte2 ModeB Byte3 + //PS2 Code PS2 Ctrl (Flags to Match) Machine X1 Keyb Mode X1 Data X1 Ctrl (Flags to Set). + { PS2_KEY_SPACE, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, ' ', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // Space + { PS2_KEY_COMMA, PS2CTRL_SHIFT, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '<', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // Less Than < + { PS2_KEY_COMMA, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, ',', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // Comma , + { PS2_KEY_SEMI, PS2CTRL_SHIFT, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, ':', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // Colon : + { PS2_KEY_SEMI, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, ';', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // Semi-Colon ; + { PS2_KEY_DOT, PS2CTRL_SHIFT, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '>', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // Greater Than > + { PS2_KEY_DOT, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '.', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // Full stop . + { PS2_KEY_DIV, PS2CTRL_SHIFT, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '?', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // Question ? + { PS2_KEY_DIV, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '/', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // Divide / + { PS2_KEY_MINUS, PS2CTRL_SHIFT, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '_', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // Underscore + { PS2_KEY_MINUS, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '-', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, + { PS2_KEY_APOS, PS2CTRL_SHIFT, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '@', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // At @ + { PS2_KEY_APOS, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '\'', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // Single quote ' + { PS2_KEY_OPEN_SQ, PS2CTRL_SHIFT, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '{', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // Open Left Brace { + { PS2_KEY_OPEN_SQ, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '[', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // Open Left Square Bracket [ + { PS2_KEY_EQUAL, PS2CTRL_SHIFT, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '+', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // Plus + + { PS2_KEY_EQUAL, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '=', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // Equal = + { PS2_KEY_CAPS, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, ' ', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // LOCK + { PS2_KEY_ENTER, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x0D, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // ENTER/RETURN + { PS2_KEY_CLOSE_SQ, PS2CTRL_SHIFT, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '}', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // Close Right Brace } + { PS2_KEY_CLOSE_SQ, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, ']', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // Close Right Square Bracket ] + { PS2_KEY_BACK, PS2CTRL_SHIFT, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '|', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // + { PS2_KEY_BACK, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '\\', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // Back slash maps to Yen + { PS2_KEY_BTICK, PS2CTRL_SHIFT, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '`', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // Pipe + { PS2_KEY_BTICK, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '|', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // Back tick ` + { PS2_KEY_HASH, PS2CTRL_SHIFT, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '~', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // Tilde has no mapping. + { PS2_KEY_HASH, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '#', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // Hash + { PS2_KEY_BS, PS2CTRL_FUNC, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x08, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // Backspace + { PS2_KEY_ESC, PS2CTRL_FUNC, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x1B, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // ESCape + { PS2_KEY_SCROLL, PS2CTRL_FUNC, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, ' ', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // Not assigned. + { PS2_KEY_INSERT, PS2CTRL_FUNC, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, X1KEY_INS, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // INSERT + { PS2_KEY_HOME, PS2CTRL_FUNC | PS2CTRL_SHIFT, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, X1KEY_CLR, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // CLR + { PS2_KEY_HOME, PS2CTRL_FUNC, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, X1KEY_HOME, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // HOME + { PS2_KEY_DELETE, PS2CTRL_FUNC, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, X1KEY_DEL, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // DELETE + { PS2_KEY_END, PS2CTRL_FUNC, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x11, 0x00, 0xFF & ~(X1_CTRL_PRESS | X1_CTRL_TENKEY), }, // END + { PS2_KEY_PGUP, PS2CTRL_FUNC, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x0E, 0x00, 0xFF & ~(X1_CTRL_PRESS | X1_CTRL_TENKEY), }, // Roll Up. + { PS2_KEY_PGDN, PS2CTRL_FUNC, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x0F, 0x00, 0xFF & ~(X1_CTRL_PRESS | X1_CTRL_TENKEY), }, // Roll Down + { PS2_KEY_UP_ARROW, PS2CTRL_FUNC, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, X1KEY_UP, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // Up Arrow + { PS2_KEY_L_ARROW, PS2CTRL_FUNC, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, X1KEY_LEFT, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // Left Arrow + { PS2_KEY_DN_ARROW, PS2CTRL_FUNC, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, X1KEY_DOWN, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // Down Arrow + { PS2_KEY_R_ARROW, PS2CTRL_FUNC, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, X1KEY_RIGHT, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // Right Arrow + { PS2_KEY_NUM, PS2CTRL_FUNC, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x00, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // Not assigned. + // GRPH (Alt Gr) + // ModeB Byte1 ModeB Byte2 ModeB Byte3 + //PS2 Code PS2 Ctrl (Flags to Match) Machine X1 Keyb Mode X1 Data X1 Ctrl (Flags to Set). + { PS2_KEY_0, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xFA, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // GRPH+0 + { PS2_KEY_1, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xF1, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // GRPH+1 + { PS2_KEY_2, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xF2, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // GRPH+2 + { PS2_KEY_3, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xF3, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // GRPH+3 + { PS2_KEY_4, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xF4, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // GRPH+4 + { PS2_KEY_5, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xF5, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // GRPH+5 + { PS2_KEY_6, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xF6, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // GRPH+6 + { PS2_KEY_7, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xF7, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // GRPH+7 + { PS2_KEY_8, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xF8, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // GRPH+8 + { PS2_KEY_9, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xF9, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // GRPH+9 + { PS2_KEY_A, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x7F, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // GRPH+A + { PS2_KEY_B, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x84, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // GRPH+B + { PS2_KEY_C, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x82, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // GRPH+C + { PS2_KEY_D, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xEA, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // GRPH+D + { PS2_KEY_E, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xE2, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // GRPH+E + { PS2_KEY_F, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xEB, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // GRPH+F + { PS2_KEY_G, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xEC, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // GRPH+G + { PS2_KEY_H, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xED, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // GRPH+H + { PS2_KEY_I, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xE7, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // GRPH+I + { PS2_KEY_J, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xEE, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // GRPH+J + { PS2_KEY_K, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xEF, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // GRPH+K + { PS2_KEY_L, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x8E, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // GRPH+L + { PS2_KEY_M, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x86, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // GRPH+M + { PS2_KEY_N, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x85, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // GRPH+N + { PS2_KEY_O, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xF0, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // GRPH+O + { PS2_KEY_P, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x8D, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // GRPH+P + { PS2_KEY_Q, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xE0, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // GRPH+Q + { PS2_KEY_R, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xE3, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // GRPH+R + { PS2_KEY_S, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xE9, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // GRPH+S + { PS2_KEY_T, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xE4, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // GRPH+T + { PS2_KEY_U, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xE6, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // GRPH+U + { PS2_KEY_V, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x83, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // GRPH+V + { PS2_KEY_W, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xE1, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // GRPH+W + { PS2_KEY_X, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x81, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // GRPH+X + { PS2_KEY_Y, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xE5, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // GRPH+Y + { PS2_KEY_Z, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x80, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // GRPH+Z + { PS2_KEY_COMMA, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x87, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // GRPH+, + { PS2_KEY_SEMI, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x89, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // GRPH+; + { PS2_KEY_DOT, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x88, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // GRPH+. + { PS2_KEY_DIV, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xFE, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // GRPH+/ + { PS2_KEY_MINUS, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x8C, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // GRPH+- + { PS2_KEY_APOS, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x8A, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // GRPH+' + { PS2_KEY_OPEN_SQ, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xFC, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // GRPH+[ + { PS2_KEY_CLOSE_SQ, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xE8, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // GRPH+] + { PS2_KEY_BACK, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x90, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // GRPH+Backslash + { PS2_KEY_KP0, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x8F, 0x00, 0xFF & ~(X1_CTRL_TENKEY | X1_CTRL_PRESS), }, // GRPH+Keypad 0 + { PS2_KEY_KP1, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x99, 0x00, 0xFF & ~(X1_CTRL_TENKEY | X1_CTRL_PRESS), }, // GRPH+Keypad 1 + { PS2_KEY_KP2, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x92, 0x00, 0xFF & ~(X1_CTRL_TENKEY | X1_CTRL_PRESS), }, // GRPH+Keypad 2 + { PS2_KEY_KP3, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x98, 0x00, 0xFF & ~(X1_CTRL_TENKEY | X1_CTRL_PRESS), }, // GRPH+Keypad 3 + { PS2_KEY_KP4, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x95, 0x00, 0xFF & ~(X1_CTRL_TENKEY | X1_CTRL_PRESS), }, // GRPH+Keypad 4 + { PS2_KEY_KP5, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x96, 0x00, 0xFF & ~(X1_CTRL_TENKEY | X1_CTRL_PRESS), }, // GRPH+Keypad 5 + { PS2_KEY_KP6, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x94, 0x00, 0xFF & ~(X1_CTRL_TENKEY | X1_CTRL_PRESS), }, // GRPH+Keypad 6 + { PS2_KEY_KP7, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x9A, 0x00, 0xFF & ~(X1_CTRL_TENKEY | X1_CTRL_PRESS), }, // GRPH+Keypad 7 + { PS2_KEY_KP8, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x93, 0x00, 0xFF & ~(X1_CTRL_TENKEY | X1_CTRL_PRESS), }, // GRPH+Keypad 8 + { PS2_KEY_KP9, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x97, 0x00, 0xFF & ~(X1_CTRL_TENKEY | X1_CTRL_PRESS), }, // GRPH+Keypad 9 + { PS2_KEY_KP_DOT, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x91, 0x00, 0xFF & ~(X1_CTRL_TENKEY | X1_CTRL_PRESS), }, // GRPH+Keypad Full stop . + { PS2_KEY_KP_PLUS, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x9D, 0x00, 0xFF & ~(X1_CTRL_TENKEY | X1_CTRL_PRESS), }, // GRPH+Keypad Plus + + { PS2_KEY_KP_MINUS, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x9C, 0x00, 0xFF & ~(X1_CTRL_TENKEY | X1_CTRL_PRESS), }, // GRPH+Keypad Minus - + { PS2_KEY_KP_TIMES, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x9B, 0x00, 0xFF & ~(X1_CTRL_TENKEY | X1_CTRL_PRESS), }, // GRPH+Keypad Times * + { PS2_KEY_KP_DIV, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x9E, 0x00, 0xFF & ~(X1_CTRL_TENKEY | X1_CTRL_PRESS), }, // GRPH+Keypad Divide / + { PS2_KEY_KP_ENTER, PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x90, 0x00, 0xFF & ~(X1_CTRL_TENKEY | X1_CTRL_PRESS), }, // GRPH+Keypad Enter / + // KANA (Alt) + // ModeB Byte1 ModeB Byte2 ModeB Byte3 + //PS2 Code PS2 Ctrl (Flags to Match) Machine X1 Keyb Mode X1 Data X1 Ctrl (Flags to Set). + { PS2_KEY_0, PS2CTRL_KANA | PS2CTRL_SHIFT, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xA6, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+SHIFT+0 + { PS2_KEY_0, PS2CTRL_KANA, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xDC, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+0 + { PS2_KEY_1, PS2CTRL_KANA, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xC7, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+1 + { PS2_KEY_2, PS2CTRL_KANA, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xCC, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+2 + { PS2_KEY_3, PS2CTRL_KANA | PS2CTRL_SHIFT, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xA7, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+SHIFT+3 + { PS2_KEY_3, PS2CTRL_KANA, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xB1, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+3 + { PS2_KEY_4, PS2CTRL_KANA | PS2CTRL_SHIFT, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xA9, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+SHIFT+4 + { PS2_KEY_4, PS2CTRL_KANA, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xB3, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+4 + { PS2_KEY_5, PS2CTRL_KANA | PS2CTRL_SHIFT, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xAA, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+SHIFT+5 + { PS2_KEY_5, PS2CTRL_KANA, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xB4, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+5 + { PS2_KEY_6, PS2CTRL_KANA | PS2CTRL_SHIFT, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xAB, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+SHIFT+6 + { PS2_KEY_6, PS2CTRL_KANA, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xB5, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+6 + { PS2_KEY_7, PS2CTRL_KANA | PS2CTRL_SHIFT, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xAC, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+SHIFT+7 + { PS2_KEY_7, PS2CTRL_KANA, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xD4, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+7 + { PS2_KEY_8, PS2CTRL_KANA | PS2CTRL_SHIFT, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xAD, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+SHIFT+8 + { PS2_KEY_8, PS2CTRL_KANA, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xD5, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+8 + { PS2_KEY_9, PS2CTRL_KANA | PS2CTRL_SHIFT, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xAE, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+SHIFT+9 + { PS2_KEY_9, PS2CTRL_KANA, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xD6, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+9 + { PS2_KEY_A, PS2CTRL_KANA, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xC1, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+A + { PS2_KEY_B, PS2CTRL_KANA, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xBA, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+B + { PS2_KEY_C, PS2CTRL_KANA, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xBF, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+C + { PS2_KEY_D, PS2CTRL_KANA, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xBC, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+D + { PS2_KEY_E, PS2CTRL_KANA | PS2CTRL_SHIFT, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xA8, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+SHIFT+E + { PS2_KEY_E, PS2CTRL_KANA, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xB2, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+E + { PS2_KEY_F, PS2CTRL_KANA, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xCA, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+F + { PS2_KEY_G, PS2CTRL_KANA, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xB7, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+G + { PS2_KEY_H, PS2CTRL_KANA, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xB8, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+H + { PS2_KEY_I, PS2CTRL_KANA, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xC6, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+I + { PS2_KEY_J, PS2CTRL_KANA, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xCF, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+J + { PS2_KEY_K, PS2CTRL_KANA, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xC9, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+K + { PS2_KEY_L, PS2CTRL_KANA, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xD8, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+L + { PS2_KEY_M, PS2CTRL_KANA, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xD3, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+M + { PS2_KEY_N, PS2CTRL_KANA, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xD0, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+N + { PS2_KEY_O, PS2CTRL_KANA, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xD7, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+O + { PS2_KEY_P, PS2CTRL_KANA, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xBE, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+P + { PS2_KEY_Q, PS2CTRL_KANA, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xC0, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+Q + { PS2_KEY_R, PS2CTRL_KANA, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xBD, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+R + { PS2_KEY_S, PS2CTRL_KANA, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xC4, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+S + { PS2_KEY_T, PS2CTRL_KANA, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xB6, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+T + { PS2_KEY_U, PS2CTRL_KANA, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xC5, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+U + { PS2_KEY_V, PS2CTRL_KANA, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xCB, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+V + { PS2_KEY_W, PS2CTRL_KANA, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xC3, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+W + { PS2_KEY_X, PS2CTRL_KANA, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xBB, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+X + { PS2_KEY_Y, PS2CTRL_KANA, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xDD, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+Y + { PS2_KEY_Z, PS2CTRL_KANA | PS2CTRL_SHIFT, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xAF, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+SHIFT+Z + { PS2_KEY_Z, PS2CTRL_KANA, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xC2, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+Z + { PS2_KEY_COMMA, PS2CTRL_KANA | PS2CTRL_SHIFT, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xA4, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+SHIFT+, + { PS2_KEY_COMMA, PS2CTRL_KANA, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xC8, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+, + { PS2_KEY_SEMI, PS2CTRL_KANA, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xDA, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+; + { PS2_KEY_DOT, PS2CTRL_KANA | PS2CTRL_SHIFT, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xA1, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+SHIFT+. + { PS2_KEY_DOT, PS2CTRL_KANA, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xD9, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+. + { PS2_KEY_DIV, PS2CTRL_KANA | PS2CTRL_SHIFT, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xA5, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+SHIFT+/ + { PS2_KEY_DIV, PS2CTRL_KANA, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xD2, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+/ + { PS2_KEY_MINUS, PS2CTRL_KANA, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xCE, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+- + { PS2_KEY_APOS, PS2CTRL_KANA, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xDE, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+' + { PS2_KEY_OPEN_SQ, PS2CTRL_KANA | PS2CTRL_SHIFT, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xA2, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+SHIFT+[ + { PS2_KEY_OPEN_SQ, PS2CTRL_KANA, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xDF, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+[ + { PS2_KEY_CLOSE_SQ, PS2CTRL_KANA | PS2CTRL_SHIFT, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xA3, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+SHIFT+] + { PS2_KEY_CLOSE_SQ, PS2CTRL_KANA, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xD1, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+] + { PS2_KEY_BACK, PS2CTRL_KANA, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0xDB, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+Backslash + { PS2_KEY_BS, PS2CTRL_KANA | PS2CTRL_SHIFT, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x12, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA+SHIFT+Backspace + // Keypad. + { PS2_KEY_KP0, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '0', 0x00, 0xFF & ~(X1_CTRL_TENKEY | X1_CTRL_PRESS), }, // Keypad 0 + { PS2_KEY_KP1, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '1', 0x00, 0xFF & ~(X1_CTRL_TENKEY | X1_CTRL_PRESS), }, // Keypad 1 + { PS2_KEY_KP2, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '2', 0x00, 0xFF & ~(X1_CTRL_TENKEY | X1_CTRL_PRESS), }, // Keypad 2 + { PS2_KEY_KP3, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '3', 0x00, 0xFF & ~(X1_CTRL_TENKEY | X1_CTRL_PRESS), }, // Keypad 3 + { PS2_KEY_KP4, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '4', 0x00, 0xFF & ~(X1_CTRL_TENKEY | X1_CTRL_PRESS), }, // Keypad 4 + { PS2_KEY_KP5, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '5', 0x00, 0xFF & ~(X1_CTRL_TENKEY | X1_CTRL_PRESS), }, // Keypad 5 + { PS2_KEY_KP6, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '6', 0x00, 0xFF & ~(X1_CTRL_TENKEY | X1_CTRL_PRESS), }, // Keypad 6 + { PS2_KEY_KP7, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '7', 0x00, 0xFF & ~(X1_CTRL_TENKEY | X1_CTRL_PRESS), }, // Keypad 7 + { PS2_KEY_KP8, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '8', 0x00, 0xFF & ~(X1_CTRL_TENKEY | X1_CTRL_PRESS), }, // Keypad 8 + { PS2_KEY_KP9, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '9', 0x00, 0xFF & ~(X1_CTRL_TENKEY | X1_CTRL_PRESS), }, // Keypad 9 + { PS2_KEY_KP_COMMA, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, ',', 0x00, 0xFF & ~(X1_CTRL_TENKEY | X1_CTRL_PRESS), }, // Keypad Comma , + { PS2_KEY_KP_DOT, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '.', 0x00, 0xFF & ~(X1_CTRL_TENKEY | X1_CTRL_PRESS), }, // Keypad Full stop . + { PS2_KEY_KP_PLUS, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '+', 0x00, 0xFF & ~(X1_CTRL_TENKEY | X1_CTRL_PRESS), }, // Keypad Plus + + { PS2_KEY_KP_MINUS, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '-', 0x00, 0xFF & ~(X1_CTRL_TENKEY | X1_CTRL_PRESS), }, // Keypad Minus - + { PS2_KEY_KP_TIMES, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '*', 0x00, 0xFF & ~(X1_CTRL_TENKEY | X1_CTRL_PRESS), }, // Keypad Times * + { PS2_KEY_KP_DIV, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '/', 0x00, 0xFF & ~(X1_CTRL_TENKEY | X1_CTRL_PRESS), }, // Keypad Divide / + { PS2_KEY_KP_ENTER, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x0D, 0x00, 0xFF & ~(X1_CTRL_TENKEY | X1_CTRL_PRESS), }, // Keypad Enter / + { PS2_KEY_KP_EQUAL, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, '=', 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // Keypad Equal = + // ModeB Byte1 ModeB Byte2 ModeB Byte3 + //PS2 Code PS2 Ctrl (Flags to Match) Machine X1 Keyb Mode X1 Data X1 Ctrl (Flags to Set). + // Special keys. + { PS2_KEY_PRTSCR, PS2CTRL_FUNC, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x00, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // ARGO KEY + { PS2_KEY_PAUSE, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x03, 0x00, 0xFF & ~(X1_CTRL_PRESS | X1_CTRL_TENKEY), }, // BREAK KEY + { PS2_KEY_L_GUI, PS2CTRL_FUNC | PS2CTRL_GUI, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x00, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // GRAPH KEY + //{ PS2_KEY_L_ALT, PS2CTRL_FUNC | PS2CTRL_KANA, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x00, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KJ1 Sentence + //{ PS2_KEY_R_ALT, PS2CTRL_FUNC | PS2CTRL_GRAPH, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x00, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KJ2 Transform + { PS2_KEY_R_GUI, PS2CTRL_FUNC | PS2CTRL_GUI, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x00, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // KANA KEY + { PS2_KEY_MENU, PS2CTRL_FUNC | PS2CTRL_GUI, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x00, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // Not assigned. + // Modifiers are last, only being selected if an earlier match isnt made. + { PS2_KEY_L_SHIFT, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x00, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, + { PS2_KEY_R_SHIFT, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x00, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, + { PS2_KEY_L_CTRL, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x00, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, + { PS2_KEY_R_CTRL, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x00, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, // Map to Control + { 0, PS2CTRL_NONE, KEYMAP_STANDARD, X1_ALL, X1_MODE_A, 0x00, 0x00, 0xFF & ~(X1_CTRL_PRESS), }, + }}; +}; + +#endif // X1_H diff --git a/main/include/X68K.h b/main/include/X68K.h deleted file mode 120000 index fc88748..0000000 --- a/main/include/X68K.h +++ /dev/null @@ -1 +0,0 @@ -../../../sharpkey/main/include/X68K.h \ No newline at end of file diff --git a/main/include/X68K.h b/main/include/X68K.h new file mode 100644 index 0000000..b33d402 --- /dev/null +++ b/main/include/X68K.h @@ -0,0 +1,533 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// Name: X68K.h +// Created: Mar 2022 +// Version: v1.0 +// Author(s): Philip Smart +// Description: Header for the Sharp X68000 to PS/2 interface logic class. +// 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 reflect changes realised in other modules due to addition of +// bluetooth and suspend logic due to NVS issues using both cores. +// v1.03 Jun 2022 - Further updates adding in keymaps for UK BT and Japan OADG109. +// +// 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 . +///////////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef X68K_H +#define X68K_H + +// Include the specification class. +#include "KeyInterface.h" +#include "NVS.h" +#include "LED.h" +#include "HID.h" +#include +#include + +// NB: Macros definitions put inside class for clarity, they are still global scope. + +// Encapsulate the Sharp X68K interface. +class X68K : public KeyInterface { + // Macros. + // + #define NUMELEM(a) (sizeof(a)/sizeof(a[0])) + + // Constants. + #define X68KIF_VERSION 1.03 + #define X68KIF_KEYMAP_FILE "X68K_KeyMap.BIN" + #define MAX_X68K_XMIT_KEY_BUF 16 + #define MAX_X68K_RCV_KEY_BUF 16 + + // PS2 Flag definitions. + #define PS2CTRL_NONE 0x00 // No keys active = 0 + #define PS2CTRL_SHIFT 0x01 // Shfit Key active = 1 + #define PS2CTRL_CTRL 0x02 // Ctrl Key active = 1 + #define PS2CTRL_CAPS 0x04 // CAPS active = 1 + #define PS2CTRL_R_CTRL 0x08 // ALT flag used as Right CTRL flag, active = 1 + #define PS2CTRL_ALTGR 0x10 // ALTGR active = 1 + #define PS2CTRL_GUI 0x20 // GUI Key active = 1 + #define PS2CTRL_FUNC 0x40 // Special Function Keys active = 1 + #define PS2CTRL_BREAK 0x80 // BREAK Key active = 1 + #define PS2CTRL_EXACT 0x80 // EXACT Match active = 1 + + // The initial mapping is made inside the PS2KeyAdvanced class from Scan Code Set 2 to ASCII + // for a selected keyboard. Special functions are detected and combined inside this module + // before mapping with the table below to extract the X68K key code and control data. + // ie. PS/2 Scan Code -> ASCII + Flags -> X68K Key Code + Ctrl Data + #define PS2TBL_X68K_MAXCOLS 6 + #define PS2TBL_X68K_MAXROWS 131 + + // Keyboard mapping table column names. + #define PS2TBL_PS2KEYCODE_NAME "PS/2 KeyCode" + #define PS2TBL_PS2CTRL_NAME "PS/2 Control Key" + #define PS2TBL_KEYBOARDMODEL_NAME "For Keyboard" + #define PS2TBL_MACHINE_NAME "For Host Model" + #define PS2TBL_X68KKEYCODE_NAME "X68K KeyCode" + #define PS2TBL_X68KCTRL_NAME "X68K Control Key" + + // Keyboard mapping table column types. + #define PS2TBL_PS2KEYCODE_TYPE "hex" + #define PS2TBL_PS2CTRL_TYPE "custom_cbp_ps2ctrl" + #define PS2TBL_KEYBOARDMODEL_TYPE "custom_cbp_keybmodel" + #define PS2TBL_MACHINE_TYPE "custom_cbp_machine" + #define PS2TBL_X68KKEYCODE_TYPE "hex" + #define PS2TBL_X68KCTRL_TYPE "custom_cbp_x68kctrl" + + // Keyboard mapping table select list for PS2CTRL. + #define PS2TBL_PS2CTRL_SEL_NONE "NONE" + #define PS2TBL_PS2CTRL_SEL_SHIFT "SHIFT" + #define PS2TBL_PS2CTRL_SEL_CTRL "CTRL" + #define PS2TBL_PS2CTRL_SEL_CAPS "CAPS" + #define PS2TBL_PS2CTRL_SEL_R_CTRL "RCTRL" + #define PS2TBL_PS2CTRL_SEL_ALTGR "ALTGR" + #define PS2TBL_PS2CTRL_SEL_GUI "GUI" + #define PS2TBL_PS2CTRL_SEL_FUNC "FUNC" + #define PS2TBL_PS2CTRL_SEL_EXACT "EXACT" + + // Keyboard mapping table select list for target machine. + #define X68K_SEL_ALL "ALL" + #define X68K_SEL_ORIG "ORIGINAL" + #define X68K_SEL_ACE "ACE" + #define X68K_SEL_EXPERT "EXPERT" + #define X68K_SEL_PRO "PRO" + #define X68K_SEL_SUPER "SUPER" + #define X68K_SEL_XVI "XVI" + #define X68K_SEL_COMPACT "COMPACT" + #define X68K_SEL_X68030 "68030" + + // Keyboard mapping table select list for Model of keyboard. + #define KEYMAP_SEL_STANDARD "ALL" + #define KEYMAP_SEL_UK_WYSE_KB3926 "UK_WYSE_KB3926" + #define KEYMAP_SEL_JAPAN_OADG109 "JAPAN_OADG109" + #define KEYMAP_SEL_JAPAN_SANWA_SKBL1 "JAPAN_SANWA_SKBL1" + #define KEYMAP_SEL_NOT_ASSIGNED_4 "KEYBOARD_4" + #define KEYMAP_SEL_NOT_ASSIGNED_5 "KEYBOARD_5" + #define KEYMAP_SEL_NOT_ASSIGNED_6 "KEYBOARD_6" + #define KEYMAP_SEL_UK_PERIBOARD_810 "UK_PERIBOARD_810" + #define KEYMAP_SEL_UK_OMOTON_K8508 "UK_OMOTON_K8508" + + // Keyboard mapping table select list for X68K Control codes. + #define X68K_CTRL_SEL_NONE "NONE" + #define X68K_CTRL_SEL_SHIFT "SHIFT" + #define X68K_CTRL_SEL_RELEASESHIFT "RELEASESHIFT" + #define X68K_CTRL_SEL_R_CTRL "RCTRL" + + // X68K Key control bit mask. + #define X68K_CTRL_SHIFT ((unsigned char) (1 << 7)) + #define X68K_CTRL_RELEASESHIFT ((unsigned char) (1 << 6)) + #define X68K_CTRL_R_CTRL ((unsigned char) (1 << 0)) + #define X68K_CTRL_NONE 0x00 + + // The Sharp X68000 Series was released over a number of years with several iterations containing changes/updates. Generally Sharp kept the X68000 compatible through the range but just in case + // differences are found, it is possible to assign a key mapping to a specific machine type(s) or all of the series by adding the flags below into the mapping table. + #define X68K_ALL 0xFF + #define X68K_ORIG 0x01 + #define X68K_ACE 0x02 + #define X68K_EXPERT 0x04 + #define X68K_PRO 0x08 + #define X68K_SUPER 0x10 + #define X68K_XVI 0x20 + #define X68K_COMPACT 0x40 + #define X68K_X68030 0x80 + + // Keyboard models. The base on which this interface was created was a Wyse KB3926 PS/2 Keyboard and this is deemed STANDARD. Other models need to insert difference maps + // prior to the STANDARD entry along with the keyboard model so that it is processed first thus allowing differing keyboards with different maps. + #define KEYMAP_STANDARD 0xFF + #define KEYMAP_UK_WYSE_KB3926 0x01 + #define KEYMAP_JAPAN_OADG109 0x02 + #define KEYMAP_JAPAN_SANWA_SKBL1 0x04 + #define KEYMAP_NOT_ASSIGNED_4 0x08 + #define KEYMAP_NOT_ASSIGNED_5 0x10 + #define KEYMAP_NOT_ASSIGNED_6 0x20 + #define KEYMAP_UK_PERIBOARD_810 0x40 + #define KEYMAP_UK_OMOTON_K8508 0x80 + + // X68000 Scan codes - PS2 codes along with function keys (SHIFT, CTRL etc) are mapped to the X68000 scan codes below. + #define X68K_KEY_NULL 0x00 + #define X68K_KEY_ESC 0x01 + #define X68K_KEY_0 0x0B + #define X68K_KEY_1 0x02 + #define X68K_KEY_2 0x03 + #define X68K_KEY_3 0x04 + #define X68K_KEY_4 0x05 + #define X68K_KEY_5 0x06 + #define X68K_KEY_6 0x07 + #define X68K_KEY_7 0x08 + #define X68K_KEY_8 0x09 + #define X68K_KEY_9 0x0A + #define X68K_KEY_A 0x1E + #define X68K_KEY_B 0x2E + #define X68K_KEY_C 0x2C + #define X68K_KEY_D 0x20 + #define X68K_KEY_E 0x13 + #define X68K_KEY_F 0x21 + #define X68K_KEY_G 0x22 + #define X68K_KEY_H 0x23 + #define X68K_KEY_I 0x18 + #define X68K_KEY_J 0x24 + #define X68K_KEY_K 0x25 + #define X68K_KEY_L 0x26 + #define X68K_KEY_M 0x30 + #define X68K_KEY_N 0x2F + #define X68K_KEY_O 0x19 + #define X68K_KEY_P 0x1A + #define X68K_KEY_Q 0x11 + #define X68K_KEY_R 0x14 + #define X68K_KEY_S 0x1F + #define X68K_KEY_T 0x15 + #define X68K_KEY_U 0x17 + #define X68K_KEY_V 0x2D + #define X68K_KEY_W 0x12 + #define X68K_KEY_X 0x2B + #define X68K_KEY_Y 0x16 + #define X68K_KEY_Z 0x2A + #define X68K_KEY_AT 0x1B + #define X68K_KEY_MINUS 0x0C + #define X68K_KEY_CIRCUMFLEX 0x0D + #define X68K_KEY_YEN 0x0E + #define X68K_KEY_BS 0x0F + #define X68K_KEY_TAB 0x10 + #define X68K_KEY_OPEN_SQ 0x1C + #define X68K_KEY_CLOSE_SQ 0x29 + #define X68K_KEY_RETURN 0x1D + #define X68K_KEY_SEMI 0x27 + #define X68K_KEY_COLON 0x28 + #define X68K_KEY_COMMA 0x31 + #define X68K_KEY_DOT 0x32 + #define X68K_KEY_DIV 0x33 + #define X68K_KEY_UNDERLINE 0x34 + #define X68K_KEY_SPACE 0x35 + #define X68K_KEY_HOME 0x36 + #define X68K_KEY_ROLLUP 0x38 + #define X68K_KEY_ROLLDN 0x39 + #define X68K_KEY_UNDO 0x3A + #define X68K_KEY_L_ARROW 0x3B + #define X68K_KEY_UP_ARROW 0x3C + #define X68K_KEY_R_ARROW 0x3D + #define X68K_KEY_DN_ARROW 0x3E + #define X68K_KEY_CLR 0x3F + #define X68K_KEY_KP0 0x4F + #define X68K_KEY_KP1 0x4B + #define X68K_KEY_KP2 0x4C + #define X68K_KEY_KP3 0x4D + #define X68K_KEY_KP4 0x47 + #define X68K_KEY_KP5 0x48 + #define X68K_KEY_KP6 0x49 + #define X68K_KEY_KP7 0x43 + #define X68K_KEY_KP8 0x44 + #define X68K_KEY_KP9 0x45 + #define X68K_KEY_KP_DIV 0x40 + #define X68K_KEY_KP_TIMES 0x41 + #define X68K_KEY_KP_MINUS 0x42 + #define X68K_KEY_KP_PLUS 0x46 + #define X68K_KEY_KP_EQUAL 0x4A + #define X68K_KEY_KP_ENTER 0x4E + #define X68K_KEY_KP_COMMA 0x50 + #define X68K_KEY_KP_DOT 0x51 + #define X68K_KEY_SYMBOL 0x52 + #define X68K_KEY_HELP 0x54 + #define X68K_KEY_CAPS 0x5D + #define X68K_KEY_INS 0x5E + #define X68K_KEY_DEL 0x37 + #define X68K_KEY_BREAK 0x61 + #define X68K_KEY_COPY 0x62 + #define X68K_KEY_SHIFT 0x70 + #define X68K_KEY_CTRL 0x71 + #define X68K_KEY_XF1 0x55 + #define X68K_KEY_XF2 0x56 + #define X68K_KEY_XF3 0x57 + #define X68K_KEY_XF4 0x58 + #define X68K_KEY_XF5 0x59 + #define X68K_KEY_REGISTRATION 0x53 + #define X68K_KEY_KATAKANA 0x5A + #define X68K_KEY_ROMAJI 0x5B + #define X68K_KEY_TRANSPOSE 0x5C + #define X68K_KEY_HIRAGANA 0x5F + #define X68K_KEY_FULLWIDTH 0x60 + #define X68K_KEY_F1 0x63 + #define X68K_KEY_F2 0x64 + #define X68K_KEY_F3 0x65 + #define X68K_KEY_F4 0x66 + #define X68K_KEY_F5 0x67 + #define X68K_KEY_F6 0x68 + #define X68K_KEY_F7 0x69 + #define X68K_KEY_F8 0x6A + #define X68K_KEY_F9 0x6B + #define X68K_KEY_F10 0x6C + #define X68K_KEY_OPT_1 0x72 + #define X68K_KEY_OPT_2 0x73 + + public: + // Prototypes. + X68K(void); + X68K(uint32_t ifMode, NVS *hdlNVS, LED *hdlLED, HID *hdlHID, const char *fsPath); + X68K(NVS *hdlNVS, HID *hdlHID, const char *fsPath); + ~X68K(void); + bool createKeyMapFile(std::fstream &outFile); + bool storeDataToKeyMapFile(std::fstream &outFile, char *data, int size); + bool storeDataToKeyMapFile(std::fstream & outFile, std::vector& dataArray); + bool closeAndCommitKeyMapFile(std::fstream &outFile, bool cleanupOnly); + std::string getKeyMapFileName(void) { return(X68KIF_KEYMAP_FILE); }; + void getKeyMapHeaders(std::vector& headerList); + void getKeyMapTypes(std::vector& typeList); + bool getKeyMapSelectList(std::vector>& selectList, std::string option); + bool getKeyMapData(std::vector& dataArray, int *row, bool start); + + // Method to return the class version number. + float version(void) + { + return(X68KIF_VERSION); + } + + protected: + + private: + // Prototypes. + IRAM_ATTR void pushKeyToQueue(uint32_t key); + IRAM_ATTR void pushHostCmdToQueue(uint8_t cmd); + IRAM_ATTR static void x68kInterface( void * pvParameters ); + IRAM_ATTR static void hidInterface( void * pvParameters ); + void selectOption(uint8_t optionCode); + uint32_t mapKey(uint16_t scanCode); + bool loadKeyMap(); + bool saveKeyMap(void); + void init(uint32_t ifMode, NVS *hdlNVS, LED *hdlLED, HID *hdlHID); + void init(NVS *hdlNVS, HID *hdlHID); + + // Structure to encapsulate a single key map from PS/2 to X68K. + typedef struct { + uint8_t ps2KeyCode; + uint8_t ps2Ctrl; + uint8_t keyboardModel; + uint8_t machine; + uint8_t x68kKey; + uint8_t x68kCtrl; + } t_keyMapEntry; + + // Structure to encapsulate the entire static keyboard mapping table. + typedef struct { + t_keyMapEntry kme[PS2TBL_X68K_MAXROWS]; + } t_keyMap; + + // Structure to maintain the X68000 interface configuration data. This data is persisted through powercycles as needed. + typedef struct { + struct { + uint8_t activeKeyboardMap; // Model of keyboard a keymap entry is applicable to. + uint8_t activeMachineModel; // Machine model a keymap entry is applicable to. + bool useOnlyPersisted; // Flag to indicate wether the inbuilt keymap array should be combined with persisted values or the inbuilt array is ignored and only persisted values used. + } params; + } t_x68kConfig; + + // Configuration data. + t_x68kConfig x68kConfig; + + // Structure to manage the control signals signifying the state of the X68K keyboard. + typedef struct { + uint8_t keyCtrl; // Keyboard state flag control. + bool optionSelect; // Flag to indicate a user requested keyboard configuration option is being selected. + int uartNum; + int uartBufferSize; + int uartQueueSize; + + std::string fsPath; // Path on the underlying filesystem where storage is mounted and accessible. + t_keyMapEntry *kme; // Pointer to an array in memory to contain PS2 to X68K mapping values. + int kmeRows; // Number of rows in the kme table. + std::string keyMapFileName; // Name of file where extension or replacement key map entries are stored. + bool persistConfig; // Flag to request saving of the config into NVS storage. + } t_x68kControl; + + // Transmit buffer queue item. + typedef struct { + uint32_t keyCode; // Key data to be sent to X68000. + } t_xmitQueueMessage; + + // Receive buffer queue item. + typedef struct { + uint8_t hostCmd; // Keyboard configuration command received from X68000. + } t_rcvQueueMessage; + + // Thread handles - one per function, ie. HID interface and host target interface. + TaskHandle_t TaskHostIF = NULL; + TaskHandle_t TaskHIDIF = NULL; + + // Control structure to control interaction and mapping of keys for the host. + t_x68kControl x68kControl; + + // Spin lock mutex to hold a coresied to an uninterruptable method. This only works on dual core ESP32's. + portMUX_TYPE x68kMutex; + + // Lookup table to match PS/2 codes to X68K Key and Control Data. + // + // Given that the X68K had many variants, with potential differences between them, the mapping table allows for ALL or variant specific entries, the first entry matching is selected. + // + // This mapping is for the UK Wyse KB-3926 PS/2 keyboard which is deemed the KEYMAP_STANDARD and all other variants need to add additional mappings below, position sensitive, ie. add non-standard entries before standard entry. + // + //const unsigned char PS2toX68K[PS2TBL_X68K_MAXROWS][PS2TBL_X68K_MAXCOLS] = + //t_keyMapEntry PS2toX68K[PS2TBL_X68K_MAXROWS] = + t_keyMap PS2toX68K = { + { + //PS2 Code PS2 Ctrl (Flags to Match) Keyboard Model Machine X68K Data X68K Ctrl (Flags to Set). + // Function keys + { PS2_KEY_F1, PS2CTRL_FUNC | PS2CTRL_CTRL | PS2CTRL_R_CTRL, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_HIRAGANA, X68K_CTRL_NONE, }, // R_CTRL + F1 = Hiragana + { PS2_KEY_F2, PS2CTRL_FUNC | PS2CTRL_CTRL | PS2CTRL_R_CTRL, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_FULLWIDTH, X68K_CTRL_NONE, }, // R_CTRL + F2 = Full Width + { PS2_KEY_F3, PS2CTRL_FUNC | PS2CTRL_CTRL | PS2CTRL_R_CTRL, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_KATAKANA, X68K_CTRL_NONE, }, // R_CTRL + F3 = Katakana + { PS2_KEY_F4, PS2CTRL_FUNC | PS2CTRL_CTRL | PS2CTRL_R_CTRL, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_ROMAJI, X68K_CTRL_NONE, }, // R_CTRL + F4 = Romaji + { PS2_KEY_F5, PS2CTRL_FUNC | PS2CTRL_CTRL | PS2CTRL_R_CTRL, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_TRANSPOSE, X68K_CTRL_NONE, }, // R_CTRL + F5 = Tranpose + { PS2_KEY_F6, PS2CTRL_FUNC | PS2CTRL_CTRL | PS2CTRL_R_CTRL, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_SYMBOL, X68K_CTRL_NONE, }, // R_CTRL + F6 = Symbol + { PS2_KEY_F7, PS2CTRL_FUNC | PS2CTRL_CTRL | PS2CTRL_R_CTRL, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_REGISTRATION, X68K_CTRL_NONE, }, // R_CTRL + F7 = Registration - maybe a poor translation, needs better one! + { PS2_KEY_F9, PS2CTRL_FUNC | PS2CTRL_CTRL | PS2CTRL_R_CTRL, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_COPY, X68K_CTRL_NONE, }, // R_CTRL + F9 = Copy + { PS2_KEY_F10, PS2CTRL_FUNC | PS2CTRL_CTRL | PS2CTRL_R_CTRL, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_HELP, X68K_CTRL_NONE, }, // R_CTRL + F10 = Help + { PS2_KEY_F1, PS2CTRL_FUNC, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_F1, X68K_CTRL_NONE, }, // F1 + { PS2_KEY_F2, PS2CTRL_FUNC, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_F2, X68K_CTRL_NONE, }, // F2 + { PS2_KEY_F3, PS2CTRL_FUNC, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_F3, X68K_CTRL_NONE, }, // F3 + { PS2_KEY_F4, PS2CTRL_FUNC, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_F4, X68K_CTRL_NONE, }, // F4 + { PS2_KEY_F5, PS2CTRL_FUNC, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_F5, X68K_CTRL_NONE, }, // F5 + { PS2_KEY_F6, PS2CTRL_FUNC, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_F6, X68K_CTRL_NONE, }, // F6 + { PS2_KEY_F7, PS2CTRL_FUNC, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_F7, X68K_CTRL_NONE, }, // F7 + { PS2_KEY_F8, PS2CTRL_FUNC, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_F8, X68K_CTRL_NONE, }, // F8 + { PS2_KEY_F9, PS2CTRL_FUNC, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_F9, X68K_CTRL_NONE, }, // F9 + { PS2_KEY_F10, PS2CTRL_FUNC, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_F10, X68K_CTRL_NONE, }, // F10 + { PS2_KEY_F11, PS2CTRL_FUNC, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_OPT_1, X68K_CTRL_NONE, }, // F11 - OPT.1 + { PS2_KEY_F12, PS2CTRL_FUNC, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_OPT_2, X68K_CTRL_NONE, }, // F12 - OPT.2 + //PS2 Code PS2 Ctrl (Flags to Match) Machine X68K Data X68K Ctrl (Flags to Set). + // ALPHA keys, case is maaped in the X68000 via the SHIFT key event or CAPS key. + { PS2_KEY_A, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_A, X68K_CTRL_NONE, }, // A + { PS2_KEY_B, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_B, X68K_CTRL_NONE, }, // B + { PS2_KEY_C, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_C, X68K_CTRL_NONE, }, // C + { PS2_KEY_D, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_D, X68K_CTRL_NONE, }, // D + { PS2_KEY_E, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_E, X68K_CTRL_NONE, }, // E + { PS2_KEY_F, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_F, X68K_CTRL_NONE, }, // F + { PS2_KEY_G, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_G, X68K_CTRL_NONE, }, // G + { PS2_KEY_H, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_H, X68K_CTRL_NONE, }, // H + { PS2_KEY_I, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_I, X68K_CTRL_NONE, }, // I + { PS2_KEY_J, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_J, X68K_CTRL_NONE, }, // J + { PS2_KEY_K, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_K, X68K_CTRL_NONE, }, // K + { PS2_KEY_L, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_L, X68K_CTRL_NONE, }, // L + { PS2_KEY_M, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_M, X68K_CTRL_NONE, }, // M + { PS2_KEY_N, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_N, X68K_CTRL_NONE, }, // N + { PS2_KEY_O, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_O, X68K_CTRL_NONE, }, // O + { PS2_KEY_P, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_P, X68K_CTRL_NONE, }, // P + { PS2_KEY_Q, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_Q, X68K_CTRL_NONE, }, // Q + { PS2_KEY_R, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_R, X68K_CTRL_NONE, }, // R + { PS2_KEY_S, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_S, X68K_CTRL_NONE, }, // S + { PS2_KEY_T, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_T, X68K_CTRL_NONE, }, // T + { PS2_KEY_U, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_U, X68K_CTRL_NONE, }, // U + { PS2_KEY_V, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_V, X68K_CTRL_NONE, }, // V + { PS2_KEY_W, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_W, X68K_CTRL_NONE, }, // W + { PS2_KEY_X, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_X, X68K_CTRL_NONE, }, // X + { PS2_KEY_Y, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_Y, X68K_CTRL_NONE, }, // Y + { PS2_KEY_Z, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_Z, X68K_CTRL_NONE, }, // Z + // Numeric keys. + { PS2_KEY_0, PS2CTRL_SHIFT, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_9, X68K_CTRL_NONE, }, // Close Bracket ) + { PS2_KEY_0, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_0, X68K_CTRL_NONE, }, // 0 + { PS2_KEY_1, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_1, X68K_CTRL_NONE, }, // 1 + { PS2_KEY_2, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_2, X68K_CTRL_NONE, }, // 2 + { PS2_KEY_3, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_3, X68K_CTRL_NONE, }, // 3 + { PS2_KEY_4, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_4, X68K_CTRL_NONE, }, // 4 + { PS2_KEY_5, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_5, X68K_CTRL_NONE, }, // 5 + { PS2_KEY_6, PS2CTRL_SHIFT, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_CIRCUMFLEX, X68K_CTRL_RELEASESHIFT, }, // Circumflex ^ + { PS2_KEY_6, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_6, X68K_CTRL_NONE, }, // 6 + { PS2_KEY_7, PS2CTRL_SHIFT, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_6, X68K_CTRL_NONE, }, // & + { PS2_KEY_7, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_7, X68K_CTRL_NONE, }, // 7 + { PS2_KEY_8, PS2CTRL_SHIFT, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_COLON, X68K_CTRL_NONE, }, // Start * + { PS2_KEY_8, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_8, X68K_CTRL_NONE, }, // 8 + { PS2_KEY_9, PS2CTRL_SHIFT, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_8, X68K_CTRL_NONE, }, // Open Bracket ( + { PS2_KEY_9, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_9, X68K_CTRL_NONE, }, // 9 + //PS2 Code PS2 Ctrl (Flags to Match) Machine X68K Data X68K Ctrl (Flags to Set). + // Punctuation keys. + { PS2_KEY_SPACE, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_SPACE, X68K_CTRL_NONE, }, // Space + { PS2_KEY_MINUS, PS2CTRL_SHIFT, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_CIRCUMFLEX, X68K_CTRL_NONE, }, // Upper Bar + { PS2_KEY_MINUS, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_MINUS, X68K_CTRL_NONE, }, // Minus - + { PS2_KEY_EQUAL, PS2CTRL_SHIFT, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_SEMI, X68K_CTRL_SHIFT, }, // Plus + + { PS2_KEY_EQUAL, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_MINUS, X68K_CTRL_SHIFT, }, // Equal = + { PS2_KEY_DOT, PS2CTRL_SHIFT, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_DOT, X68K_CTRL_NONE, }, // Greater Than > + { PS2_KEY_DOT, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_DOT, X68K_CTRL_NONE, }, // Dot + { PS2_KEY_DIV, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_DIV, X68K_CTRL_NONE, }, // Divide / + { PS2_KEY_SEMI, PS2CTRL_SHIFT, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_COLON, X68K_CTRL_RELEASESHIFT, }, // Colon : + { PS2_KEY_SEMI, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_SEMI, X68K_CTRL_NONE, }, // Semi-Colon ; + { PS2_KEY_OPEN_SQ, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_OPEN_SQ, X68K_CTRL_NONE, }, // [ + { PS2_KEY_CLOSE_SQ, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_CLOSE_SQ, X68K_CTRL_NONE, }, // ] + { PS2_KEY_APOS, PS2CTRL_SHIFT, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_AT, X68K_CTRL_RELEASESHIFT, }, // @ + { PS2_KEY_APOS, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_7, X68K_CTRL_SHIFT, }, // ' + { PS2_KEY_BACK, PS2CTRL_SHIFT, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_YEN, X68K_CTRL_NONE, }, // Back slash maps to Yen + { PS2_KEY_BACK, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_YEN, X68K_CTRL_NONE, }, // Back slash maps to Yen + { PS2_KEY_HASH, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_3, X68K_CTRL_SHIFT, }, // Hash + { PS2_KEY_COMMA, PS2CTRL_SHIFT, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_COMMA, X68K_CTRL_NONE, }, // Less Than < + { PS2_KEY_COMMA, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_COMMA, X68K_CTRL_NONE, }, // Comma , + { PS2_KEY_BTICK, PS2CTRL_FUNC | PS2CTRL_SHIFT, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_UNDERLINE, X68K_CTRL_SHIFT, }, // Underline + { PS2_KEY_BTICK, PS2CTRL_FUNC, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_AT, X68K_CTRL_SHIFT, }, // Back Tick ` + // Control keys. + { PS2_KEY_TAB, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_TAB, X68K_CTRL_NONE, }, // TAB + { PS2_KEY_BS, PS2CTRL_FUNC, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_BS, X68K_CTRL_NONE, }, // Backspace + { PS2_KEY_ESC, PS2CTRL_FUNC, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_ESC, X68K_CTRL_NONE, }, // ESCape + { PS2_KEY_INSERT, PS2CTRL_FUNC, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_INS, X68K_CTRL_NONE, }, // INSERT + { PS2_KEY_HOME, PS2CTRL_FUNC | PS2CTRL_SHIFT, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_CLR, X68K_CTRL_NONE, }, // CLR + { PS2_KEY_HOME, PS2CTRL_FUNC, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_HOME, X68K_CTRL_NONE, }, // HOME + { PS2_KEY_DELETE, PS2CTRL_FUNC, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_DEL, X68K_CTRL_NONE, }, // DELETE + { PS2_KEY_UP_ARROW, PS2CTRL_FUNC, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_UP_ARROW, X68K_CTRL_NONE, }, // Up Arrow + { PS2_KEY_L_ARROW, PS2CTRL_FUNC, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_L_ARROW, X68K_CTRL_NONE, }, // Left Arrow + { PS2_KEY_DN_ARROW, PS2CTRL_FUNC, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_DN_ARROW, X68K_CTRL_NONE, }, // Down Arrow + { PS2_KEY_R_ARROW, PS2CTRL_FUNC, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_R_ARROW, X68K_CTRL_NONE, }, // Right Arrow + { PS2_KEY_PGUP, PS2CTRL_FUNC, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_ROLLUP, X68K_CTRL_NONE, }, // Roll Up. + { PS2_KEY_PGDN, PS2CTRL_FUNC, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_ROLLDN, X68K_CTRL_NONE, }, // Roll Down + { PS2_KEY_SCROLL, PS2CTRL_FUNC, KEYMAP_STANDARD, X68K_ALL, ' ', X68K_CTRL_NONE, }, // Not assigned. + { PS2_KEY_ENTER, PS2CTRL_FUNC, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_RETURN, X68K_CTRL_NONE, }, // Not assigned. + { PS2_KEY_CAPS, PS2CTRL_CAPS, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_CAPS, X68K_CTRL_NONE, }, // CAPS + { PS2_KEY_END, PS2CTRL_FUNC, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_UNDO, X68K_CTRL_NONE, }, // UNDO + // Keypad. + { PS2_KEY_KP0, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_KP0, X68K_CTRL_NONE, }, // Keypad 0 + { PS2_KEY_KP1, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_KP1, X68K_CTRL_NONE, }, // Keypad 1 + { PS2_KEY_KP2, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_KP2, X68K_CTRL_NONE, }, // Keypad 2 + { PS2_KEY_KP3, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_KP3, X68K_CTRL_NONE, }, // Keypad 3 + { PS2_KEY_KP4, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_KP4, X68K_CTRL_NONE, }, // Keypad 4 + { PS2_KEY_KP5, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_KP5, X68K_CTRL_NONE, }, // Keypad 5 + { PS2_KEY_KP6, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_KP6, X68K_CTRL_NONE, }, // Keypad 6 + { PS2_KEY_KP7, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_KP7, X68K_CTRL_NONE, }, // Keypad 7 + { PS2_KEY_KP8, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_KP8, X68K_CTRL_NONE, }, // Keypad 8 + { PS2_KEY_KP9, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_KP9, X68K_CTRL_NONE, }, // Keypad 9 + { PS2_KEY_KP_COMMA, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_KP_COMMA, X68K_CTRL_NONE, }, // Keypad Comma , + { PS2_KEY_KP_DOT, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_KP_DOT, X68K_CTRL_NONE, }, // Keypad Full stop . + { PS2_KEY_KP_PLUS, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_KP_PLUS, X68K_CTRL_NONE, }, // Keypad Plus + + { PS2_KEY_KP_MINUS, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_KP_MINUS, X68K_CTRL_NONE, }, // Keypad Minus - + { PS2_KEY_KP_TIMES, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_KP_TIMES, X68K_CTRL_NONE, }, // Keypad Times * + { PS2_KEY_KP_DIV, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_KP_DIV, X68K_CTRL_NONE, }, // Keypad Divide / + { PS2_KEY_KP_EQUAL, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_MINUS, X68K_CTRL_SHIFT, }, // Keypad Equal = + { PS2_KEY_KP_ENTER, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_KP_ENTER, X68K_CTRL_NONE, }, // Keypad Ebter / + { PS2_KEY_KP_ENTER, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_KP_EQUAL, X68K_CTRL_NONE, }, // Keypad Ebter / + //PS2 Code PS2 Ctrl (Flags to Match) Machine X68K Data X68K Ctrl (Flags to Set). + // Special keys. + { PS2_KEY_PRTSCR, PS2CTRL_FUNC, KEYMAP_STANDARD, X68K_ALL, 0x00, X68K_CTRL_NONE, }, // + { PS2_KEY_PAUSE, PS2CTRL_FUNC | PS2CTRL_SHIFT, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_BREAK, X68K_CTRL_RELEASESHIFT, }, // BREAK KEY + { PS2_KEY_L_GUI, PS2CTRL_FUNC, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_XF1, X68K_CTRL_NONE, }, // XF1 + { PS2_KEY_L_ALT, PS2CTRL_FUNC, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_XF2, X68K_CTRL_NONE, }, // XF2 + { PS2_KEY_R_ALT, PS2CTRL_FUNC, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_XF3, X68K_CTRL_NONE, }, // XF3 + { PS2_KEY_R_GUI, PS2CTRL_FUNC, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_XF4, X68K_CTRL_NONE, }, // XF4 + { PS2_KEY_MENU, PS2CTRL_FUNC, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_XF5, X68K_CTRL_NONE, }, // XF5 + // Modifiers are last, only being selected if an earlier match isnt made. + { PS2_KEY_L_SHIFT, PS2CTRL_FUNC, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_SHIFT, X68K_CTRL_NONE, }, // + { PS2_KEY_R_SHIFT, PS2CTRL_FUNC, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_SHIFT, X68K_CTRL_NONE, }, // + { PS2_KEY_L_CTRL, PS2CTRL_FUNC, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_CTRL, X68K_CTRL_NONE, }, // Map to Control + { PS2_KEY_R_CTRL, PS2CTRL_FUNC, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_CTRL, X68K_CTRL_NONE, }, // Map to Control + { 0, PS2CTRL_NONE, KEYMAP_STANDARD, X68K_ALL, X68K_KEY_NULL, X68K_CTRL_NONE, }, // + }}; +}; + +#endif // X68K_H diff --git a/sdkconfig b/sdkconfig deleted file mode 120000 index 8484564..0000000 --- a/sdkconfig +++ /dev/null @@ -1 +0,0 @@ -../sharpkey/sdkconfig \ No newline at end of file diff --git a/sdkconfig b/sdkconfig new file mode 100644 index 0000000..8bf8ba2 --- /dev/null +++ b/sdkconfig @@ -0,0 +1,1684 @@ +# +# Automatically generated file. DO NOT EDIT. +# Espressif IoT Development Framework (ESP-IDF) Project Configuration +# +CONFIG_IDF_CMAKE=y +CONFIG_IDF_TARGET_ARCH_XTENSA=y +CONFIG_IDF_TARGET="esp32" +CONFIG_IDF_TARGET_ESP32=y +CONFIG_IDF_FIRMWARE_CHIP_ID=0x0000 + +# +# SDK tool configuration +# +CONFIG_SDK_TOOLPREFIX="xtensa-esp32-elf-" +# CONFIG_SDK_TOOLCHAIN_SUPPORTS_TIME_WIDE_64_BITS is not set +# end of SDK tool configuration + +# +# Build type +# +CONFIG_APP_BUILD_TYPE_APP_2NDBOOT=y +# CONFIG_APP_BUILD_TYPE_ELF_RAM is not set +CONFIG_APP_BUILD_GENERATE_BINARIES=y +CONFIG_APP_BUILD_BOOTLOADER=y +CONFIG_APP_BUILD_USE_FLASH_SECTIONS=y +# end of Build type + +# +# Application manager +# +CONFIG_APP_COMPILE_TIME_DATE=y +# CONFIG_APP_EXCLUDE_PROJECT_VER_VAR is not set +# CONFIG_APP_EXCLUDE_PROJECT_NAME_VAR is not set +# CONFIG_APP_PROJECT_VER_FROM_CONFIG is not set +CONFIG_APP_RETRIEVE_LEN_ELF_SHA=16 +# end of Application manager + +# +# Arduino Configuration +# +CONFIG_ENABLE_ARDUINO_DEPENDS=y +# CONFIG_AUTOSTART_ARDUINO is not set +# CONFIG_ARDUINO_RUN_CORE0 is not set +# CONFIG_ARDUINO_RUN_CORE1 is not set +CONFIG_ARDUINO_RUN_NO_AFFINITY=y +CONFIG_ARDUINO_RUNNING_CORE=-1 +CONFIG_ARDUINO_LOOP_STACK_SIZE=8192 +CONFIG_ARDUINO_EVENT_RUN_CORE0=y +# CONFIG_ARDUINO_EVENT_RUN_CORE1 is not set +# CONFIG_ARDUINO_EVENT_RUN_NO_AFFINITY is not set +CONFIG_ARDUINO_EVENT_RUNNING_CORE=0 +CONFIG_ARDUINO_UDP_RUN_CORE0=y +# CONFIG_ARDUINO_UDP_RUN_CORE1 is not set +# CONFIG_ARDUINO_UDP_RUN_NO_AFFINITY is not set +CONFIG_ARDUINO_UDP_TASK_PRIORITY=3 +CONFIG_ARDUINO_UDP_RUNNING_CORE=0 +CONFIG_ARDUINO_ISR_IRAM=y +CONFIG_DISABLE_HAL_LOCKS=y + +# +# Debug Log Configuration +# +# CONFIG_ARDUHAL_LOG_DEFAULT_LEVEL_NONE is not set +CONFIG_ARDUHAL_LOG_DEFAULT_LEVEL_ERROR=y +# CONFIG_ARDUHAL_LOG_DEFAULT_LEVEL_WARN is not set +# CONFIG_ARDUHAL_LOG_DEFAULT_LEVEL_INFO is not set +# CONFIG_ARDUHAL_LOG_DEFAULT_LEVEL_DEBUG is not set +# CONFIG_ARDUHAL_LOG_DEFAULT_LEVEL_VERBOSE is not set +CONFIG_ARDUHAL_LOG_DEFAULT_LEVEL=1 +# CONFIG_ARDUHAL_LOG_COLORS is not set +# CONFIG_ARDUHAL_ESP_LOG is not set +# end of Debug Log Configuration + +CONFIG_ARDUHAL_PARTITION_SCHEME_DEFAULT=y +# CONFIG_ARDUHAL_PARTITION_SCHEME_MINIMAL is not set +# CONFIG_ARDUHAL_PARTITION_SCHEME_NO_OTA is not set +# CONFIG_ARDUHAL_PARTITION_SCHEME_HUGE_APP is not set +# CONFIG_ARDUHAL_PARTITION_SCHEME_MIN_SPIFFS is not set +CONFIG_ARDUHAL_PARTITION_SCHEME="default" +CONFIG_ARDUINO_SELECTIVE_COMPILATION=y +CONFIG_ARDUINO_SELECTIVE_ArduinoOTA=y +# CONFIG_ARDUINO_SELECTIVE_AsyncUDP is not set +# CONFIG_ARDUINO_SELECTIVE_AzureIoT is not set +# CONFIG_ARDUINO_SELECTIVE_BLE is not set +# CONFIG_ARDUINO_SELECTIVE_BluetoothSerial is not set +# CONFIG_ARDUINO_SELECTIVE_DNSServer is not set +# CONFIG_ARDUINO_SELECTIVE_EEPROM is not set +CONFIG_ARDUINO_SELECTIVE_ESP32=y +CONFIG_ARDUINO_SELECTIVE_ESPmDNS=y +# CONFIG_ARDUINO_SELECTIVE_FFat is not set +CONFIG_ARDUINO_SELECTIVE_FS=y +CONFIG_ARDUINO_SELECTIVE_HTTPClient=y +CONFIG_ARDUINO_SELECTIVE_LITTLEFS=y +# CONFIG_ARDUINO_SELECTIVE_NetBIOS is not set +CONFIG_ARDUINO_SELECTIVE_Preferences=y +# CONFIG_ARDUINO_SELECTIVE_SD is not set +# CONFIG_ARDUINO_SELECTIVE_SD_MMC is not set +# CONFIG_ARDUINO_SELECTIVE_SimpleBLE is not set +# CONFIG_ARDUINO_SELECTIVE_SPI is not set +# CONFIG_ARDUINO_SELECTIVE_SPIFFS is not set +# CONFIG_ARDUINO_SELECTIVE_Ticker is not set +# CONFIG_ARDUINO_SELECTIVE_Update is not set +CONFIG_ARDUINO_SELECTIVE_WebServer=y +CONFIG_ARDUINO_SELECTIVE_WiFi=y +CONFIG_ARDUINO_SELECTIVE_WiFiClientSecure=y +# CONFIG_ARDUINO_SELECTIVE_WiFiProv is not set +# CONFIG_ARDUINO_SELECTIVE_Wire is not set +# end of Arduino Configuration + +# +# Bootloader config +# +CONFIG_BOOTLOADER_OFFSET_IN_FLASH=0x1000 +CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_SIZE=y +# CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_DEBUG is not set +# CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_PERF is not set +# CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_NONE is not set +# CONFIG_BOOTLOADER_LOG_LEVEL_NONE is not set +# CONFIG_BOOTLOADER_LOG_LEVEL_ERROR is not set +CONFIG_BOOTLOADER_LOG_LEVEL_WARN=y +# CONFIG_BOOTLOADER_LOG_LEVEL_INFO is not set +# CONFIG_BOOTLOADER_LOG_LEVEL_DEBUG is not set +# CONFIG_BOOTLOADER_LOG_LEVEL_VERBOSE is not set +CONFIG_BOOTLOADER_LOG_LEVEL=2 +# CONFIG_BOOTLOADER_VDDSDIO_BOOST_1_8V is not set +CONFIG_BOOTLOADER_VDDSDIO_BOOST_1_9V=y +# CONFIG_BOOTLOADER_FACTORY_RESET is not set +# CONFIG_BOOTLOADER_APP_TEST is not set +CONFIG_BOOTLOADER_WDT_ENABLE=y +# CONFIG_BOOTLOADER_WDT_DISABLE_IN_USER_CODE is not set +CONFIG_BOOTLOADER_WDT_TIME_MS=9000 +# CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE is not set +# CONFIG_BOOTLOADER_SKIP_VALIDATE_IN_DEEP_SLEEP is not set +# CONFIG_BOOTLOADER_SKIP_VALIDATE_ON_POWER_ON is not set +# CONFIG_BOOTLOADER_SKIP_VALIDATE_ALWAYS is not set +CONFIG_BOOTLOADER_RESERVE_RTC_SIZE=0 +# CONFIG_BOOTLOADER_CUSTOM_RESERVE_RTC is not set +CONFIG_BOOTLOADER_FLASH_XMC_SUPPORT=y +# end of Bootloader config + +# +# Security features +# +# CONFIG_SECURE_SIGNED_APPS_NO_SECURE_BOOT is not set +# CONFIG_SECURE_BOOT is not set +# CONFIG_SECURE_FLASH_ENC_ENABLED is not set +# end of Security features + +# +# Serial flasher config +# +CONFIG_ESPTOOLPY_BAUD_OTHER_VAL=115200 +# CONFIG_ESPTOOLPY_NO_STUB is not set +# CONFIG_ESPTOOLPY_FLASHMODE_QIO is not set +# CONFIG_ESPTOOLPY_FLASHMODE_QOUT is not set +CONFIG_ESPTOOLPY_FLASHMODE_DIO=y +# CONFIG_ESPTOOLPY_FLASHMODE_DOUT is not set +CONFIG_ESPTOOLPY_FLASH_SAMPLE_MODE_STR=y +CONFIG_ESPTOOLPY_FLASHMODE="dio" +# CONFIG_ESPTOOLPY_FLASHFREQ_80M is not set +CONFIG_ESPTOOLPY_FLASHFREQ_40M=y +# CONFIG_ESPTOOLPY_FLASHFREQ_26M is not set +# CONFIG_ESPTOOLPY_FLASHFREQ_20M is not set +CONFIG_ESPTOOLPY_FLASHFREQ="40m" +# CONFIG_ESPTOOLPY_FLASHSIZE_1MB is not set +# CONFIG_ESPTOOLPY_FLASHSIZE_2MB is not set +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y +# CONFIG_ESPTOOLPY_FLASHSIZE_8MB is not set +# CONFIG_ESPTOOLPY_FLASHSIZE_16MB is not set +CONFIG_ESPTOOLPY_FLASHSIZE="4MB" +CONFIG_ESPTOOLPY_FLASHSIZE_DETECT=y +CONFIG_ESPTOOLPY_BEFORE_RESET=y +# CONFIG_ESPTOOLPY_BEFORE_NORESET is not set +CONFIG_ESPTOOLPY_BEFORE="default_reset" +CONFIG_ESPTOOLPY_AFTER_RESET=y +# CONFIG_ESPTOOLPY_AFTER_NORESET is not set +CONFIG_ESPTOOLPY_AFTER="hard_reset" +# CONFIG_ESPTOOLPY_MONITOR_BAUD_CONSOLE is not set +# CONFIG_ESPTOOLPY_MONITOR_BAUD_9600B is not set +# CONFIG_ESPTOOLPY_MONITOR_BAUD_57600B is not set +CONFIG_ESPTOOLPY_MONITOR_BAUD_115200B=y +# CONFIG_ESPTOOLPY_MONITOR_BAUD_230400B is not set +# CONFIG_ESPTOOLPY_MONITOR_BAUD_921600B is not set +# CONFIG_ESPTOOLPY_MONITOR_BAUD_2MB is not set +# CONFIG_ESPTOOLPY_MONITOR_BAUD_OTHER is not set +CONFIG_ESPTOOLPY_MONITOR_BAUD_OTHER_VAL=115200 +CONFIG_ESPTOOLPY_MONITOR_BAUD=115200 +# end of Serial flasher config + +# +# Partition Table +# +# CONFIG_PARTITION_TABLE_SINGLE_APP is not set +# CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE is not set +# CONFIG_PARTITION_TABLE_TWO_OTA is not set +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="sharpkey_partition_table.csv" +CONFIG_PARTITION_TABLE_FILENAME="sharpkey_partition_table.csv" +CONFIG_PARTITION_TABLE_OFFSET=0x8000 +CONFIG_PARTITION_TABLE_MD5=y +# end of Partition Table + +# +# SharpKey Configuration +# +CONFIG_SHARPKEY=y +# CONFIG_MZ25KEY_MZ2500 is not set +# CONFIG_MZ25KEY_MZ2800 is not set +CONFIG_DISABLE_FEATURE_SECURITY=y +# CONFIG_ENABLE_FEATURE_SECURITY is not set + +# +# PS2 Keyboard +# +CONFIG_PS2_HW_DATAPIN=32 +CONFIG_PS2_HW_CLKPIN=33 +# end of PS2 Keyboard + +# +# Host Interface +# + +# +# 4Bit Strobe Input +# +CONFIG_HOST_KDB0=23 +CONFIG_HOST_KDB1=25 +CONFIG_HOST_KDB2=26 +CONFIG_HOST_KDB3=27 +# end of 4Bit Strobe Input + +# +# 8Bit Scan Data Output +# +CONFIG_HOST_KDO0=14 +CONFIG_HOST_KDO1=15 +CONFIG_HOST_KDO2=16 +CONFIG_HOST_KDO3=17 +CONFIG_HOST_KDO4=18 +CONFIG_HOST_KDO5=19 +CONFIG_HOST_KDO6=21 +CONFIG_HOST_KDO7=22 +# end of 8Bit Scan Data Output + +CONFIG_HOST_BITBANG_UART=y +# CONFIG_HOST_HW_UART is not set +CONFIG_HOST_RTSNI=35 +CONFIG_HOST_MPXI=12 +CONFIG_HOST_KDI4=13 +# end of Host Interface + +# +# WiFi +# +CONFIG_IF_WIFI_ENABLED=y +CONFIG_IF_WIFI_EN_KEY=34 +CONFIG_IF_WIFI_SSID="sharpkey" +CONFIG_IF_WIFI_DEFAULT_SSID_PWD="sharpkey" +CONFIG_IF_WIFI_MAX_RETRIES=20 +CONFIG_IF_WIFI_AP_CHANNEL=8 +CONFIG_IF_WIFI_SSID_HIDDEN=0 +CONFIG_IF_WIFI_MAX_CONNECTIONS=5 +# end of WiFi + +# +# Debug Options +# +# CONFIG_DEBUG_SERIAL is not set +# CONFIG_DEBUG_DISABLE_KDB is not set +# CONFIG_DEBUG_DISABLE_KDO is not set +# CONFIG_DEBUG_DISABLE_RTSNI is not set +# CONFIG_DEBUG_DISABLE_MPXI is not set +# CONFIG_DEBUG_DISABLE_KDI is not set +# end of Debug Options + +CONFIG_PWRLED=2 +# end of SharpKey Configuration + +# +# Arduino Configuration +# +# CONFIG_ARDUINO_SERIAL_EVENT_RUN_CORE0 is not set +# CONFIG_ARDUINO_SERIAL_EVENT_RUN_CORE1 is not set +CONFIG_ARDUINO_SERIAL_EVENT_RUN_NO_AFFINITY=y +CONFIG_ARDUINO_SERIAL_EVENT_TASK_RUNNING_CORE=-1 +CONFIG_ARDUINO_SERIAL_EVENT_TASK_STACK_SIZE=2048 +CONFIG_ARDUINO_SERIAL_EVENT_TASK_PRIORITY=24 + +# +# Debug Log Configuration +# +# end of Debug Log Configuration +# end of Arduino Configuration + +# +# Compiler options +# +# CONFIG_COMPILER_OPTIMIZATION_DEFAULT is not set +# CONFIG_COMPILER_OPTIMIZATION_SIZE is not set +CONFIG_COMPILER_OPTIMIZATION_PERF=y +# CONFIG_COMPILER_OPTIMIZATION_NONE is not set +CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_ENABLE=y +# CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_SILENT is not set +# CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_DISABLE is not set +CONFIG_COMPILER_OPTIMIZATION_ASSERTION_LEVEL=2 +# CONFIG_COMPILER_OPTIMIZATION_CHECKS_SILENT is not set +CONFIG_COMPILER_HIDE_PATHS_MACROS=y +# CONFIG_COMPILER_CXX_EXCEPTIONS is not set +# CONFIG_COMPILER_CXX_RTTI is not set +CONFIG_COMPILER_STACK_CHECK_MODE_NONE=y +# CONFIG_COMPILER_STACK_CHECK_MODE_NORM is not set +# CONFIG_COMPILER_STACK_CHECK_MODE_STRONG is not set +# CONFIG_COMPILER_STACK_CHECK_MODE_ALL is not set +# CONFIG_COMPILER_WARN_WRITE_STRINGS is not set +# CONFIG_COMPILER_DISABLE_GCC8_WARNINGS is not set +# CONFIG_COMPILER_DUMP_RTL_FILES is not set +# end of Compiler options + +# +# Component config +# + +# +# Application Level Tracing +# +# CONFIG_APPTRACE_DEST_JTAG is not set +CONFIG_APPTRACE_DEST_NONE=y +CONFIG_APPTRACE_LOCK_ENABLE=y +# end of Application Level Tracing + +# +# ESP-ASIO +# +# CONFIG_ASIO_SSL_SUPPORT is not set +# end of ESP-ASIO + +# +# Bluetooth +# +CONFIG_BT_ENABLED=y + +# +# Bluetooth controller +# +# CONFIG_BTDM_CTRL_MODE_BLE_ONLY is not set +# CONFIG_BTDM_CTRL_MODE_BR_EDR_ONLY is not set +CONFIG_BTDM_CTRL_MODE_BTDM=y +CONFIG_BTDM_CTRL_BLE_MAX_CONN=5 +CONFIG_BTDM_CTRL_BR_EDR_MAX_ACL_CONN=5 +CONFIG_BTDM_CTRL_BR_EDR_MAX_SYNC_CONN=0 +CONFIG_BTDM_CTRL_BR_EDR_SCO_DATA_PATH_HCI=y +# CONFIG_BTDM_CTRL_BR_EDR_SCO_DATA_PATH_PCM is not set +CONFIG_BTDM_CTRL_BR_EDR_SCO_DATA_PATH_EFF=0 +CONFIG_BTDM_CTRL_PCM_ROLE_EFF=0 +CONFIG_BTDM_CTRL_PCM_POLAR_EFF=0 +CONFIG_BTDM_CTRL_AUTO_LATENCY=y +CONFIG_BTDM_CTRL_AUTO_LATENCY_EFF=y +CONFIG_BTDM_CTRL_LEGACY_AUTH_VENDOR_EVT=y +CONFIG_BTDM_CTRL_LEGACY_AUTH_VENDOR_EVT_EFF=y +CONFIG_BTDM_CTRL_BLE_MAX_CONN_EFF=5 +CONFIG_BTDM_CTRL_BR_EDR_MAX_ACL_CONN_EFF=5 +CONFIG_BTDM_CTRL_BR_EDR_MAX_SYNC_CONN_EFF=0 +CONFIG_BTDM_CTRL_PINNED_TO_CORE_0=y +# CONFIG_BTDM_CTRL_PINNED_TO_CORE_1 is not set +CONFIG_BTDM_CTRL_PINNED_TO_CORE=0 +CONFIG_BTDM_CTRL_HCI_MODE_VHCI=y +# CONFIG_BTDM_CTRL_HCI_MODE_UART_H4 is not set + +# +# MODEM SLEEP Options +# +# CONFIG_BTDM_CTRL_MODEM_SLEEP is not set +# end of MODEM SLEEP Options + +CONFIG_BTDM_BLE_DEFAULT_SCA_250PPM=y +CONFIG_BTDM_BLE_SLEEP_CLOCK_ACCURACY_INDEX_EFF=1 +CONFIG_BTDM_BLE_SCAN_DUPL=y +CONFIG_BTDM_SCAN_DUPL_TYPE_DEVICE=y +# CONFIG_BTDM_SCAN_DUPL_TYPE_DATA is not set +# CONFIG_BTDM_SCAN_DUPL_TYPE_DATA_DEVICE is not set +CONFIG_BTDM_SCAN_DUPL_TYPE=0 +CONFIG_BTDM_SCAN_DUPL_CACHE_SIZE=10 +# CONFIG_BTDM_BLE_MESH_SCAN_DUPL_EN is not set +CONFIG_BTDM_CTRL_FULL_SCAN_SUPPORTED=y +CONFIG_BTDM_BLE_ADV_REPORT_FLOW_CTRL_SUPP=y +CONFIG_BTDM_BLE_ADV_REPORT_FLOW_CTRL_NUM=100 +CONFIG_BTDM_BLE_ADV_REPORT_DISCARD_THRSHOLD=20 +CONFIG_BTDM_RESERVE_DRAM=0xdb5c +CONFIG_BTDM_CTRL_HLI=y +# end of Bluetooth controller + +CONFIG_BT_BLUEDROID_ENABLED=y +# CONFIG_BT_NIMBLE_ENABLED is not set +# CONFIG_BT_CONTROLLER_ONLY is not set + +# +# Bluedroid Options +# +CONFIG_BT_BTC_TASK_STACK_SIZE=3072 +CONFIG_BT_BLUEDROID_PINNED_TO_CORE_0=y +# CONFIG_BT_BLUEDROID_PINNED_TO_CORE_1 is not set +CONFIG_BT_BLUEDROID_PINNED_TO_CORE=0 +CONFIG_BT_BTU_TASK_STACK_SIZE=4096 +# CONFIG_BT_BLUEDROID_MEM_DEBUG is not set +CONFIG_BT_CLASSIC_ENABLED=y +# CONFIG_BT_A2DP_ENABLE is not set +# CONFIG_BT_SPP_ENABLED is not set +# CONFIG_BT_HFP_ENABLE is not set +CONFIG_BT_HID_ENABLED=y +CONFIG_BT_HID_HOST_ENABLED=y +# CONFIG_BT_HID_DEVICE_ENABLED is not set +CONFIG_BT_SSP_ENABLED=y +CONFIG_BT_BLE_ENABLED=y +# CONFIG_BT_GATTS_ENABLE is not set +CONFIG_BT_GATTC_ENABLE=y +# CONFIG_BT_GATTC_CACHE_NVS_FLASH is not set +CONFIG_BT_GATTC_CONNECT_RETRY_COUNT=7 +CONFIG_BT_BLE_SMP_ENABLE=y +# CONFIG_BT_SMP_SLAVE_CON_PARAMS_UPD_ENABLE is not set +CONFIG_BT_STACK_NO_LOG=y +CONFIG_BT_ACL_CONNECTIONS=5 +CONFIG_BT_MULTI_CONNECTION_ENBALE=y +# CONFIG_BT_ALLOCATION_FROM_SPIRAM_FIRST is not set +CONFIG_BT_BLE_DYNAMIC_ENV_MEMORY=y +# CONFIG_BT_BLE_HOST_QUEUE_CONG_CHECK is not set +CONFIG_BT_SMP_ENABLE=y +# CONFIG_BT_BLE_ACT_SCAN_REP_ADV_SCAN is not set +CONFIG_BT_BLE_ESTAB_LINK_CONN_TOUT=60 +# CONFIG_BT_BLE_RPA_SUPPORTED is not set +# end of Bluedroid Options +# end of Bluetooth + +# CONFIG_BLE_MESH is not set + +# +# CoAP Configuration +# +CONFIG_COAP_MBEDTLS_PSK=y +# CONFIG_COAP_MBEDTLS_PKI is not set +# CONFIG_COAP_MBEDTLS_DEBUG is not set +CONFIG_COAP_LOG_DEFAULT_LEVEL=0 +# end of CoAP Configuration + +# +# Driver configurations +# + +# +# ADC configuration +# +# CONFIG_ADC_FORCE_XPD_FSM is not set +CONFIG_ADC_DISABLE_DAC=y +# end of ADC configuration + +# +# MCPWM configuration +# +# CONFIG_MCPWM_ISR_IN_IRAM is not set +# end of MCPWM configuration + +# +# SPI configuration +# +# CONFIG_SPI_MASTER_IN_IRAM is not set +CONFIG_SPI_MASTER_ISR_IN_IRAM=y +# CONFIG_SPI_SLAVE_IN_IRAM is not set +CONFIG_SPI_SLAVE_ISR_IN_IRAM=y +# end of SPI configuration + +# +# TWAI configuration +# +# CONFIG_TWAI_ISR_IN_IRAM is not set +# CONFIG_TWAI_ERRATA_FIX_BUS_OFF_REC is not set +# CONFIG_TWAI_ERRATA_FIX_TX_INTR_LOST is not set +# CONFIG_TWAI_ERRATA_FIX_RX_FRAME_INVALID is not set +# CONFIG_TWAI_ERRATA_FIX_RX_FIFO_CORRUPT is not set +# end of TWAI configuration + +# +# UART configuration +# +# CONFIG_UART_ISR_IN_IRAM is not set +# end of UART configuration + +# +# RTCIO configuration +# +# CONFIG_RTCIO_SUPPORT_RTC_GPIO_DESC is not set +# end of RTCIO configuration + +# +# GPIO Configuration +# +# CONFIG_GPIO_ESP32_SUPPORT_SWITCH_SLP_PULL is not set +# end of GPIO Configuration + +# +# GDMA Configuration +# +# CONFIG_GDMA_CTRL_FUNC_IN_IRAM is not set +# CONFIG_GDMA_ISR_IRAM_SAFE is not set +# end of GDMA Configuration +# end of Driver configurations + +# +# eFuse Bit Manager +# +CONFIG_EFUSE_CUSTOM_TABLE=y +CONFIG_EFUSE_CUSTOM_TABLE_FILENAME="main/esp_efuse_custom_table.csv" +# CONFIG_EFUSE_VIRTUAL is not set +CONFIG_EFUSE_CODE_SCHEME_COMPAT_NONE=y +# CONFIG_EFUSE_CODE_SCHEME_COMPAT_3_4 is not set +# CONFIG_EFUSE_CODE_SCHEME_COMPAT_REPEAT is not set +CONFIG_EFUSE_MAX_BLK_LEN=256 +# end of eFuse Bit Manager + +# +# ESP-TLS +# +CONFIG_ESP_TLS_USING_MBEDTLS=y +# CONFIG_ESP_TLS_USE_SECURE_ELEMENT is not set +# CONFIG_ESP_TLS_SERVER is not set +# CONFIG_ESP_TLS_CLIENT_SESSION_TICKETS is not set +# CONFIG_ESP_TLS_PSK_VERIFICATION is not set +# CONFIG_ESP_TLS_INSECURE is not set +# end of ESP-TLS + +# +# ESP32-specific +# +CONFIG_ESP32_REV_MIN_0=y +# CONFIG_ESP32_REV_MIN_1 is not set +# CONFIG_ESP32_REV_MIN_2 is not set +# CONFIG_ESP32_REV_MIN_3 is not set +CONFIG_ESP32_REV_MIN=0 +CONFIG_ESP32_DPORT_WORKAROUND=y +# CONFIG_ESP32_DEFAULT_CPU_FREQ_80 is not set +# CONFIG_ESP32_DEFAULT_CPU_FREQ_160 is not set +CONFIG_ESP32_DEFAULT_CPU_FREQ_240=y +CONFIG_ESP32_DEFAULT_CPU_FREQ_MHZ=240 +# CONFIG_ESP32_SPIRAM_SUPPORT is not set +# CONFIG_ESP32_TRAX is not set +CONFIG_ESP32_TRACEMEM_RESERVE_DRAM=0x0 +# CONFIG_ESP32_ULP_COPROC_ENABLED is not set +CONFIG_ESP32_ULP_COPROC_RESERVE_MEM=0 +CONFIG_ESP32_DEBUG_OCDAWARE=y +CONFIG_ESP32_BROWNOUT_DET=y +CONFIG_ESP32_BROWNOUT_DET_LVL_SEL_0=y +# CONFIG_ESP32_BROWNOUT_DET_LVL_SEL_1 is not set +# CONFIG_ESP32_BROWNOUT_DET_LVL_SEL_2 is not set +# CONFIG_ESP32_BROWNOUT_DET_LVL_SEL_3 is not set +# CONFIG_ESP32_BROWNOUT_DET_LVL_SEL_4 is not set +# CONFIG_ESP32_BROWNOUT_DET_LVL_SEL_5 is not set +# CONFIG_ESP32_BROWNOUT_DET_LVL_SEL_6 is not set +# CONFIG_ESP32_BROWNOUT_DET_LVL_SEL_7 is not set +CONFIG_ESP32_BROWNOUT_DET_LVL=0 +CONFIG_ESP32_TIME_SYSCALL_USE_RTC_FRC1=y +# CONFIG_ESP32_TIME_SYSCALL_USE_RTC is not set +# CONFIG_ESP32_TIME_SYSCALL_USE_FRC1 is not set +# CONFIG_ESP32_TIME_SYSCALL_USE_NONE is not set +CONFIG_ESP32_RTC_CLK_SRC_INT_RC=y +# CONFIG_ESP32_RTC_CLK_SRC_EXT_CRYS is not set +# CONFIG_ESP32_RTC_CLK_SRC_EXT_OSC is not set +# CONFIG_ESP32_RTC_CLK_SRC_INT_8MD256 is not set +CONFIG_ESP32_RTC_CLK_CAL_CYCLES=1024 +CONFIG_ESP32_DEEP_SLEEP_WAKEUP_DELAY=2000 +CONFIG_ESP32_XTAL_FREQ_40=y +# CONFIG_ESP32_XTAL_FREQ_26 is not set +# CONFIG_ESP32_XTAL_FREQ_AUTO is not set +CONFIG_ESP32_XTAL_FREQ=40 +# CONFIG_ESP32_DISABLE_BASIC_ROM_CONSOLE is not set +# CONFIG_ESP32_COMPATIBLE_PRE_V2_1_BOOTLOADERS is not set +# CONFIG_ESP32_COMPATIBLE_PRE_V3_1_BOOTLOADERS is not set +# CONFIG_ESP32_USE_FIXED_STATIC_RAM_SIZE is not set +CONFIG_ESP32_DPORT_DIS_INTERRUPT_LVL=5 +# end of ESP32-specific + +# +# ADC-Calibration +# +CONFIG_ADC_CAL_EFUSE_TP_ENABLE=y +CONFIG_ADC_CAL_EFUSE_VREF_ENABLE=y +CONFIG_ADC_CAL_LUT_ENABLE=y +# end of ADC-Calibration + +# +# Common ESP-related +# +CONFIG_ESP_ERR_TO_NAME_LOOKUP=y +# end of Common ESP-related + +# +# Ethernet +# +CONFIG_ETH_ENABLED=y +CONFIG_ETH_USE_ESP32_EMAC=y +CONFIG_ETH_PHY_INTERFACE_RMII=y +CONFIG_ETH_RMII_CLK_INPUT=y +# CONFIG_ETH_RMII_CLK_OUTPUT is not set +CONFIG_ETH_RMII_CLK_IN_GPIO=0 +CONFIG_ETH_DMA_BUFFER_SIZE=512 +CONFIG_ETH_DMA_RX_BUFFER_NUM=10 +CONFIG_ETH_DMA_TX_BUFFER_NUM=10 +# CONFIG_ETH_USE_SPI_ETHERNET is not set +# CONFIG_ETH_USE_OPENETH is not set +# end of Ethernet + +# +# Event Loop Library +# +# CONFIG_ESP_EVENT_LOOP_PROFILING is not set +CONFIG_ESP_EVENT_POST_FROM_ISR=y +CONFIG_ESP_EVENT_POST_FROM_IRAM_ISR=y +# end of Event Loop Library + +# +# GDB Stub +# +# end of GDB Stub + +# +# ESP HTTP client +# +CONFIG_ESP_HTTP_CLIENT_ENABLE_HTTPS=y +# CONFIG_ESP_HTTP_CLIENT_ENABLE_BASIC_AUTH is not set +# CONFIG_ESP_HTTP_CLIENT_ENABLE_DIGEST_AUTH is not set +# end of ESP HTTP client + +# +# HTTP Server +# +CONFIG_HTTPD_MAX_REQ_HDR_LEN=2048 +CONFIG_HTTPD_MAX_URI_LEN=512 +CONFIG_HTTPD_ERR_RESP_NO_DELAY=y +CONFIG_HTTPD_PURGE_BUF_LEN=32 +# CONFIG_HTTPD_LOG_PURGE_DATA is not set +# CONFIG_HTTPD_WS_SUPPORT is not set +# end of HTTP Server + +# +# ESP HTTPS OTA +# +# CONFIG_OTA_ALLOW_HTTP is not set +# end of ESP HTTPS OTA + +# +# ESP HTTPS server +# +# CONFIG_ESP_HTTPS_SERVER_ENABLE is not set +# end of ESP HTTPS server + +# +# Hardware Settings +# + +# +# MAC Config +# +CONFIG_ESP_MAC_ADDR_UNIVERSE_WIFI_STA=y +CONFIG_ESP_MAC_ADDR_UNIVERSE_WIFI_AP=y +CONFIG_ESP_MAC_ADDR_UNIVERSE_BT=y +CONFIG_ESP_MAC_ADDR_UNIVERSE_ETH=y +# CONFIG_ESP32_UNIVERSAL_MAC_ADDRESSES_TWO is not set +CONFIG_ESP32_UNIVERSAL_MAC_ADDRESSES_FOUR=y +CONFIG_ESP32_UNIVERSAL_MAC_ADDRESSES=4 +# end of MAC Config + +# +# Sleep Config +# +CONFIG_ESP_SLEEP_POWER_DOWN_FLASH=y +CONFIG_ESP_SLEEP_RTC_BUS_ISO_WORKAROUND=y +# CONFIG_ESP_SLEEP_GPIO_RESET_WORKAROUND is not set +# CONFIG_ESP_SLEEP_FLASH_LEAKAGE_WORKAROUND is not set +# end of Sleep Config +# end of Hardware Settings + +# +# IPC (Inter-Processor Call) +# +CONFIG_ESP_IPC_TASK_STACK_SIZE=1024 +CONFIG_ESP_IPC_USES_CALLERS_PRIORITY=y +CONFIG_ESP_IPC_ISR_ENABLE=y +# end of IPC (Inter-Processor Call) + +# +# LCD and Touch Panel +# + +# +# LCD Peripheral Configuration +# +CONFIG_LCD_PANEL_IO_FORMAT_BUF_SIZE=32 +# end of LCD Peripheral Configuration +# end of LCD and Touch Panel + +# +# ESP NETIF Adapter +# +CONFIG_ESP_NETIF_IP_LOST_TIMER_INTERVAL=120 +CONFIG_ESP_NETIF_TCPIP_LWIP=y +# CONFIG_ESP_NETIF_LOOPBACK is not set +CONFIG_ESP_NETIF_TCPIP_ADAPTER_COMPATIBLE_LAYER=y +# end of ESP NETIF Adapter + +# +# PHY +# +CONFIG_ESP_PHY_CALIBRATION_AND_DATA_STORAGE=y +# CONFIG_ESP_PHY_INIT_DATA_IN_PARTITION is not set +CONFIG_ESP_PHY_MAX_WIFI_TX_POWER=20 +CONFIG_ESP_PHY_MAX_TX_POWER=20 +# CONFIG_ESP_PHY_REDUCE_TX_POWER is not set +# end of PHY + +# +# Power Management +# +# CONFIG_PM_ENABLE is not set +# end of Power Management + +# +# ESP System Settings +# +# CONFIG_ESP_SYSTEM_PANIC_PRINT_HALT is not set +CONFIG_ESP_SYSTEM_PANIC_PRINT_REBOOT=y +# CONFIG_ESP_SYSTEM_PANIC_SILENT_REBOOT is not set +# CONFIG_ESP_SYSTEM_PANIC_GDBSTUB is not set +# CONFIG_ESP_SYSTEM_GDBSTUB_RUNTIME is not set + +# +# Memory protection +# +# end of Memory protection + +CONFIG_ESP_SYSTEM_EVENT_QUEUE_SIZE=32 +CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=6144 +CONFIG_ESP_MAIN_TASK_STACK_SIZE=6144 +CONFIG_ESP_MAIN_TASK_AFFINITY_CPU0=y +# CONFIG_ESP_MAIN_TASK_AFFINITY_CPU1 is not set +# CONFIG_ESP_MAIN_TASK_AFFINITY_NO_AFFINITY is not set +CONFIG_ESP_MAIN_TASK_AFFINITY=0x0 +CONFIG_ESP_MINIMAL_SHARED_STACK_SIZE=2048 +CONFIG_ESP_CONSOLE_UART_DEFAULT=y +# CONFIG_ESP_CONSOLE_UART_CUSTOM is not set +# CONFIG_ESP_CONSOLE_NONE is not set +CONFIG_ESP_CONSOLE_UART=y +CONFIG_ESP_CONSOLE_MULTIPLE_UART=y +CONFIG_ESP_CONSOLE_UART_NUM=0 +CONFIG_ESP_CONSOLE_UART_BAUDRATE=115200 +CONFIG_ESP_INT_WDT=y +CONFIG_ESP_INT_WDT_TIMEOUT_MS=5000 +# CONFIG_ESP_INT_WDT_CHECK_CPU1 is not set +CONFIG_ESP_TASK_WDT=y +CONFIG_ESP_TASK_WDT_PANIC=y +CONFIG_ESP_TASK_WDT_TIMEOUT_S=5 +# CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0 is not set +# CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1 is not set +# CONFIG_ESP_PANIC_HANDLER_IRAM is not set +CONFIG_ESP_SYSTEM_CHECK_INT_LEVEL_5=y +# end of ESP System Settings + +# +# High resolution timer (esp_timer) +# +# CONFIG_ESP_TIMER_PROFILING is not set +CONFIG_ESP_TIME_FUNCS_USE_RTC_TIMER=y +CONFIG_ESP_TIME_FUNCS_USE_ESP_TIMER=y +CONFIG_ESP_TIMER_TASK_STACK_SIZE=3584 +CONFIG_ESP_TIMER_INTERRUPT_LEVEL=1 +# CONFIG_ESP_TIMER_SUPPORTS_ISR_DISPATCH_METHOD is not set +# CONFIG_ESP_TIMER_IMPL_FRC2 is not set +CONFIG_ESP_TIMER_IMPL_TG0_LAC=y +# end of High resolution timer (esp_timer) + +# +# Wi-Fi +# +CONFIG_ESP32_WIFI_ENABLED=y +# CONFIG_ESP32_WIFI_SW_COEXIST_ENABLE is not set +CONFIG_ESP32_WIFI_STATIC_RX_BUFFER_NUM=10 +CONFIG_ESP32_WIFI_DYNAMIC_RX_BUFFER_NUM=32 +# CONFIG_ESP32_WIFI_STATIC_TX_BUFFER is not set +CONFIG_ESP32_WIFI_DYNAMIC_TX_BUFFER=y +CONFIG_ESP32_WIFI_TX_BUFFER_TYPE=1 +CONFIG_ESP32_WIFI_DYNAMIC_TX_BUFFER_NUM=32 +CONFIG_ESP32_WIFI_CSI_ENABLED=y +CONFIG_ESP32_WIFI_AMPDU_TX_ENABLED=y +CONFIG_ESP32_WIFI_TX_BA_WIN=6 +CONFIG_ESP32_WIFI_AMPDU_RX_ENABLED=y +CONFIG_ESP32_WIFI_RX_BA_WIN=6 +CONFIG_ESP32_WIFI_NVS_ENABLED=y +CONFIG_ESP32_WIFI_TASK_PINNED_TO_CORE_0=y +# CONFIG_ESP32_WIFI_TASK_PINNED_TO_CORE_1 is not set +CONFIG_ESP32_WIFI_SOFTAP_BEACON_MAX_LEN=752 +CONFIG_ESP32_WIFI_MGMT_SBUF_NUM=32 +CONFIG_ESP32_WIFI_IRAM_OPT=y +CONFIG_ESP32_WIFI_RX_IRAM_OPT=y +# CONFIG_ESP32_WIFI_ENABLE_WPA3_SAE is not set +# CONFIG_ESP_WIFI_SLP_IRAM_OPT is not set +# CONFIG_ESP_WIFI_STA_DISCONNECTED_PM_ENABLE is not set +# CONFIG_ESP_WIFI_GMAC_SUPPORT is not set +CONFIG_ESP_WIFI_SOFTAP_SUPPORT=y +# end of Wi-Fi + +# +# Core dump +# +# CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH is not set +# CONFIG_ESP_COREDUMP_ENABLE_TO_UART is not set +CONFIG_ESP_COREDUMP_ENABLE_TO_NONE=y +# end of Core dump + +# +# FAT Filesystem support +# +# CONFIG_FATFS_CODEPAGE_DYNAMIC is not set +CONFIG_FATFS_CODEPAGE_437=y +# CONFIG_FATFS_CODEPAGE_720 is not set +# CONFIG_FATFS_CODEPAGE_737 is not set +# CONFIG_FATFS_CODEPAGE_771 is not set +# CONFIG_FATFS_CODEPAGE_775 is not set +# CONFIG_FATFS_CODEPAGE_850 is not set +# CONFIG_FATFS_CODEPAGE_852 is not set +# CONFIG_FATFS_CODEPAGE_855 is not set +# CONFIG_FATFS_CODEPAGE_857 is not set +# CONFIG_FATFS_CODEPAGE_860 is not set +# CONFIG_FATFS_CODEPAGE_861 is not set +# CONFIG_FATFS_CODEPAGE_862 is not set +# CONFIG_FATFS_CODEPAGE_863 is not set +# CONFIG_FATFS_CODEPAGE_864 is not set +# CONFIG_FATFS_CODEPAGE_865 is not set +# CONFIG_FATFS_CODEPAGE_866 is not set +# CONFIG_FATFS_CODEPAGE_869 is not set +# CONFIG_FATFS_CODEPAGE_932 is not set +# CONFIG_FATFS_CODEPAGE_936 is not set +# CONFIG_FATFS_CODEPAGE_949 is not set +# CONFIG_FATFS_CODEPAGE_950 is not set +CONFIG_FATFS_CODEPAGE=437 +CONFIG_FATFS_LFN_NONE=y +# CONFIG_FATFS_LFN_HEAP is not set +# CONFIG_FATFS_LFN_STACK is not set +CONFIG_FATFS_FS_LOCK=0 +CONFIG_FATFS_TIMEOUT_MS=10000 +CONFIG_FATFS_PER_FILE_CACHE=y +# CONFIG_FATFS_USE_FASTSEEK is not set +# end of FAT Filesystem support + +# +# Modbus configuration +# +CONFIG_FMB_COMM_MODE_TCP_EN=y +CONFIG_FMB_TCP_PORT_DEFAULT=502 +CONFIG_FMB_TCP_PORT_MAX_CONN=5 +CONFIG_FMB_TCP_CONNECTION_TOUT_SEC=20 +CONFIG_FMB_COMM_MODE_RTU_EN=y +CONFIG_FMB_COMM_MODE_ASCII_EN=y +CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND=150 +CONFIG_FMB_MASTER_DELAY_MS_CONVERT=200 +CONFIG_FMB_QUEUE_LENGTH=20 +CONFIG_FMB_PORT_TASK_STACK_SIZE=4096 +CONFIG_FMB_SERIAL_BUF_SIZE=256 +CONFIG_FMB_SERIAL_ASCII_BITS_PER_SYMB=8 +CONFIG_FMB_SERIAL_ASCII_TIMEOUT_RESPOND_MS=1000 +CONFIG_FMB_PORT_TASK_PRIO=10 +# CONFIG_FMB_PORT_TASK_AFFINITY_NO_AFFINITY is not set +CONFIG_FMB_PORT_TASK_AFFINITY_CPU0=y +# CONFIG_FMB_PORT_TASK_AFFINITY_CPU1 is not set +CONFIG_FMB_PORT_TASK_AFFINITY=0x0 +CONFIG_FMB_CONTROLLER_SLAVE_ID_SUPPORT=y +CONFIG_FMB_CONTROLLER_SLAVE_ID=0x00112233 +CONFIG_FMB_CONTROLLER_NOTIFY_TIMEOUT=20 +CONFIG_FMB_CONTROLLER_NOTIFY_QUEUE_SIZE=20 +CONFIG_FMB_CONTROLLER_STACK_SIZE=4096 +CONFIG_FMB_EVENT_QUEUE_TIMEOUT=20 +# CONFIG_FMB_TIMER_PORT_ENABLED is not set +CONFIG_FMB_TIMER_GROUP=0 +CONFIG_FMB_TIMER_INDEX=0 +CONFIG_FMB_MASTER_TIMER_GROUP=0 +CONFIG_FMB_MASTER_TIMER_INDEX=0 +# CONFIG_FMB_TIMER_ISR_IN_IRAM is not set +# end of Modbus configuration + +# +# FreeRTOS +# +# CONFIG_FREERTOS_UNICORE is not set +CONFIG_FREERTOS_NO_AFFINITY=0x7FFFFFFF +CONFIG_FREERTOS_TICK_SUPPORT_CORETIMER=y +CONFIG_FREERTOS_CORETIMER_0=y +# CONFIG_FREERTOS_CORETIMER_1 is not set +CONFIG_FREERTOS_SYSTICK_USES_CCOUNT=y +CONFIG_FREERTOS_HZ=1000 +CONFIG_FREERTOS_ASSERT_ON_UNTESTED_FUNCTION=y +# CONFIG_FREERTOS_CHECK_STACKOVERFLOW_NONE is not set +# CONFIG_FREERTOS_CHECK_STACKOVERFLOW_PTRVAL is not set +CONFIG_FREERTOS_CHECK_STACKOVERFLOW_CANARY=y +# CONFIG_FREERTOS_WATCHPOINT_END_OF_STACK is not set +CONFIG_FREERTOS_INTERRUPT_BACKTRACE=y +CONFIG_FREERTOS_THREAD_LOCAL_STORAGE_POINTERS=1 +CONFIG_FREERTOS_ASSERT_FAIL_ABORT=y +# CONFIG_FREERTOS_ASSERT_FAIL_PRINT_CONTINUE is not set +# CONFIG_FREERTOS_ASSERT_DISABLE is not set +CONFIG_FREERTOS_IDLE_TASK_STACKSIZE=1536 +CONFIG_FREERTOS_ISR_STACKSIZE=1536 +# CONFIG_FREERTOS_LEGACY_HOOKS is not set +CONFIG_FREERTOS_MAX_TASK_NAME_LEN=16 +CONFIG_FREERTOS_SUPPORT_STATIC_ALLOCATION=y +# CONFIG_FREERTOS_ENABLE_STATIC_TASK_CLEAN_UP is not set +CONFIG_FREERTOS_TIMER_TASK_PRIORITY=1 +CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=2048 +CONFIG_FREERTOS_TIMER_QUEUE_LENGTH=10 +CONFIG_FREERTOS_QUEUE_REGISTRY_SIZE=0 +# CONFIG_FREERTOS_USE_TRACE_FACILITY is not set +# CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS is not set +CONFIG_FREERTOS_CHECK_MUTEX_GIVEN_BY_OWNER=y +# CONFIG_FREERTOS_CHECK_PORT_CRITICAL_COMPLIANCE is not set +# CONFIG_FREERTOS_PLACE_FUNCTIONS_INTO_FLASH is not set +CONFIG_FREERTOS_DEBUG_OCDAWARE=y +# CONFIG_FREERTOS_FPU_IN_ISR is not set +CONFIG_FREERTOS_ENABLE_TASK_SNAPSHOT=y +# CONFIG_FREERTOS_PLACE_SNAPSHOT_FUNS_INTO_FLASH is not set +# end of FreeRTOS + +# +# Hardware Abstraction Layer (HAL) and Low Level (LL) +# +CONFIG_HAL_ASSERTION_EQUALS_SYSTEM=y +# CONFIG_HAL_ASSERTION_DISABLE is not set +# CONFIG_HAL_ASSERTION_SILIENT is not set +# CONFIG_HAL_ASSERTION_ENABLE is not set +CONFIG_HAL_DEFAULT_ASSERTION_LEVEL=2 +# end of Hardware Abstraction Layer (HAL) and Low Level (LL) + +# +# Heap memory debugging +# +CONFIG_HEAP_POISONING_DISABLED=y +# CONFIG_HEAP_POISONING_LIGHT is not set +# CONFIG_HEAP_POISONING_COMPREHENSIVE is not set +CONFIG_HEAP_TRACING_OFF=y +# CONFIG_HEAP_TRACING_STANDALONE is not set +# CONFIG_HEAP_TRACING_TOHOST is not set +# CONFIG_HEAP_ABORT_WHEN_ALLOCATION_FAILS is not set +# end of Heap memory debugging + +# +# jsmn +# +# CONFIG_JSMN_PARENT_LINKS is not set +# CONFIG_JSMN_STRICT is not set +# end of jsmn + +# +# libsodium +# +# end of libsodium + +# +# Log output +# +# CONFIG_LOG_DEFAULT_LEVEL_NONE is not set +# CONFIG_LOG_DEFAULT_LEVEL_ERROR is not set +# CONFIG_LOG_DEFAULT_LEVEL_WARN is not set +CONFIG_LOG_DEFAULT_LEVEL_INFO=y +# CONFIG_LOG_DEFAULT_LEVEL_DEBUG is not set +# CONFIG_LOG_DEFAULT_LEVEL_VERBOSE is not set +CONFIG_LOG_DEFAULT_LEVEL=3 +CONFIG_LOG_MAXIMUM_EQUALS_DEFAULT=y +# CONFIG_LOG_MAXIMUM_LEVEL_DEBUG is not set +# CONFIG_LOG_MAXIMUM_LEVEL_VERBOSE is not set +CONFIG_LOG_MAXIMUM_LEVEL=3 +CONFIG_LOG_COLORS=y +CONFIG_LOG_TIMESTAMP_SOURCE_RTOS=y +# CONFIG_LOG_TIMESTAMP_SOURCE_SYSTEM is not set +# end of Log output + +# +# LWIP +# +CONFIG_LWIP_LOCAL_HOSTNAME="espressif" +# CONFIG_LWIP_NETIF_API is not set +# CONFIG_LWIP_TCPIP_CORE_LOCKING is not set +CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES=y +# CONFIG_LWIP_L2_TO_L3_COPY is not set +# CONFIG_LWIP_IRAM_OPTIMIZATION is not set +CONFIG_LWIP_TIMERS_ONDEMAND=y +CONFIG_LWIP_MAX_SOCKETS=10 +# CONFIG_LWIP_USE_ONLY_LWIP_SELECT is not set +# CONFIG_LWIP_SO_LINGER is not set +CONFIG_LWIP_SO_REUSE=y +CONFIG_LWIP_SO_REUSE_RXTOALL=y +CONFIG_LWIP_SO_RCVBUF=y +# CONFIG_LWIP_NETBUF_RECVINFO is not set +CONFIG_LWIP_IP4_FRAG=y +CONFIG_LWIP_IP6_FRAG=y +# CONFIG_LWIP_IP4_REASSEMBLY is not set +# CONFIG_LWIP_IP6_REASSEMBLY is not set +# CONFIG_LWIP_IP_FORWARD is not set +# CONFIG_LWIP_STATS is not set +# CONFIG_LWIP_ETHARP_TRUST_IP_MAC is not set +CONFIG_LWIP_ESP_GRATUITOUS_ARP=y +CONFIG_LWIP_GARP_TMR_INTERVAL=60 +CONFIG_LWIP_TCPIP_RECVMBOX_SIZE=32 +CONFIG_LWIP_DHCP_DOES_ARP_CHECK=y +# CONFIG_LWIP_DHCP_DISABLE_CLIENT_ID is not set +CONFIG_LWIP_DHCP_DISABLE_VENDOR_CLASS_ID=y +# CONFIG_LWIP_DHCP_RESTORE_LAST_IP is not set +CONFIG_LWIP_DHCP_OPTIONS_LEN=68 + +# +# DHCP server +# +CONFIG_LWIP_DHCPS=y +CONFIG_LWIP_DHCPS_LEASE_UNIT=60 +CONFIG_LWIP_DHCPS_MAX_STATION_NUM=8 +# end of DHCP server + +# CONFIG_LWIP_AUTOIP is not set +CONFIG_LWIP_IPV6=y +# CONFIG_LWIP_IPV6_AUTOCONFIG is not set +CONFIG_LWIP_IPV6_NUM_ADDRESSES=3 +# CONFIG_LWIP_IPV6_FORWARD is not set +# CONFIG_LWIP_NETIF_STATUS_CALLBACK is not set +CONFIG_LWIP_NETIF_LOOPBACK=y +CONFIG_LWIP_LOOPBACK_MAX_PBUFS=8 + +# +# TCP +# +CONFIG_LWIP_MAX_ACTIVE_TCP=16 +CONFIG_LWIP_MAX_LISTENING_TCP=16 +CONFIG_LWIP_TCP_HIGH_SPEED_RETRANSMISSION=y +CONFIG_LWIP_TCP_MAXRTX=12 +CONFIG_LWIP_TCP_SYNMAXRTX=12 +CONFIG_LWIP_TCP_MSS=1440 +CONFIG_LWIP_TCP_TMR_INTERVAL=250 +CONFIG_LWIP_TCP_MSL=60000 +CONFIG_LWIP_TCP_SND_BUF_DEFAULT=5744 +CONFIG_LWIP_TCP_WND_DEFAULT=5744 +CONFIG_LWIP_TCP_RECVMBOX_SIZE=6 +CONFIG_LWIP_TCP_QUEUE_OOSEQ=y +# CONFIG_LWIP_TCP_SACK_OUT is not set +# CONFIG_LWIP_TCP_KEEP_CONNECTION_WHEN_IP_CHANGES is not set +CONFIG_LWIP_TCP_OVERSIZE_MSS=y +# CONFIG_LWIP_TCP_OVERSIZE_QUARTER_MSS is not set +# CONFIG_LWIP_TCP_OVERSIZE_DISABLE is not set +CONFIG_LWIP_TCP_RTO_TIME=1500 +# end of TCP + +# +# UDP +# +CONFIG_LWIP_MAX_UDP_PCBS=16 +CONFIG_LWIP_UDP_RECVMBOX_SIZE=6 +# end of UDP + +# +# Checksums +# +# CONFIG_LWIP_CHECKSUM_CHECK_IP is not set +# CONFIG_LWIP_CHECKSUM_CHECK_UDP is not set +CONFIG_LWIP_CHECKSUM_CHECK_ICMP=y +# end of Checksums + +CONFIG_LWIP_TCPIP_TASK_STACK_SIZE=3072 +# CONFIG_LWIP_TCPIP_TASK_AFFINITY_NO_AFFINITY is not set +CONFIG_LWIP_TCPIP_TASK_AFFINITY_CPU0=y +# CONFIG_LWIP_TCPIP_TASK_AFFINITY_CPU1 is not set +CONFIG_LWIP_TCPIP_TASK_AFFINITY=0x0 +# CONFIG_LWIP_PPP_SUPPORT is not set +CONFIG_LWIP_IPV6_MEMP_NUM_ND6_QUEUE=3 +CONFIG_LWIP_IPV6_ND6_NUM_NEIGHBORS=5 +# CONFIG_LWIP_SLIP_SUPPORT is not set + +# +# ICMP +# +CONFIG_LWIP_ICMP=y +# CONFIG_LWIP_MULTICAST_PING is not set +# CONFIG_LWIP_BROADCAST_PING is not set +# end of ICMP + +# +# LWIP RAW API +# +CONFIG_LWIP_MAX_RAW_PCBS=16 +# end of LWIP RAW API + +# +# SNTP +# +CONFIG_LWIP_SNTP_MAX_SERVERS=1 +# CONFIG_LWIP_DHCP_GET_NTP_SRV is not set +CONFIG_LWIP_SNTP_UPDATE_DELAY=3600000 +# end of SNTP + +CONFIG_LWIP_ESP_LWIP_ASSERT=y + +# +# Hooks +# +# CONFIG_LWIP_HOOK_TCP_ISN_NONE is not set +CONFIG_LWIP_HOOK_TCP_ISN_DEFAULT=y +# CONFIG_LWIP_HOOK_TCP_ISN_CUSTOM is not set +CONFIG_LWIP_HOOK_IP6_ROUTE_NONE=y +# CONFIG_LWIP_HOOK_IP6_ROUTE_DEFAULT is not set +# CONFIG_LWIP_HOOK_IP6_ROUTE_CUSTOM is not set +CONFIG_LWIP_HOOK_ND6_GET_GW_NONE=y +# CONFIG_LWIP_HOOK_ND6_GET_GW_DEFAULT is not set +# CONFIG_LWIP_HOOK_ND6_GET_GW_CUSTOM is not set +CONFIG_LWIP_HOOK_NETCONN_EXT_RESOLVE_NONE=y +# CONFIG_LWIP_HOOK_NETCONN_EXT_RESOLVE_DEFAULT is not set +# CONFIG_LWIP_HOOK_NETCONN_EXT_RESOLVE_CUSTOM is not set +# end of Hooks + +# CONFIG_LWIP_DEBUG is not set +# end of LWIP + +# +# mbedTLS +# +CONFIG_MBEDTLS_INTERNAL_MEM_ALLOC=y +# CONFIG_MBEDTLS_DEFAULT_MEM_ALLOC is not set +# CONFIG_MBEDTLS_CUSTOM_MEM_ALLOC is not set +CONFIG_MBEDTLS_ASYMMETRIC_CONTENT_LEN=y +CONFIG_MBEDTLS_SSL_IN_CONTENT_LEN=16384 +CONFIG_MBEDTLS_SSL_OUT_CONTENT_LEN=4096 +# CONFIG_MBEDTLS_DYNAMIC_BUFFER is not set +# CONFIG_MBEDTLS_DEBUG is not set + +# +# Certificate Bundle +# +CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y +CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_FULL=y +# CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_CMN is not set +# CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_NONE is not set +# CONFIG_MBEDTLS_CUSTOM_CERTIFICATE_BUNDLE is not set +# end of Certificate Bundle + +CONFIG_MBEDTLS_ECP_RESTARTABLE=y +CONFIG_MBEDTLS_CMAC_C=y +CONFIG_MBEDTLS_HARDWARE_AES=y +CONFIG_MBEDTLS_HARDWARE_MPI=y +CONFIG_MBEDTLS_HARDWARE_SHA=y +CONFIG_MBEDTLS_ROM_MD5=y +# CONFIG_MBEDTLS_ATCA_HW_ECDSA_SIGN is not set +# CONFIG_MBEDTLS_ATCA_HW_ECDSA_VERIFY is not set +CONFIG_MBEDTLS_HAVE_TIME=y +# CONFIG_MBEDTLS_HAVE_TIME_DATE is not set +CONFIG_MBEDTLS_ECDSA_DETERMINISTIC=y +CONFIG_MBEDTLS_SHA512_C=y +CONFIG_MBEDTLS_TLS_SERVER_AND_CLIENT=y +# CONFIG_MBEDTLS_TLS_SERVER_ONLY is not set +# CONFIG_MBEDTLS_TLS_CLIENT_ONLY is not set +# CONFIG_MBEDTLS_TLS_DISABLED is not set +CONFIG_MBEDTLS_TLS_SERVER=y +CONFIG_MBEDTLS_TLS_CLIENT=y +CONFIG_MBEDTLS_TLS_ENABLED=y + +# +# TLS Key Exchange Methods +# +# CONFIG_MBEDTLS_PSK_MODES is not set +CONFIG_MBEDTLS_KEY_EXCHANGE_RSA=y +CONFIG_MBEDTLS_KEY_EXCHANGE_DHE_RSA=y +CONFIG_MBEDTLS_KEY_EXCHANGE_ELLIPTIC_CURVE=y +CONFIG_MBEDTLS_KEY_EXCHANGE_ECDHE_RSA=y +CONFIG_MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA=y +CONFIG_MBEDTLS_KEY_EXCHANGE_ECDH_ECDSA=y +CONFIG_MBEDTLS_KEY_EXCHANGE_ECDH_RSA=y +# end of TLS Key Exchange Methods + +CONFIG_MBEDTLS_SSL_RENEGOTIATION=y +# CONFIG_MBEDTLS_SSL_PROTO_SSL3 is not set +CONFIG_MBEDTLS_SSL_PROTO_TLS1=y +CONFIG_MBEDTLS_SSL_PROTO_TLS1_1=y +CONFIG_MBEDTLS_SSL_PROTO_TLS1_2=y +# CONFIG_MBEDTLS_SSL_PROTO_GMTSSL1_1 is not set +# CONFIG_MBEDTLS_SSL_PROTO_DTLS is not set +CONFIG_MBEDTLS_SSL_ALPN=y +CONFIG_MBEDTLS_CLIENT_SSL_SESSION_TICKETS=y +CONFIG_MBEDTLS_X509_CHECK_KEY_USAGE=y +CONFIG_MBEDTLS_X509_CHECK_EXTENDED_KEY_USAGE=y +CONFIG_MBEDTLS_SERVER_SSL_SESSION_TICKETS=y + +# +# Symmetric Ciphers +# +CONFIG_MBEDTLS_AES_C=y +# CONFIG_MBEDTLS_CAMELLIA_C is not set +# CONFIG_MBEDTLS_DES_C is not set +CONFIG_MBEDTLS_RC4_DISABLED=y +# CONFIG_MBEDTLS_RC4_ENABLED_NO_DEFAULT is not set +# CONFIG_MBEDTLS_RC4_ENABLED is not set +# CONFIG_MBEDTLS_BLOWFISH_C is not set +# CONFIG_MBEDTLS_XTEA_C is not set +CONFIG_MBEDTLS_CCM_C=y +CONFIG_MBEDTLS_GCM_C=y +# CONFIG_MBEDTLS_NIST_KW_C is not set +# end of Symmetric Ciphers + +# CONFIG_MBEDTLS_RIPEMD160_C is not set + +# +# Certificates +# +CONFIG_MBEDTLS_PEM_PARSE_C=y +CONFIG_MBEDTLS_PEM_WRITE_C=y +CONFIG_MBEDTLS_X509_CRL_PARSE_C=y +CONFIG_MBEDTLS_X509_CSR_PARSE_C=y +# end of Certificates + +CONFIG_MBEDTLS_ECP_C=y +CONFIG_MBEDTLS_ECDH_C=y +CONFIG_MBEDTLS_ECDSA_C=y +# CONFIG_MBEDTLS_ECJPAKE_C is not set +CONFIG_MBEDTLS_ECP_DP_SECP192R1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_SECP224R1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_SECP256R1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_SECP384R1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_SECP521R1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_SECP192K1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_SECP224K1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_SECP256K1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_BP256R1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_BP384R1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_BP512R1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_CURVE25519_ENABLED=y +CONFIG_MBEDTLS_ECP_NIST_OPTIM=y +# CONFIG_MBEDTLS_POLY1305_C is not set +# CONFIG_MBEDTLS_CHACHA20_C is not set +# CONFIG_MBEDTLS_HKDF_C is not set +# CONFIG_MBEDTLS_THREADING_C is not set +# CONFIG_MBEDTLS_LARGE_KEY_SOFTWARE_MPI is not set +# CONFIG_MBEDTLS_SECURITY_RISKS is not set +# end of mbedTLS + +# +# mDNS +# +CONFIG_MDNS_MAX_SERVICES=10 +CONFIG_MDNS_TASK_PRIORITY=1 +CONFIG_MDNS_TASK_STACK_SIZE=4096 +# CONFIG_MDNS_TASK_AFFINITY_NO_AFFINITY is not set +CONFIG_MDNS_TASK_AFFINITY_CPU0=y +# CONFIG_MDNS_TASK_AFFINITY_CPU1 is not set +CONFIG_MDNS_TASK_AFFINITY=0x0 +CONFIG_MDNS_SERVICE_ADD_TIMEOUT_MS=2000 +# CONFIG_MDNS_STRICT_MODE is not set +CONFIG_MDNS_TIMER_PERIOD_MS=100 +# CONFIG_MDNS_NETWORKING_SOCKET is not set +CONFIG_MDNS_MULTIPLE_INSTANCE=y +# end of mDNS + +# +# ESP-MQTT Configurations +# +# CONFIG_MQTT_PROTOCOL_311 is not set +# CONFIG_MQTT_TRANSPORT_SSL is not set +# CONFIG_MQTT_TRANSPORT_WEBSOCKET is not set +# CONFIG_MQTT_MSG_ID_INCREMENTAL is not set +# CONFIG_MQTT_SKIP_PUBLISH_IF_DISCONNECTED is not set +# CONFIG_MQTT_REPORT_DELETED_MESSAGES is not set +# CONFIG_MQTT_USE_CUSTOM_CONFIG is not set +# CONFIG_MQTT_TASK_CORE_SELECTION_ENABLED is not set +# CONFIG_MQTT_CUSTOM_OUTBOX is not set +# end of ESP-MQTT Configurations + +# +# Newlib +# +CONFIG_NEWLIB_STDOUT_LINE_ENDING_CRLF=y +# CONFIG_NEWLIB_STDOUT_LINE_ENDING_LF is not set +# CONFIG_NEWLIB_STDOUT_LINE_ENDING_CR is not set +# CONFIG_NEWLIB_STDIN_LINE_ENDING_CRLF is not set +# CONFIG_NEWLIB_STDIN_LINE_ENDING_LF is not set +CONFIG_NEWLIB_STDIN_LINE_ENDING_CR=y +# CONFIG_NEWLIB_NANO_FORMAT is not set +# end of Newlib + +# +# NVS +# +# end of NVS + +# +# OpenSSL +# +# CONFIG_OPENSSL_DEBUG is not set +# CONFIG_OPENSSL_ERROR_STACK is not set +# CONFIG_OPENSSL_ASSERT_DO_NOTHING is not set +CONFIG_OPENSSL_ASSERT_EXIT=y +# end of OpenSSL + +# +# OpenThread +# +# CONFIG_OPENTHREAD_ENABLED is not set +# end of OpenThread + +# +# PThreads +# +CONFIG_PTHREAD_TASK_PRIO_DEFAULT=5 +CONFIG_PTHREAD_TASK_STACK_SIZE_DEFAULT=3072 +CONFIG_PTHREAD_STACK_MIN=768 +CONFIG_PTHREAD_DEFAULT_CORE_NO_AFFINITY=y +# CONFIG_PTHREAD_DEFAULT_CORE_0 is not set +# CONFIG_PTHREAD_DEFAULT_CORE_1 is not set +CONFIG_PTHREAD_TASK_CORE_DEFAULT=-1 +CONFIG_PTHREAD_TASK_NAME_DEFAULT="pthread" +# end of PThreads + +# +# SPI Flash driver +# +# CONFIG_SPI_FLASH_VERIFY_WRITE is not set +# CONFIG_SPI_FLASH_ENABLE_COUNTERS is not set +CONFIG_SPI_FLASH_ROM_DRIVER_PATCH=y +CONFIG_SPI_FLASH_DANGEROUS_WRITE_ABORTS=y +# CONFIG_SPI_FLASH_DANGEROUS_WRITE_FAILS is not set +# CONFIG_SPI_FLASH_DANGEROUS_WRITE_ALLOWED is not set +# CONFIG_SPI_FLASH_USE_LEGACY_IMPL is not set +# CONFIG_SPI_FLASH_SHARE_SPI1_BUS is not set +# CONFIG_SPI_FLASH_BYPASS_BLOCK_ERASE is not set +CONFIG_SPI_FLASH_YIELD_DURING_ERASE=y +CONFIG_SPI_FLASH_ERASE_YIELD_DURATION_MS=20 +CONFIG_SPI_FLASH_ERASE_YIELD_TICKS=1 +CONFIG_SPI_FLASH_WRITE_CHUNK_SIZE=8192 +# CONFIG_SPI_FLASH_SIZE_OVERRIDE is not set +# CONFIG_SPI_FLASH_CHECK_ERASE_TIMEOUT_DISABLED is not set +# CONFIG_SPI_FLASH_OVERRIDE_CHIP_DRIVER_LIST is not set + +# +# Auto-detect flash chips +# +CONFIG_SPI_FLASH_SUPPORT_ISSI_CHIP=y +CONFIG_SPI_FLASH_SUPPORT_MXIC_CHIP=y +CONFIG_SPI_FLASH_SUPPORT_GD_CHIP=y +CONFIG_SPI_FLASH_SUPPORT_WINBOND_CHIP=y +# end of Auto-detect flash chips + +# CONFIG_SPI_FLASH_ENABLE_ENCRYPTED_READ_WRITE is not set +# end of SPI Flash driver + +# +# SPIFFS Configuration +# +CONFIG_SPIFFS_MAX_PARTITIONS=3 + +# +# SPIFFS Cache Configuration +# +CONFIG_SPIFFS_CACHE=y +CONFIG_SPIFFS_CACHE_WR=y +# CONFIG_SPIFFS_CACHE_STATS is not set +# end of SPIFFS Cache Configuration + +CONFIG_SPIFFS_PAGE_CHECK=y +CONFIG_SPIFFS_GC_MAX_RUNS=10 +# CONFIG_SPIFFS_GC_STATS is not set +CONFIG_SPIFFS_PAGE_SIZE=256 +CONFIG_SPIFFS_OBJ_NAME_LEN=32 +# CONFIG_SPIFFS_FOLLOW_SYMLINKS is not set +CONFIG_SPIFFS_USE_MAGIC=y +CONFIG_SPIFFS_USE_MAGIC_LENGTH=y +CONFIG_SPIFFS_META_LENGTH=4 +CONFIG_SPIFFS_USE_MTIME=y + +# +# Debug Configuration +# +# CONFIG_SPIFFS_DBG is not set +# CONFIG_SPIFFS_API_DBG is not set +# CONFIG_SPIFFS_GC_DBG is not set +# CONFIG_SPIFFS_CACHE_DBG is not set +# CONFIG_SPIFFS_CHECK_DBG is not set +# CONFIG_SPIFFS_TEST_VISUALISATION is not set +# end of Debug Configuration +# end of SPIFFS Configuration + +# +# TCP Transport +# + +# +# Websocket +# +CONFIG_WS_TRANSPORT=y +CONFIG_WS_BUFFER_SIZE=1024 +# end of Websocket +# end of TCP Transport + +# +# Unity unit testing library +# +CONFIG_UNITY_ENABLE_FLOAT=y +CONFIG_UNITY_ENABLE_DOUBLE=y +# CONFIG_UNITY_ENABLE_64BIT is not set +# CONFIG_UNITY_ENABLE_COLOR is not set +CONFIG_UNITY_ENABLE_IDF_TEST_RUNNER=y +# CONFIG_UNITY_ENABLE_FIXTURE is not set +# CONFIG_UNITY_ENABLE_BACKTRACE_ON_FAIL is not set +# end of Unity unit testing library + +# +# Virtual file system +# +CONFIG_VFS_SUPPORT_IO=y +CONFIG_VFS_SUPPORT_DIR=y +CONFIG_VFS_SUPPORT_SELECT=y +CONFIG_VFS_SUPPRESS_SELECT_DEBUG_OUTPUT=y +CONFIG_VFS_SUPPORT_TERMIOS=y + +# +# Host File System I/O (Semihosting) +# +CONFIG_VFS_SEMIHOSTFS_MAX_MOUNT_POINTS=1 +CONFIG_VFS_SEMIHOSTFS_HOST_PATH_MAX_LEN=128 +# end of Host File System I/O (Semihosting) +# end of Virtual file system + +# +# Wear Levelling +# +# CONFIG_WL_SECTOR_SIZE_512 is not set +CONFIG_WL_SECTOR_SIZE_4096=y +CONFIG_WL_SECTOR_SIZE=4096 +# end of Wear Levelling + +# +# Wi-Fi Provisioning Manager +# +CONFIG_WIFI_PROV_SCAN_MAX_ENTRIES=16 +CONFIG_WIFI_PROV_AUTOSTOP_TIMEOUT=30 +# CONFIG_WIFI_PROV_BLE_BONDING is not set +# end of Wi-Fi Provisioning Manager + +# +# Supplicant +# +CONFIG_WPA_MBEDTLS_CRYPTO=y +# CONFIG_WPA_WAPI_PSK is not set +# CONFIG_WPA_SUITE_B_192 is not set +# CONFIG_WPA_DEBUG_PRINT is not set +# CONFIG_WPA_TESTING_OPTIONS is not set +# CONFIG_WPA_WPS_STRICT is not set +# CONFIG_WPA_11KV_SUPPORT is not set +# end of Supplicant + +# +# LittleFS +# +CONFIG_LITTLEFS_MAX_PARTITIONS=3 +CONFIG_LITTLEFS_PAGE_SIZE=256 +CONFIG_LITTLEFS_OBJ_NAME_LEN=64 +CONFIG_LITTLEFS_READ_SIZE=128 +CONFIG_LITTLEFS_WRITE_SIZE=128 +CONFIG_LITTLEFS_LOOKAHEAD_SIZE=128 +CONFIG_LITTLEFS_CACHE_SIZE=512 +CONFIG_LITTLEFS_BLOCK_CYCLES=512 +CONFIG_LITTLEFS_USE_MTIME=y +# CONFIG_LITTLEFS_USE_ONLY_HASH is not set +# CONFIG_LITTLEFS_HUMAN_READABLE is not set +CONFIG_LITTLEFS_MTIME_USE_SECONDS=y +# CONFIG_LITTLEFS_MTIME_USE_NONCE is not set +# CONFIG_LITTLEFS_SPIFFS_COMPAT is not set +# CONFIG_LITTLEFS_FLUSH_FILE_EVERY_WRITE is not set +# end of LittleFS +# end of Component config + +# +# Compatibility options +# +# CONFIG_LEGACY_INCLUDE_COMMON_HEADERS is not set +# end of Compatibility options + +# Deprecated options for backward compatibility +CONFIG_TOOLPREFIX="xtensa-esp32-elf-" +# CONFIG_LOG_BOOTLOADER_LEVEL_NONE is not set +# CONFIG_LOG_BOOTLOADER_LEVEL_ERROR is not set +CONFIG_LOG_BOOTLOADER_LEVEL_WARN=y +# CONFIG_LOG_BOOTLOADER_LEVEL_INFO is not set +# CONFIG_LOG_BOOTLOADER_LEVEL_DEBUG is not set +# CONFIG_LOG_BOOTLOADER_LEVEL_VERBOSE is not set +CONFIG_LOG_BOOTLOADER_LEVEL=2 +# CONFIG_APP_ROLLBACK_ENABLE is not set +# CONFIG_FLASH_ENCRYPTION_ENABLED is not set +# CONFIG_FLASHMODE_QIO is not set +# CONFIG_FLASHMODE_QOUT is not set +CONFIG_FLASHMODE_DIO=y +# CONFIG_FLASHMODE_DOUT is not set +# CONFIG_MONITOR_BAUD_9600B is not set +# CONFIG_MONITOR_BAUD_57600B is not set +CONFIG_MONITOR_BAUD_115200B=y +# CONFIG_MONITOR_BAUD_230400B is not set +# CONFIG_MONITOR_BAUD_921600B is not set +# CONFIG_MONITOR_BAUD_2MB is not set +# CONFIG_MONITOR_BAUD_OTHER is not set +CONFIG_MONITOR_BAUD_OTHER_VAL=115200 +CONFIG_MONITOR_BAUD=115200 +# CONFIG_COMPILER_OPTIMIZATION_LEVEL_DEBUG is not set +# CONFIG_COMPILER_OPTIMIZATION_LEVEL_RELEASE is not set +CONFIG_OPTIMIZATION_ASSERTIONS_ENABLED=y +# CONFIG_OPTIMIZATION_ASSERTIONS_SILENT is not set +# CONFIG_OPTIMIZATION_ASSERTIONS_DISABLED is not set +CONFIG_OPTIMIZATION_ASSERTION_LEVEL=2 +# CONFIG_CXX_EXCEPTIONS is not set +CONFIG_STACK_CHECK_NONE=y +# CONFIG_STACK_CHECK_NORM is not set +# CONFIG_STACK_CHECK_STRONG is not set +# CONFIG_STACK_CHECK_ALL is not set +# CONFIG_WARN_WRITE_STRINGS is not set +# CONFIG_DISABLE_GCC8_WARNINGS is not set +# CONFIG_ESP32_APPTRACE_DEST_TRAX is not set +CONFIG_ESP32_APPTRACE_DEST_NONE=y +CONFIG_ESP32_APPTRACE_LOCK_ENABLE=y +# CONFIG_BTDM_CONTROLLER_MODE_BLE_ONLY is not set +# CONFIG_BTDM_CONTROLLER_MODE_BR_EDR_ONLY is not set +CONFIG_BTDM_CONTROLLER_MODE_BTDM=y +CONFIG_BTDM_CONTROLLER_BLE_MAX_CONN=5 +CONFIG_BTDM_CONTROLLER_BR_EDR_MAX_ACL_CONN=5 +CONFIG_BTDM_CONTROLLER_BR_EDR_MAX_SYNC_CONN=0 +CONFIG_BTDM_CONTROLLER_BLE_MAX_CONN_EFF=5 +CONFIG_BTDM_CONTROLLER_BR_EDR_MAX_ACL_CONN_EFF=5 +CONFIG_BTDM_CONTROLLER_BR_EDR_MAX_SYNC_CONN_EFF=0 +CONFIG_BTDM_CONTROLLER_PINNED_TO_CORE=0 +CONFIG_BTDM_CONTROLLER_HCI_MODE_VHCI=y +# CONFIG_BTDM_CONTROLLER_HCI_MODE_UART_H4 is not set +# CONFIG_BTDM_CONTROLLER_MODEM_SLEEP is not set +CONFIG_BLE_SCAN_DUPLICATE=y +CONFIG_SCAN_DUPLICATE_BY_DEVICE_ADDR=y +# CONFIG_SCAN_DUPLICATE_BY_ADV_DATA is not set +# CONFIG_SCAN_DUPLICATE_BY_ADV_DATA_AND_DEVICE_ADDR is not set +CONFIG_SCAN_DUPLICATE_TYPE=0 +CONFIG_DUPLICATE_SCAN_CACHE_SIZE=10 +# CONFIG_BLE_MESH_SCAN_DUPLICATE_EN is not set +CONFIG_BTDM_CONTROLLER_FULL_SCAN_SUPPORTED=y +CONFIG_BLE_ADV_REPORT_FLOW_CONTROL_SUPPORTED=y +CONFIG_BLE_ADV_REPORT_FLOW_CONTROL_NUM=100 +CONFIG_BLE_ADV_REPORT_DISCARD_THRSHOLD=20 +CONFIG_BLUEDROID_ENABLED=y +# CONFIG_NIMBLE_ENABLED is not set +CONFIG_BTC_TASK_STACK_SIZE=3072 +CONFIG_BLUEDROID_PINNED_TO_CORE_0=y +# CONFIG_BLUEDROID_PINNED_TO_CORE_1 is not set +CONFIG_BLUEDROID_PINNED_TO_CORE=0 +CONFIG_BTU_TASK_STACK_SIZE=4096 +# CONFIG_BLUEDROID_MEM_DEBUG is not set +CONFIG_CLASSIC_BT_ENABLED=y +# CONFIG_A2DP_ENABLE is not set +# CONFIG_HFP_ENABLE is not set +# CONFIG_GATTS_ENABLE is not set +CONFIG_GATTC_ENABLE=y +# CONFIG_GATTC_CACHE_NVS_FLASH is not set +CONFIG_BLE_SMP_ENABLE=y +# CONFIG_SMP_SLAVE_CON_PARAMS_UPD_ENABLE is not set +# CONFIG_BLE_HOST_QUEUE_CONGESTION_CHECK is not set +CONFIG_SMP_ENABLE=y +# CONFIG_BLE_ACTIVE_SCAN_REPORT_ADV_SCAN_RSP_INDIVIDUALLY is not set +CONFIG_BLE_ESTABLISH_LINK_CONNECTION_TIMEOUT=60 +CONFIG_ADC2_DISABLE_DAC=y +# CONFIG_SPIRAM_SUPPORT is not set +CONFIG_TRACEMEM_RESERVE_DRAM=0x0 +# CONFIG_ULP_COPROC_ENABLED is not set +CONFIG_ULP_COPROC_RESERVE_MEM=0 +CONFIG_BROWNOUT_DET=y +CONFIG_BROWNOUT_DET_LVL_SEL_0=y +# CONFIG_BROWNOUT_DET_LVL_SEL_1 is not set +# CONFIG_BROWNOUT_DET_LVL_SEL_2 is not set +# CONFIG_BROWNOUT_DET_LVL_SEL_3 is not set +# CONFIG_BROWNOUT_DET_LVL_SEL_4 is not set +# CONFIG_BROWNOUT_DET_LVL_SEL_5 is not set +# CONFIG_BROWNOUT_DET_LVL_SEL_6 is not set +# CONFIG_BROWNOUT_DET_LVL_SEL_7 is not set +CONFIG_BROWNOUT_DET_LVL=0 +CONFIG_ESP32_RTC_CLOCK_SOURCE_INTERNAL_RC=y +# CONFIG_ESP32_RTC_CLOCK_SOURCE_EXTERNAL_CRYSTAL is not set +# CONFIG_ESP32_RTC_CLOCK_SOURCE_EXTERNAL_OSC is not set +# CONFIG_ESP32_RTC_CLOCK_SOURCE_INTERNAL_8MD256 is not set +# CONFIG_DISABLE_BASIC_ROM_CONSOLE is not set +# CONFIG_COMPATIBLE_PRE_V2_1_BOOTLOADERS is not set +# CONFIG_EVENT_LOOP_PROFILING is not set +CONFIG_POST_EVENTS_FROM_ISR=y +CONFIG_POST_EVENTS_FROM_IRAM_ISR=y +# CONFIG_TWO_UNIVERSAL_MAC_ADDRESS is not set +CONFIG_FOUR_UNIVERSAL_MAC_ADDRESS=y +CONFIG_NUMBER_OF_UNIVERSAL_MAC_ADDRESS=4 +CONFIG_ESP_SYSTEM_PD_FLASH=y +# CONFIG_ESP32C3_LIGHTSLEEP_GPIO_RESET_WORKAROUND is not set +CONFIG_IPC_TASK_STACK_SIZE=1024 +CONFIG_ESP32_PHY_CALIBRATION_AND_DATA_STORAGE=y +# CONFIG_ESP32_PHY_INIT_DATA_IN_PARTITION is not set +CONFIG_ESP32_PHY_MAX_WIFI_TX_POWER=20 +CONFIG_ESP32_PHY_MAX_TX_POWER=20 +# CONFIG_ESP32_REDUCE_PHY_TX_POWER is not set +# CONFIG_ESP32S2_PANIC_PRINT_HALT is not set +CONFIG_ESP32S2_PANIC_PRINT_REBOOT=y +# CONFIG_ESP32S2_PANIC_SILENT_REBOOT is not set +# CONFIG_ESP32S2_PANIC_GDBSTUB is not set +CONFIG_SYSTEM_EVENT_QUEUE_SIZE=32 +CONFIG_SYSTEM_EVENT_TASK_STACK_SIZE=6144 +CONFIG_MAIN_TASK_STACK_SIZE=6144 +CONFIG_CONSOLE_UART_DEFAULT=y +# CONFIG_CONSOLE_UART_CUSTOM is not set +# CONFIG_ESP_CONSOLE_UART_NONE is not set +CONFIG_CONSOLE_UART=y +CONFIG_CONSOLE_UART_NUM=0 +CONFIG_CONSOLE_UART_BAUDRATE=115200 +CONFIG_INT_WDT=y +CONFIG_INT_WDT_TIMEOUT_MS=5000 +# CONFIG_INT_WDT_CHECK_CPU1 is not set +CONFIG_TASK_WDT=y +CONFIG_TASK_WDT_PANIC=y +CONFIG_TASK_WDT_TIMEOUT_S=5 +# CONFIG_TASK_WDT_CHECK_IDLE_TASK_CPU0 is not set +# CONFIG_TASK_WDT_CHECK_IDLE_TASK_CPU1 is not set +CONFIG_TIMER_TASK_STACK_SIZE=3584 +# CONFIG_SW_COEXIST_ENABLE is not set +# CONFIG_ESP32_ENABLE_COREDUMP_TO_FLASH is not set +# CONFIG_ESP32_ENABLE_COREDUMP_TO_UART is not set +CONFIG_ESP32_ENABLE_COREDUMP_TO_NONE=y +CONFIG_MB_MASTER_TIMEOUT_MS_RESPOND=150 +CONFIG_MB_MASTER_DELAY_MS_CONVERT=200 +CONFIG_MB_QUEUE_LENGTH=20 +CONFIG_MB_SERIAL_TASK_STACK_SIZE=4096 +CONFIG_MB_SERIAL_BUF_SIZE=256 +CONFIG_MB_SERIAL_TASK_PRIO=10 +CONFIG_MB_CONTROLLER_SLAVE_ID_SUPPORT=y +CONFIG_MB_CONTROLLER_SLAVE_ID=0x00112233 +CONFIG_MB_CONTROLLER_NOTIFY_TIMEOUT=20 +CONFIG_MB_CONTROLLER_NOTIFY_QUEUE_SIZE=20 +CONFIG_MB_CONTROLLER_STACK_SIZE=4096 +CONFIG_MB_EVENT_QUEUE_TIMEOUT=20 +# CONFIG_MB_TIMER_PORT_ENABLED is not set +CONFIG_MB_TIMER_GROUP=0 +CONFIG_MB_TIMER_INDEX=0 +# CONFIG_ENABLE_STATIC_TASK_CLEAN_UP_HOOK is not set +CONFIG_TIMER_TASK_PRIORITY=1 +CONFIG_TIMER_TASK_STACK_DEPTH=2048 +CONFIG_TIMER_QUEUE_LENGTH=10 +# CONFIG_L2_TO_L3_COPY is not set +# CONFIG_USE_ONLY_LWIP_SELECT is not set +CONFIG_ESP_GRATUITOUS_ARP=y +CONFIG_GARP_TMR_INTERVAL=60 +CONFIG_TCPIP_RECVMBOX_SIZE=32 +CONFIG_TCP_MAXRTX=12 +CONFIG_TCP_SYNMAXRTX=12 +CONFIG_TCP_MSS=1440 +CONFIG_TCP_MSL=60000 +CONFIG_TCP_SND_BUF_DEFAULT=5744 +CONFIG_TCP_WND_DEFAULT=5744 +CONFIG_TCP_RECVMBOX_SIZE=6 +CONFIG_TCP_QUEUE_OOSEQ=y +# CONFIG_ESP_TCP_KEEP_CONNECTION_WHEN_IP_CHANGES is not set +CONFIG_TCP_OVERSIZE_MSS=y +# CONFIG_TCP_OVERSIZE_QUARTER_MSS is not set +# CONFIG_TCP_OVERSIZE_DISABLE is not set +CONFIG_UDP_RECVMBOX_SIZE=6 +CONFIG_TCPIP_TASK_STACK_SIZE=3072 +# CONFIG_TCPIP_TASK_AFFINITY_NO_AFFINITY is not set +CONFIG_TCPIP_TASK_AFFINITY_CPU0=y +# CONFIG_TCPIP_TASK_AFFINITY_CPU1 is not set +CONFIG_TCPIP_TASK_AFFINITY=0x0 +# CONFIG_PPP_SUPPORT is not set +CONFIG_ESP32_PTHREAD_TASK_PRIO_DEFAULT=5 +CONFIG_ESP32_PTHREAD_TASK_STACK_SIZE_DEFAULT=3072 +CONFIG_ESP32_PTHREAD_STACK_MIN=768 +CONFIG_ESP32_DEFAULT_PTHREAD_CORE_NO_AFFINITY=y +# CONFIG_ESP32_DEFAULT_PTHREAD_CORE_0 is not set +# CONFIG_ESP32_DEFAULT_PTHREAD_CORE_1 is not set +CONFIG_ESP32_PTHREAD_TASK_CORE_DEFAULT=-1 +CONFIG_ESP32_PTHREAD_TASK_NAME_DEFAULT="pthread" +CONFIG_SPI_FLASH_WRITING_DANGEROUS_REGIONS_ABORTS=y +# CONFIG_SPI_FLASH_WRITING_DANGEROUS_REGIONS_FAILS is not set +# CONFIG_SPI_FLASH_WRITING_DANGEROUS_REGIONS_ALLOWED is not set +CONFIG_SUPPRESS_SELECT_DEBUG_OUTPUT=y +CONFIG_SUPPORT_TERMIOS=y +CONFIG_SEMIHOSTFS_MAX_MOUNT_POINTS=1 +CONFIG_SEMIHOSTFS_HOST_PATH_MAX_LEN=128 +# End of deprecated options diff --git a/version.txt b/version.txt index 290e4a2..fafe2ff 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.021 +1.041 diff --git a/webserver/css/bootstrap.min.css.gz b/webserver/css/bootstrap.min.css.gz deleted file mode 120000 index 8c999ad..0000000 --- a/webserver/css/bootstrap.min.css.gz +++ /dev/null @@ -1 +0,0 @@ -../../../sharpkey/webserver/css/bootstrap.min.css.gz \ No newline at end of file diff --git a/webserver/css/bootstrap.min.css.gz b/webserver/css/bootstrap.min.css.gz new file mode 100644 index 0000000000000000000000000000000000000000..f5196567d2d5052ad62db0bfe4dde8580925ac3b GIT binary patch literal 18136 zcmX6^V{m3o(~fOxW81cE+s4LsYKo~sF> zprGE8tc`&{jU60ZoL!uZe$$&cI|E(%tk>gm{Ml*m3Z#<=Ba15X=*A0a;#=oqnn#|$ zum5C3Zgwvmp~hElA2HTIcO~_DypG~Q5!_QgNVr(7<()@?O^*;YCrNFtES1= z-KjbLOiTHeXl0nPOF*nX?y!Uyu5Rb`e!rZbpXY2heK%pM&K!22L0Is8C-D2c@BTXe zep{Zk<1$lwb!OUXx!n1*<^44c`R#tw(*31(C*{kRviYyFTdfw3$EMB6 z0KFemOXujIrsJf(-a3o{;c%b7-Nnqu{JV?2=tdAs!t`cO)0FG|y{@b#bvH^y9p$g{ zlkZCb-4kAEXO6tqz2nVzR4*aJx=gZPbrbh{ANx0A8d*?= z8itx+H!IoH>Mn#25@8pr>P7!1r!N1}W!wo$#pOh9DwuJcMIvtgB{Rk|dljgrE3@TJ z|40y?5hzqS4^4L_;ll@VuovP-@6D+uH}Mg%o3K7RVcHrTq4gc#=OXNRrp zbNM$IY9?VtN+0!o0$fo%=_ZHzaaVHUFH^6F-f-Dy51@%&zEQR@hxv;!4V^;Xsl}%2 zdaRn-_kec^zRrI(R#iJ!vY(!S5R|9Dw;=bv5aTFuW~92BQ`VRBe6IOjGhuBk7V9Xd zVK^NkvPihfk6g_+uOs<*I1HE#lo4#szfjzUz*#Lw9D*;S1LF{p+dRUAwDqoO|m z*3w}9>t>#wpzELFzC*+OAX}`8Pjy7#)8OwsS+EfY>H`T%#*I>wRdkZj8%9C`355Z8 zrf|RdJ3oCXL>mGK20udm<7tN1Z9il`hk@<&RJcMuC4<{2tzB_VO|N3fO$;Zd$Nf-&OqA%Yimg)#__KX0zC2n4S&q~99uRGyld5DXS@x}_q)(iG0C-1UO z{g`tY%9P?XSiYV!>VV@R8(>*}jUuoeG*^##-z@qHf14~+?9~y+AXdq+P!Zw|}cZp!H2+({_qzpSB&4n0W}T&YNP-Q~F2X|FZWyGp(3gYucXwW49~ z24wJ4AZotk0QPM)&|l*qSk5Yhco`5@oA&ULorFU-KNX4^cx)|950K#2U>3H7UmFn- zc2gL;1Ylt5`HU2j4l0GeU@-9(Nm<+&;t&y0;{32Lsm$4!;V45BG2h!p|4uVzz;V^t zhR5g8nfK5C<;}J7?kRVIE%-Hh!nB5Lx?DY!g>ox^7n1>lmlK{@Q!k^VIC|%I+Lnfz z0*7=UkR91E5w1Y)Hg4(N|Fw)o1K~%hklJ=y?>wwF682(}qVn`t5D#kF@TyOV zrohB!oFuisg$sqHAm%3Yj+5%>9>>uMX9lw%?2vGKMf%|6R*N>pyjN{Jp9*phy3KjW zid7oRd=oZ~b2m>g!XVj06aT6nKT%(;{`)aR8YnV@R0y)%YUuhqbC0rU-ffuZiS5}r z;kZ>K^zaBIs_k+|r4qd@`W2J|o5huk1*2)y#$T zR!YnStt{rqF|{)T4s!2XKNFRhgw*r(TI{haugmu)cd!Y-h4&Y+zqgy!OdY;z6+1c$ zgSU~TujmxOv`)J`KN!sgV@2rvG@Jz^SV8Ld0N`mPrlskVfjaBa2;zCvn*=9A3_joF z_j(6$e6hyENbJmKl1c(IU$w5&%LnTTPhjjf?em0%O&!oYovB=y3iD)+=Xb&eIZ^WA zj7R!20j;?MPY(8!=S!^QH>ib!nuZL`$G~>mbX_&tu1PSXB8E+-nS-fVu!au$x7Hem zP(R{a7rF#X*S#ah?YOd1_#6fLF;J7DmlPD@W0WXz-3GF$n-Lg zaGr}fFecp1usg0_8vAnG++j$!{;e3PHErZRz;2)K^!+ZqW^trYaItlOD-8%@>T(TWSNs*Z_E;i*p?muCq6>>A@8Cj&AF?=|}|<;*gC52TEonLOld`e(CszCFT}& z!9$dYUBG5^I;moeI%j+(_&FwBr8+IcIa&L#A;dVNVwF1A-t_fP#TLEB3rz(}q!ac` z%qnl-?Ly*=U^a1JEFNs!OLDI)`59_Ky7VCN z@pR`0J7gFVno$2x%tIyISet3;A&80lv)(GD(9k2G*Xz-fd-ZahC!YZWf&PQF(F4&b z3LL1|(L`9*YjMfFO{~sFGK~r~Tb&QfhLt0Z5lEc`ehswgJV_#7B-3rSqLxUlf2_z> zI+E6*h6~yNy0Ka3fsRpgc&Qf5orFs>Ikw*|hbnwtrH7uaXp{UlgUYR~p=Blb3sPEz z1i6fw`bw@A0~8=@n8RE(!!o8QOcMLKt8@9!ie{mig4tw(GoV>K3JO|rz{V|QFe<1C zm9<$lr_CqEH~pq}42d;#jkkx|YJ~Ppct>cDX}_<^_$ugJWf+>V3@20bO9g|$0Yu?w zH@|rVRUSu?taLMokeyiT;t5Eryl7`o7iavf#;d`0F-dO!IQXL4u{mL&X9;J{&y8Ru zHy?kuyk%XU@9sDZwi5e&w(_I4kF4$P9OjOKmV{mC zsg?V)%lVcPcu(rieeblvZgg4r5i|dGH74)}mT7uQn(@j2HCf^u*}owzkdrRhtJEI{Q-c1sTqt~_eJOgc6C(baOLdy#(89WM0KGRrX-P3*QMU85S)jbBly&0VPY)dCvi0{DMY-q7bCSHOAo34P=Ihimp`|EV?JYg-b)(7adMFd3??hkDQ4OFlsyn_kEZ%qs4 zi?E}oLj)9>Q&1w@1eA=_fChq?Y>opXuudp9s5{$0C*CKow-SCTIS~WXY< zf)bC5$!x3!;&@3sG$CVHjU=nHu*1e4McD|#D)=JOAeFjeWIbX<8BprM4b>#mk5YF&OS7e@bw(*aPr^+$~TOD@yb~!Dbzq^G_M#Vij`92 zEaP1&n681IeGHr6GUx#6NWS4(WL$QTrKEWD7T^W20(HCKYWo{%x2FUkxAyEokd@0d z1jAS1<}aoofuoyYkT1i8t_C)q`E26zjhlKr)Z(H2%d)$OwjIOpmaJW!#?NwO~Ya{%Zc8Aetdd z4F1WM2|((BKPEEuX(06iF%V7Y|7kUU%n|6&hGd#Q+S6(cAey>BU|LXVC_{AJ8X$F} zN0=W6PXA=Z&YyV;|8v*{QWyT8R{6(F90IBdy~yCNMtzRjBVD8jBxCp!6wq!85JuiM zN{{tJ5s-S&|AdQ75Y3nzv>xjlogcwR2B@Mg5kwQ-R|N>xb|(}_-Q*e3u(V;66r)$Z zR32&^(eQ+T<;4WSk9H@H?4vGFVCiNU;%lXkfH2rB42qbjlAm)2(TFI!5h>+%mL|A( z5-O~=aj{cale)o|h!)+0Gq@P%482eJ^9jbN%r#g<(^~dZShGA;{v!(p zKWPcB#o?n0UflVg@LqzXUPuvAi}R6!q;9f}(r0~9g!Gdyv_25re?-BHD+x%=EC@w9 zOnL{6JYvUEe!S0P=yp-n*F*U|0E&9fgl^m?6DVm-VeH0Wa;8BUIP2C_?Wg!r=nnz4QKUBYl(eu+dm^ZY6&4x-J^301Lm zw!=Z|@%~nxJNdivsrzN~c@*SW{qgO&Z$en!(g-01pSj1-ypQCFb~Eb>67cQoib}ig z*DJDL++xg%G#-RhtvM@dPIc@B)sN$<0E~ouTFw$hh8Hrf%w`8D+;QHSt^_z>#ux-( zMmlPBa0o-UJVo?^2?6|fAXRjJph_5C{~KUKwW72{4K3nh1wkFC;9`2PzB!Fb479#E z&A%90STpH}!ZHhx0ZV&InTQ}&gBAZiey1H|*3L7DG9=!B=f{oCkQ+|awa$En%0*uK zqBl$CEKnQ9)BBsn{fyWIYDS=OEKs%ZdVeZUpvD*gP%SmD+X-+_`AvJAmi~cQBzhmO zsS=4S82e5Exh1FbwW`U`j}`Eosfg}8r$F$+ttm29#oP32jtXj-e^PuAj+$_9ty zz+Rl2;%9x!tGU757Rd032uv4v^&){$JLt&UKFRhG4&CZJY`UlyHw^E{!--06kkZgF zc;;S>n_qG_=QriRmPMOA4eY1v;A4vFU2kaA|2W`#mGYM$cit6M(ziY$T^UQwh&;0d zs{S3UgPO*$Ejh5T7$YhR&M)C?Pi9=cHd=RN4&NqW$IUW-sofqCTHKgvsma~mO~)}; z%W7`cUe6-*xR|Ez4ivnv4}E-tfWyyk|0hYVKPia~%loctrf~UnHdU}^oX5VJ*dWD@ z7M}EvhL$<+kVT#_ zfO*2m?WnT1aOdBu7=ji4k(Qr==37nXY)YrpvELe$x+`TxpFa6~+~DoP$98VDnufln zyx?ufS&m71#5(#^UYCpa_)aQjP|oT{%8HfuNt%A1&X#WqDZi(FIZo9lz>3b#aX#u&8kmQaFQF^ zzzMocEihO~*NPd`J~&lpkpMeWt}M(7m#rnG@K3r>g4b?281F`~o)Wsjt#uH}6M< z|7izhdFBauSfXioS?KHX;S5FaAs0awO8Miu|@_XUP2ImSSqyxvP3_cg!c*i zzg|@MJzX3Iq$oHI4%Yqz^<>bu znRw8}W~9j)tqPH58Wmyj8x&rE&;=v3N=4tBs!)xV9Ww8rtB&g?58b-5`BLp*jMW?Z zQp#MBR!*O-<<-d$YyLHtNi&|ShRpD@g<9p4*IE@KAea~5=&^uOnU|v6E;p+r8Fd$r z4X@@5Dlf0n>s;rIDw8pl8X4&)r4ce4Kv2Z~ zNq@}xO8ornQ4ZD*5k7@Qku4!$jI4Si2??U$!x-EsF$x$DJc{isfd+Dn_< z84EbD(_26Ql45rlei0!Lk)op(?uk-!-Z(PJ?u;*f`mfh5U^p((!AAG?hJi7q7v>mi%Bvasj;$B%Y*dpl(^i<+j$rV z93u34CejQin}M&koojLl81YC2D`@txIYAAY-$|xbUsU|14+_BC1E)sTlGnMwdV8Pq zRYY?Fpeehjx%TLF)u;;;TLRhA1)uYchz8%XS7J`c+^cPfiL?fW=bmB-lSedR_4z8V z@YX`UECfDt^aFi~$ z5*_|RK@z;1JCPRXBLhSD&Mc5QZO(<+lG%}DH#z2<+FQX#*Q4Hlur%`Tr^>)14>hma5R3#tH53L$8&cz~OT!V55st4CeJ zY*rA;G2nOz^X5z;Av;$#?a{q!Uu=8W;CWRc=s+w9q z2%|mz6muZ%jfHh!g@*DKix@R13EuJBtF7;I^k6^FDi4#YxR}%C0BsZ+bBc)5-A-b3 z`UU0ZufGOwQz2^#d~a@ec`R8ZJ5X$X)O4 zwpa`>pSSETd001B=#W+EeDmb}O4)1N=BHknRqBh)*t^v>oBa2;y~bcW{6(OPI!feq z_k0=D)1UBM-$yUY4IEjDzd#6%t+K`d@g2#h91&75_WMFC|C}(Ox)pbukhTMgBynRO zjN|%iSgDZ!OT>q%0QzdV%b*-nFUT*|oX-P``ShULnlqa#5ly)gdWqv_F)_~&L<_#v zHaq&M_xd{%JiKU$jMdp7MOma~`Q_$~cjS%Gds86a=++=dQmkwOqf_-+vls&3kiNLI z#n-we1Ps29y|8W0o3MKB*Im6s*I{Hw^f>+y%*0c$-dmCOwnDa~Duc%G)uk?ci9EeY zw!w&KSKsK6pj&6Xd6HCLG>vt*;x34cUF$43Sl{0oi*~PoDeup4!+&F2%W=NHMVIB4 z-qbjp(S0fvd|Rh<)#g=)Krx$p(%r8mlPNar@No zp3o-$-sq=IV^1W0FUk$?&UV>85mB{UJ4DJPysitlKW?ExqJ+K6nO&9VxZUQZ9aq0u zk?w}MFZ%su?(Uq{dz`nGP9=eE_%xZ^Q2X-AsQ(A$w$)nU8z4qE9Cx(lFQ{SxKqg&A z_*%U<-*5nNUIug1!ZHiseD$xy3{RcL|5Voh*S5U~Ows+@Rbh8@gV}%o?0YWBqFabw zck#MGW!BUePUDhwEn4MmAVY(JqEb9|AT zCu(9DqC>O#iVH*F_F~xC&VQe6x1`m)XZFj{uKjK&t}~^Y3#>n-*anVla(7#7ARq=5 zj&6>gbb-X#o;xshE7fB+Qvykr)R^IUv+1@yDA|Uf^n$tIlOeATz2Gz}ax-rC>>sS_ zYI4N}F$3t0HZ|JdSQ0C`pvakZY_ zkmE+q)Dlp~P4y82c6#g2{J|EN*KvfZ^_{aEXxhlI$&K*P0zfqx+IOA?MhBE6 zuOICehwDIowVq5ozzDP6kuHEaq?2YKsk?C`OX8+fgofgd{XL#P1kuX^Abi((0_Vmd-vQ z-rp#M&M_()4UP^8(jfuhzY8XERb_*33kD^x)?95Bf2#OHGT_#w`3Cbj#|aa(QP6@y zyu~AUQga(CQ0Em}8pD_A?kgEl)>1RwN}od0qU1^X0(RjR57d`Ck4M zN2p>NAcvP84K;zzj=eS&5Q)T*tv1N@T=OND;Mz>ibKK-^W*=Eue|bJ%b^|G*Vp?{Y z#FrBV1W_+YD~{kdoJmY$k3zHXSTfS4qvE0<7A{1qU%`Os#>Y_0%Oi%djX*1+HxL=G z#uwGanaOHO;8%Q@0ouO*ebE6>l0@Idd5fN`n5H8Cp4a3DbR_L0sYD zMI8j1AbgC;Td%qNtEN;Bw_en=Se_6Z9$BlB=g$@veqqT-;IDp|m@pO7*;t|woI@Ni z5omU3t%m)5udy2*v6O4L3q#ZdS6)!A8C6rMX+e07)uy!J@~u}nIIT4=>&ON_4a-Q{ zb62hRM0Y)#NZOMKo^pM-WkEMNExaeK9vLE7PgbPegi=gct@-hWr+*co4QW*M_3oqe z;7ZD;%QW>J!*26^?RqMv$#K8VZ~I7Syj@(E<94)xd>UT%wqMhUWxSW!3F$Sm2|wQ!1fpNa z6ghqlkujL|A(x&=!JqaU85P!u@Z8r9nNiAg@>(%r<$a&>IoF@avU&|%TN7*qCa(w9 z;aH)rSWqvY0A35>z*1#nBVGK>{ zkw@GSDfjoLE18m|KbNVye|CGx@r-b?k>g;p9C3N}ws$<6Kx{Lf+^3|bu26zH5h-n2 zp5!?a3bWo{27=WMbF?+s9O4RJxm(TVUjlIWLgG&$r%Y`pW${$?@xx6079u4&8kfJI zFRj)pnGcwnSC9z;Q!(F}yO*{K0|#x1*%Uf7jXG(JbMMiHNmkR1_x@UD*!B99dYgJWHVeeG4YA_~k zB>;p<6lMyh&&L**Lgih#s|Yuc>O=gR5(AqZPuzSs5f|~%TSGL;ET2nAZ++6RQ=4z} z-kcHIfer;d!W406(@RNTfH6@r6K5;%xeW5WUh}!rsK!QCe? zN!+$v>%~TriX}deK>HbUd?{9IcVoeKT~3I>Q$!c9y+*MC{WOxeU|Fgb)V@=tml8qj zWA?w=V-F6+=Pz$M|HChKEX6MwbtMGP<(*{dyK#mm!`;ae3g=P1jJLW8g0DLRzv4qb z3n6Z%m)q?zAC;EJd|Ux#3bB(;>Z$1CYXq%Dj6HVncJOz5|7hS9GJN3dkT7KYzUR}f zTQ5^f12J?dF>yal(AE5p9<%Di24P4`)8T=k=1X-}RDhDIJyVNkCmdiw_~I<9bK7h( z{Fa;;M9v8)5Esjv5IN`oKSsu7=V9)kZ-;LxW?gj%!67>(4-V;qcFKxM85~i!OES5{ zTMjyiwT#+=fVw~7@`5a7g0ut;Y@Qj5;4u?*r%>{*vA0R;XumFFDhxWRJy0s;8iZtz z6)dgG8W?PLGq$&uU9eeBvo>7Ckuq9JygSral^bDauN~n6Vkcd(b>3#mM-Pn;WP^{f zD{elm`0_urXk~XIQ{WEHm_lx(`ksGGvHNX=1*Uisq0cAD3Rxbc@hfo7XiV1}&YrTE z4tTD;*VW(w)BL)H3vwNFEVlhNBYJxCElY#ZE|$I*sTnxKF3L*ZXH9E!LUIOuT_Q@)x&$0>iNOXnJNKJ4ub7{AOy7pn*#;sGp{5RuG25QMHstLHMA`z# zxYKUb$a=CaeSZNZA?wx9(5^eL(7+*pFheOa<>}6`uY_QQF?qQdp*A9H`etN+Tm7|_ z2n;VBoTU}=#b1mpkJ0E-3i4qO;eyyhU;$v@yLje9CL&fb$(lq^ZMdLJ|EUaI-p(m_ z6wuLIB+*`OD1G-&5#FRJL8j>(g789e*(Z_K`6I3YGeOFm&6NZ?bYEefis)!Te%4BZ zh#1}I6D0c>KCo}R)`Vs!gcqsL++u-F6o@J{$8JpX1}hfY)7fqVxzxa<{*?N|csV#w zicmF$P$%m7>Q%>KV+DHujx;&xfrE!|OqkgOMad9(BbnyR!_~jDaES?VP|!V=1)vzm z2k-P}OMveU{l+44qa_XyrNx1A#)KjFg5(lkB!8Y&959)m#=-JDlZ-{0zX8qPvzRs7 z{>xN3_OIZDZn=JhFLwOyl{*uUNBD`+GD>F2)VTW(aSRkcUq2b9zt18STc&I5lb9O# z5I+BecdmsVxhk=cG^e6bYrTc=GxT!&4( z*mDH01@G{de9$!R-450qwlUCDRJal)y{{RH!B>^J-+PClBu%0riJhD${@S!X5Rwrm z@>+lW=Nx-h6?k+o=s@&9$ldST;vCB7mMPd2bK*{&j0ftr+)W_E@(9v~=uD^*2Yjl~ zUl`n~c>}1z@GW0kl1mlM?t@v-cT8FA&Z~U6Un1vBnA|%ep)LH{BNV*EyA1FB1)vYN z!WTs_?}hDVHz)Df`33N32H}%WTKso(=&vKsa!BVocutQ&L(IsWG@f-^Qt+V=Gl6xU zAct;Qrubw@^YZX z4oc)+VQIPARTL7fDVQbrR3DRtvtYYuP&t->K{W^L?4$@MhPBKoX5Wg?T?q$D=MD`A zt2hSqVk{YDHQ(nPh~)oTQl5t3f7jw4E#Ma6X!OOcJgdnV44xU_aynhG&f;sJc}91_ z?+Z(FVfOu4h@;iOOZdk2IdUEhUF^n9DwWu+To4b~MH4U5X}+p89`NFNH_88tV3q64 zq4(#hm%OCWdVZ54E2v|a&i3gWl`DzM8;a*;u!+*%`puz+aDru|i`ep#(|?`A^@6@V zkCXJ6DP$+zZ?t<@zpiWi#q^s|gX)$|%4Tyc$RRVM0P)cevn#UQ)zC1NOV00j~R-0QITd@3Hras>s9 zc4lt8#xjr$MG2k)Q!R$UnMkc0!k!|drN+M<+asAD0teyWo#3*mV@cm=qhgtEB*`gy zA}_IXCLT{lVhiP%0+NPe!&zhR)kxK9-yVG=5YDLsI;M_zAIS1O2tAvLZ zE%W*8BKA1rC*lN;ED>?kk;FZ*oKTShyzFUTeWKIXntO}tyH^K=R|lg^XDtzMXLLO3&5Fe zD;~nbx9pILTZdW>ml|8kduU?>-JR>BtAn*{HcXvs35@+5PT*xj`ZfaOnFlXbYh0%> z>9D6N9%(=Qc~0raK@R*^a6Fz)z2ND`DO8oRNLynUixepD)lE*8+3 zkPvE2>jRh{^p@uEvzm`>STg7D)rP?M%7R7%DLXWO0?!93;t&kWSV9}~A6F){UR2IU;r*)Npd1DkEgvFHu4mkFgv>Mx?QI1xYme3%cSDW~+k zk=ymsK(M#O9;Bw44i2g&B8th|2g&!!Y?tKZqsgvqeC95pu z&D*cX&N$2AluUOaH@iIF5@$TWOe?B5X+@EARkLM5eEWCS@vYO z1|4oc5YzM?O)%whufMhBtsl|=p8=LJ7OmNRdVx3D!H#n5E5964gSm{i6!!V807DwK z!E*Jnf(q*CS)O2a7cq=JQ~Wzq<1D}^@3!Gv<`;-41`6yC0!c{%UCmc>fP{-WV8}D~ zsRhiXmB#EB?f@$X?@d&Wr7KDAPrOOQfn}u`)~9Pu>MTQ--WP6Q&N}#n7th|W4Xg02 z7w(yu{@W%mMQ!r!Tc#=QVdoM)YI4y^9vA)mQ$h<#7aE8OUHTh3lUAj>`2 z{><|g&JRm&^0AS{-Gk$byv9eB@6nO^wkYn~5PnC`7|0WJq>1j3b`pagP{wR}?7q?I;A~fl=st@`}zKEK-iD znB-N=joOtygJAK7jW~{NF}4tngAk?AzxJe-hO}ra1>C&tYXhEV^xE*khfwPNNo)qv|+==kh+vlF*|-Lb%3v-?uvG z&wyU%R2zB5&f}hstM?9uEZPj=#fZ8otcu?_u4OC&jOB=fr*l56#pQY^pb^Y`;3bVsQ_5!1@xHyNvXu)Dkn{sz#q*Mlh50}`o_1;Q^P?WglS^$K` z?dnp<{;QXUMEn=;_ACY@z=o`mQhdy(8-F^q+I9U$9aNmbjg~VHG^Y=z<)z%!JMNA4 z*VJ`v(b?NCROy*%QxNgLf?wYJ8pHU)b7Q@!wt~d4L#R=6fGs8Bvc#M=Cu#LS;$Sj$1Qtm(m=C^^*?UTOCvww;LEW#)*e& z{sZma#%S2Y>#ut(p0NKPi5p2otP_TGy^>{{m3bTE)hMR6dmS^ge}U-Ym`?|N-uRmE z1uOTk;?f+x*Ql`)jQNKvo@0Y<@ayS^mLdTVFdek-9=3d66{w6lwU5|+(T@Kik#GD3 z7hRQ(UKeX#dBI^EkHQ-lkXxNDg|ZU(9yD&*R6s%_cqcvDo_w9SRB)1fDSH7})~n1S z(9395_%P+fP=vp+rmb}F9FL2_Fm@|S; zm<&9LdlT+zDP({JM5EmZcmMh(l&KCV0Xo>f60mX{1^qH^riJ6I0*o=kDkC?e|ZILL&L@>pEE6(>HwS?&q&ukCL;)?hbdH*+J(FZ`c|vfRpEj0(SMtvwDf2g0xV7KTS`P4z3& z1&OW0BkzTFKeTs*O~hrvhrNnudest5k3rdhSyTqv(j4h2YqGq|ogHrQ-1M|5FoM}a zzAllCmt9*QEK2W}^)*38rB$wi9(((>@wOZak?VGFbEZ=-8eX@6F~5ZI>K;!>qIl*l zKPnpEez?SL$Fwm%txIsfAiO@5Bg&X%E>`mkvA`k)`n=EH6UvcW*swr;jJ=b)lAtpu z8Jgc12xYQKfb@jJQ;u%?j-x}q?qgJ!N~RXVNN+Ic@{lmrIjz#Si)nYT@&3j{_0qIE z5p>B?p$=ZonxMnA!SKI0w;Jm)4W`Z>WxKk0knRh}MZWyZyWS{WUZWfqr6Wfd1Kp0; zLdx(9Bwem|W5NE<-U}^VsXj=e2Sd{}DrAq7OFhxK%YAuD8ZjNpf8>dLW@M$}vQ!J% zBKH)uog&8QUyqL1Pxe6|{A5!_-uuI4sTdwxlP!=)B7xmLA&4T-*3oxHud2K%A;Rzq zM}j(dX7ZThXjxC%g(L>$zwK8LDYtSKpNhb5(>8J1saiEEdFq zQ#5?t?`FdOd+d~gdllcpIe9wFUR_>Ip%A0jUVCw#2vHkuKL4HxQtfHZm4h`>1|Iii zrz>(cTzzb?B6^NqeH-;(pf}cczK(ULq>zV6KKaCZAPkXy@I~;9mqy;?(<7Cq7IS%a zN$|yc5p`F>)NI~hw7nFGN(?j^@XbScH z@70)Ts7n2xHDWg9*GvyOTO#r-kEf6==rXpwRbkc|0TB?Qbb=NGB3f)JFyV+$!W$mh+9zep467|g zErO5@;q4nl`61Z@{*=RyKcj^Jv4Frz%+@A6xjxwGMdQciS&4|9i!^r%_RT zCfah4Z^j;g#V67^e7lS@)d%1*{v_#FC@@rg9##AjgDP_da?9{@HN^?~&LMYcr7%DkuUBn=>Ba!HrtwBJAO{z=+ z^1n{Un^VWgthF?{4WZ{!j2%E_+amsEwTcX8$2vGR5}d2Q5q zAOqF-JQqj}X;Bv}+;Z?depY#9i28VeYMGp_X#H(B7vRgv^wS(8fIp-=Qh!{`QeD$} zrq_UZbeMS>^zGPFdV6~QBvnZ;5+QJa{X6k8otAHbfVIOE@Va!~EgJsmyp(e-?VGNh z33kyj<%IcV1ijw_p+V*d$E~*nNy!Z(A@awa3#e6Ldg zDz)!iaQ^c%*GlnibkqK=?& zF+xe(I#Su(Zpz&&e;qyEV&O{3R5q%TL64H+=Q*qgqnL64G z>m2&8$P{TIFk{BF>?=yXc)ucZu**wRW#Ej%@qxHO-gcwv}Mfs z-2y8ClXJVruN4UEP|=sUg?BPON{Ih<k!Rsf=?0TYW*}u>)*ghGUr`Kn{kveG{%BBLt*|94y3>`Io|Fqbo zxvu4Z0?RV~+=~yTPvZfneHd!+lmK*q&M^YV+$l@d8T3qkweP=KQWHGgzq>*QMz_oR z`Y4{b)EwFjV_eAKA~c8xB|j4K%6IU-=`VzEi(S29b-LrhASe0DN-)9?Kwq_k)pF8u zv5)OL1x*nDQBg+0r|iiiVXat-A2G!;8ohu}x#zvOphAp-ZzT@Cg&iPI)h}2&@+eqU zag}u8-GYV)kXkYq4UAnew9>TPp!{72tRFCQ01sfXZy__kIUX6WMo(PfCkc|%Sy|`y zEJgqc_`d)#2G02zDZZ#@rM}0vkt}NO2IJHV^Y$_}+`LeET16Ir6@g(p9UkBSckB^> zVW2o0-1$y`z3+)LBd=s5MQ3*57b&0*g`s?fs$dAlg`Qh#jK$!F4E&V3A}sdVykBfuPVn+thXk#l`8{8IfWJ52J~l_@4Qlk&SmOHT8-) zzn;4wc8j$^`C&!`GYPr14JNOH5UgIt zw6}6BB@eF|tIqujs=#ZoiFY+0gzm4PDJ}&&3c~&#eSo$=(^8fB!(aaEuat|w%U$2` z-^zVcxAmbLD~8%GYq~#EA+}xf-Pd1j_?p1v>*1g{=b&Js3;sRze^A)t&A7XkOW|I&yK_O zc>o&Y9e+l7WUre|EnS*wGuNSMtEk*YWX>vLywKr6xX?cM!Shy|l%AX7(CH1{vt#);ja&T%!-|@I}{{Kab|m9Q=%{On9hQV6|+$S}V6?kqRnSJR{Zw zkVFmxVk8Kn*$(o-X^BH=PcL!Uq+qKYXUl!CZtQkDMH;LA*`97{cK|d`L(AAO$) z42uISv!6%vXKtI|b9kBdbf3cs;wQ@1pkc{2f&S<5NH@8mn|+VtaK5mY0b;(%%LMWJ zn}OqH66oLSWk3}-d6^)7e-SudCV_s`%TT!jmf6pv`7^f#{TyEA6pAYnWE;d!l&wL- zl5GP0&*NoIp&s8)FgN=igN9!)U)UqvJ8#P6?g$IB$pk9rwO z6tK*G9?hRlEzjX)s>l1hOb|a&wgwGLwh8n{d+yqO>XFB-{Ux(-d={U`#38t=&Bdt*aMEUOHqy*g*mDq0f)=Dw*u6e zNX_3z>$hEhczx^+U!kgz{}R2l@s=FDB{^1d2m2{K+Nn|WC4_%bg7B~UA5wA$|3<%a zNQ*9kFe*wAMpZXLN>UlMRFm41Z19AhB$h9N1a9w1WMbs$Q%=dzTasfXU!=#`{mkxN zcAxrEJxL3~?nzDwB38R6!IEe0Nt(3XlbjNSQM)I>lD<93o-s#yk`12Flf)D~NZ|IK zbjp1_$tgK{OLDB_i}a*ZZuJ?2eYj*=%H_B&P%spxu*T zN#CAi&k7?w$p%m8Nn#orByf9AB5}K?CpjfYZ%K}oe371{E3|u(Q-ZK>_as>IWqOjX z(e6`D3BteK&tSLlG(w=?%o4>* noLq`#81u^jJ#>nxb)%^@?x&;sDK|c)>FNIg`;n-?;OzkbfO<>| literal 0 HcmV?d00001 diff --git a/webserver/css/css b/webserver/css/css deleted file mode 120000 index edb244c..0000000 --- a/webserver/css/css +++ /dev/null @@ -1 +0,0 @@ -../../../sharpkey/webserver/css \ No newline at end of file diff --git a/webserver/css/jquery.edittable.min.css b/webserver/css/jquery.edittable.min.css deleted file mode 120000 index cdf68cf..0000000 --- a/webserver/css/jquery.edittable.min.css +++ /dev/null @@ -1 +0,0 @@ -../../../sharpkey/webserver/css/jquery.edittable.min.css \ No newline at end of file diff --git a/webserver/css/jquery.edittable.min.css b/webserver/css/jquery.edittable.min.css new file mode 100644 index 0000000..b77cb44 --- /dev/null +++ b/webserver/css/jquery.edittable.min.css @@ -0,0 +1,18 @@ +table.inputtable{width:100%;border:1px solid #ddd;border-collapse:collapse;border-spacing:0;-moz-box-shadow:0 1px 3px rgba(0,0,0,.075);-webkit-box-shadow:0 1px 3px rgba(0,0,0,.075);box-shadow:0 1px 3px rgba(0,0,0,.075);margin:15px 0} +table.inputtable a.icon-button{background-color:#ccc;display:inline-block;width:18px;height:18px;text-decoration:none;color:#fff;font-weight:800;line-height:16px;text-align:center;font-size:14px;-moz-border-radius:1px;-webkit-border-radius:1px;border-radius:1px;-moz-box-shadow:0 0 1px rgba(0,0,0,0.2);-webkit-box-shadow:0 0 1px rgba(0,0,0,0.2);box-shadow:0 0 1px rgba(0,0,0,0.2)} +table.inputtable a.icon-button.addcol,table.inputtable a.icon-button.addrow{background-color:#81b71a} +table.inputtable a.icon-button.delcol,table.inputtable a.icon-button.delrow{background-color:#db4a39} +table.inputtable a.icon-button.disabled{background-color:#eee} +table.inputtable td:last-child,table.inputtable th:last-child{width:54px;border:1px solid #eee;border-right:thick;} +table.inputtable td,table.inputtable th{border:1px solid #eee;text-align:center;height:40px;vertical-align:middle;font-size:14px} +table.inputtable th{background-color:#f1f1f1;border-top:none;border-bottom:2px solid #ddd;border-color:#ddd} +table.inputtable td input[type=text]{border:0;width:90%;height:100%;text-align:center;padding:0 5%} +table.inputtable tr td input:focus{background-color:#fafafa} +table.inputtable.wh tbody tr:nth-child(1),table.inputtable.wh tbody tr:nth-child(1) input{background-color:#fdfdfd;font-weight:800} +table.inputtable th:first-child,table.inputtable td:first-child{border-left:none} +table.inputtable tr:last-child td{border-bottom:none} +@media only screen and max-width 480px { +table.inputtable td,table.inputtable th{min-width:40px;height:80px} +table.inputtable a.icon-button{width:40px;height:40px;font-size:18px;min-width:40px;line-height:40px;margin:3px 0} +table.inputtable td input{height:80px} +} diff --git a/webserver/css/sb-admin.css b/webserver/css/sb-admin.css deleted file mode 120000 index bdce139..0000000 --- a/webserver/css/sb-admin.css +++ /dev/null @@ -1 +0,0 @@ -../../../sharpkey/webserver/css/sb-admin.css \ No newline at end of file diff --git a/webserver/css/sb-admin.css b/webserver/css/sb-admin.css new file mode 100644 index 0000000..f371f07 --- /dev/null +++ b/webserver/css/sb-admin.css @@ -0,0 +1,159 @@ +/* +Author: Start Bootstrap - http://startbootstrap.com +'SB Admin' HTML Template by Start Bootstrap + +All Start Bootstrap themes are licensed under Apache 2.0. +For more info and more free Bootstrap 3 HTML themes, visit http://startbootstrap.com! +*/ + +/* ATTN: This is mobile first CSS - to update 786px and up screen width use the media query near the bottom of the document! */ + +/* Global Styles */ + +body { + margin-top: 50px; +} + +#wrapper { + padding-left: 0; +} + +#page-wrapper { + width: 100%; + padding: 5px 15px; +} + +/* Nav Messages */ + +.messages-dropdown .dropdown-menu .message-preview .avatar, +.messages-dropdown .dropdown-menu .message-preview .name, +.messages-dropdown .dropdown-menu .message-preview .message, +.messages-dropdown .dropdown-menu .message-preview .time { + display: block; +} + +.messages-dropdown .dropdown-menu .message-preview .avatar { + float: left; + margin-right: 15px; +} + +.messages-dropdown .dropdown-menu .message-preview .name { + font-weight: bold; +} + +.messages-dropdown .dropdown-menu .message-preview .message { + font-size: 12px; +} + +.messages-dropdown .dropdown-menu .message-preview .time { + font-size: 12px; +} + + +/* Nav Announcements */ + +.announcement-heading { + font-size: 50px; + margin: 0; +} + +.announcement-text { + margin: 0; +} + +/* Table Headers */ + +table.tablesorter thead { + cursor: pointer; +} + +table.tablesorter thead tr th:hover { + background-color: #f5f5f5; +} + +/* Flot Chart Containers */ + +.flot-chart { + display: block; + height: 400px; +} + +.flot-chart-content { + width: 100%; + height: 100%; +} + +/* Edit Below to Customize Widths > 768px */ +@media (min-width:768px) { + + /* Wrappers */ + + #wrapper { + padding-left: 225px; + } + + #page-wrapper { + padding: 15px 25px; + } + + /* Side Nav */ + + .side-nav { + margin-left: -225px; + left: 225px; + width: 225px; + position: fixed; + top: 50px; + height: 100%; + border-radius: 0; + border: none; + background-color: #222222; + overflow-y: auto; + } + + /* Bootstrap Default Overrides - Customized Dropdowns for the Side Nav */ + + .side-nav>li.dropdown>ul.dropdown-menu { + position: relative; + min-width: 225px; + margin: 0; + padding: 0; + border: none; + border-radius: 0; + background-color: transparent; + box-shadow: none; + -webkit-box-shadow: none; + } + + .side-nav>li.dropdown>ul.dropdown-menu>li>a { + color: #999999; + padding: 15px 15px 15px 25px; + } + + .side-nav>li.dropdown>ul.dropdown-menu>li>a:hover, + .side-nav>li.dropdown>ul.dropdown-menu>li>a.active, + .side-nav>li.dropdown>ul.dropdown-menu>li>a:focus { + color: #fff; + background-color: #080808; + } + + .side-nav>li>a { + width: 225px; + } + + .navbar-inverse .navbar-nav>li>a:hover, + .navbar-inverse .navbar-nav>li>a:focus { + background-color: #080808; + } + + /* Nav Messages */ + + .messages-dropdown .dropdown-menu { + min-width: 300px; + } + + .messages-dropdown .dropdown-menu li a { + white-space: normal; + } + +} diff --git a/webserver/css/sharpkey.css b/webserver/css/sharpkey.css deleted file mode 120000 index 331d03e..0000000 --- a/webserver/css/sharpkey.css +++ /dev/null @@ -1 +0,0 @@ -../../../sharpkey/webserver/css/sharpkey.css \ No newline at end of file diff --git a/webserver/css/sharpkey.css b/webserver/css/sharpkey.css new file mode 100644 index 0000000..9bd439e --- /dev/null +++ b/webserver/css/sharpkey.css @@ -0,0 +1,266 @@ +.wm-button { + width: 95px; + height: 25px; + background: blue; + border: none; + vertical-align: top; + margin-left: 25px; + cursor: pointer; + color: white; + transition: 2s; + border-radius: 12px; +} + +.wm-button:disabled { + background: #999; + color: #555; + cursor: not-allowed; +} + +.wm-button:hover { + background-color: #81ecec; + border: 2px solid #00cec9; +} + +input[type="file"] { + display: none; +} + +.firmware-file-upload { + width: 95px; + height: 25px; + background: blue; + border: none; + vertical-align: middle; + text-align: center; + line-height: 25px; + margin-left: 35px; + margin-bottom: 0px; + cursor: pointer; + color: white; + transition: 2s; + border-radius: 12px; +} + +.firmware-file-upload:hover { + background-color: #81ecec; + border: 2px solid #00cec9; +} + + +.keymap-file-upload { + width: 95px; + height: 25px; + background: blue; + border: none; + vertical-align: middle; + text-align: center; + line-height: 25px; + margin-left: 35px; + margin-bottom: 0px; + cursor: pointer; + color: white; + transition: 2s; + border-radius: 12px; +} + +.keymap-file-upload:hover { + background-color: #81ecec; + border: 2px solid #00cec9; +} + +input[type=file]::file-selector-button { + border: 2px solid #6c5ce7; + padding: .2em .4em; + border-radius: .2em; + background-color: #a29bfe; + transition: 1s; +} + +input[type=file]::file-selector-button:hover { + background-color: #81ecec; + border: 2px solid #00cec9; +} + +.sk-modules-table { + border: none; + border-collapse: collapse; +} +.sk-modules-table caption { + padding-bottom: 0.5em; +} +.sk-modules-table th, .sk-modules-table td { + border: none; + padding: 0.2rem 2rem; +} +.sk-modules-table td { + white-space: nowrap; +} +.sk-modules-table th { + font-weight: normal; +} +.sk-modules-table td { + border-style: none; + vertical-align: top; +} +.sk-modules-table th { + padding: 0.2em; + vertical-align: middle; + text-align: center; +} + +.sk-modules-table tbody td:first-child::after { + content: leader(". "); +} + +.sk-client-wifi-config-table { + border: none; + border-collapse: collapse; + width: 1px !important; + table-layout: auto !important; +} +.sk-client-wifi-config-table caption { + padding-bottom: 0.5em; +} +.sk-client-wifi-config-table th, .sk-client-wifi-config-table td { + border: none; + padding: 0.2rem 2rem; + padding-left: 0; + padding-right: 4rem; +} +.sk-client-wifi-config-table td { + white-space: nowrap; + overflow: hidden; + border-style: none; + vertical-align: top; + width: auto !important; +} +.sk-client-wifi-config-table th { + font-weight: normal; + padding: 0.2em; + vertical-align: middle; + text-align: center; + width: auto !important; +} + +#client-wifi-config-area { + overflow-x: scroll; + width: fit-content; +} + +#firmware-revision-area { + overflow-x: scroll; + width: fit-content; +} + +#esp32-partitions-area { + overflow-x: scroll; + width: fit-content; +} + +.sk-partitions-table { + border: none; + border-collapse: collapse; +} +.sk-partitions-table caption { + padding-bottom: 0.5em; +} +.sk-partitions-table th, .sk-partitions-table td { + border: none; + padding: 0.2rem 2rem; +} +.sk-partitions-table td { + white-space: nowrap; + border-style: none; + vertical-align: top; +} +.sk-partitions-table th { + font-weight: normal; + padding: 0.2em; + vertical-align: middle; + text-align: center; +} + +.sk-partitions-table tbody td:first-child::after { + content: leader(". "); +} + +.sk-partitions-table tbody td:nth-child(4) { + text-align: right; +} +.sk-partitions-table tbody td:nth-child(5) { + text-align: right; +} +.sk-partitions-table tbody td:nth-child(6) { + text-align: center; +} +.sk-partitions-table tbody td:nth-child(8) { + text-align: center; +} + +.table-condensed .progress { + margin-bottom: 0 !important; + width: 400px; + display: none; +} + +.table-borderless > tbody > tr > td, +.table-borderless > thead > tr > td, +.table-borderless { + border-bottom: 0; + border-top: 0; + padding: 4px; + padding-top: 2px; + padding-bottom: 0; + white-space: nowrap; +} + +.table-responsive { + border-bottom: 0; + border-top: 0; + border-left: 0; + border-right: 0; + padding: 4px; + padding-top: 2px; + padding-bottom: 0; + white-space: nowrap; + table-layout: auto; +} + +.table { + margin-bottom: 10px; +} + +.justify { + text-align: justify; + text-justify: inter-word; +} + +.keymap { + max-height: 36.2em; + overflow: auto; +} + +.hr_no_margin { + margin-top: 0.2em; + margin-bottom: 0.2em; +} + +fieldset { + overflow: hidden; +} +.radio-mouse { + padding-right: 1em; +} +.radio-mouse label { + float: left; + clear: none; + display: block; + padding: 0px 1em 0px 8px; +} +input[type=radio] .radio-mouse, +input.radio .radio-mouse { + float: left; + clear: none; + margin: 2px 0 0 2px; +} diff --git a/webserver/css/style.css b/webserver/css/style.css deleted file mode 120000 index 4d7d9e2..0000000 --- a/webserver/css/style.css +++ /dev/null @@ -1 +0,0 @@ -../../../sharpkey/webserver/css/style.css \ No newline at end of file diff --git a/webserver/css/style.css b/webserver/css/style.css new file mode 100644 index 0000000..11e8166 --- /dev/null +++ b/webserver/css/style.css @@ -0,0 +1,253 @@ +/* +body { + margin: 0; +} +*/ + +/* +h1 { + text-align: center; + font-family: Tahoma, Arial, sans-serif; + color: #06D85F; + margin: 10px; +} + +*/ +.box { + width: 40%; + margin: 0 auto; + background: rgba(255,255,255,0.2); + padding: 35px; + border: 2px solid #fff; + border-radius: 20px/50px; + background-clip: padding-box; + text-align: center; +} + +.overlay { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + background: rgba(0, 0, 0, 0.7); + //transition: opacity 500ms; + visibility: hidden; + opacity: 0; +} +.overlay:target { + visibility: visible; + opacity: 1; +} + +.popup { + margin: 70px auto; + padding: 20px; + background: #fff; + border-radius: 5px; + width: 30%; + position: relative; + transition: all 5s ease-in-out; +} + +.popup h2 { + margin-top: 0; + color: #333; + font-family: Tahoma, Arial, sans-serif; +} +.popup .close { + position: absolute; + top: 20px; + right: 30px; + transition: all 200ms; + font-size: 30px; + font-weight: bold; + text-decoration: none; + color: #333; +} +.popup .close:hover { + color: #06D85F; +} +.popup .content { + max-height: 30%; + overflow: auto; +} + +@media screen and (max-width: 700px){ + .box{ + width: 90%; + } + .popup{ + width: 90%; + } +} + +/* +html { + font-family: Arial, Helvetica, sans-serif; + display: inline-block; + text-align: center; +} + +h1 { + font-size: 1.8rem; + color: white; +} + +p { + font-size: 1.1rem; +} +*/ + +.topnav { + overflow: hidden; + background-color: #0A1128; +} + +.content { + padding: 5%; +} + +.card-grid { + max-width: 800px; + margin: 0 auto; + display: grid; + grid-gap: 2rem; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); +} + +.card { + background-color: lightyellow; + box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5); +} + +.card-title { + font-size: 1.1rem; + font-weight: bold; + color: #034078 +} +/* +input[type=submit] { + border: none; + color: #FEFCFB; + background-color: #034078; + padding: 15px 15px; + text-align: center; + text-decoration: none; + display: inline-block; + font-size: 18px; + width: 30%; + margin-right: 10px; + border-radius: 4px; + transition-duration: 0.4s; + } + +input[type=submit]:hover { + background-color: #1282A2; +} + +input[type=text], input[type=number], select { + width: 40%; + padding: 8px 10px; + margin: 12px; + display: inline-block; + border: 1px solid #ccc; + border-radius: 4px; + box-sizing: border-box; +} + +input[type=url], input[type=number], select { + width: 50%; + padding: 12px 20px; + margin: 18px; + display: inline-block; + border: 1px solid #ccc; + border-radius: 4px; + box-sizing: border-box; +} + +label { + font-size: 1rem; +} +*/ +.value{ + font-size: 1rem; + color: #1282A2; +} +.state { + font-size: 1rem; + color: #1282A2; +} + +.button { + font-size: 2em; + padding: 3px; + color: #dbd75e; + border: 1px solid #000; + border-radius: 5px; + text-decoration: none; + cursor: pointer; + transition: all 1s ease-out; + + background: #1b06d8; +} +.button:hover { + background: #06D85F; +} + +/* +button { + //border: none; + // color: #FEFCFB; + // padding: 15px 32px; + // text-align: center; + // font-size: 30px; + // width: 100px; + // border-radius: 4px; + transition-duration: 0.4s; +} +.button-on { + // background-color: #034078; +} +.button-on:hover { + // background-color: #1282A2; +} +.button-off { + // background-color: #858585; +} +.button-off:hover { + // background-color: #252524; +} +*/ +#sk-modules-table { + border: solid thin; + border-collapse: collapse; +} +#sk-modules-table caption { + padding-bottom: 0.5em; +} +#sk-modules-table th, +#sk-modules-table td { + border: solid thin; + padding: 0.5rem 2rem; +} +#sk-modules-table td { + white-space: nowrap; +} +#sk-modules-table th { + font-weight: normal; +} +#sk-modules-table td { + border-style: none solid; + vertical-align: top; +} +#sk-modules-table th { + padding: 0.2em; + vertical-align: middle; + text-align: center; +} + +#sk-modules-table tbody td:first-child::after { + content: leader(". "); +} + diff --git a/webserver/css/styles.css b/webserver/css/styles.css deleted file mode 120000 index aec5f86..0000000 --- a/webserver/css/styles.css +++ /dev/null @@ -1 +0,0 @@ -../../../sharpkey/webserver/css/styles.css \ No newline at end of file diff --git a/webserver/css/styles.css b/webserver/css/styles.css new file mode 100644 index 0000000..e362361 --- /dev/null +++ b/webserver/css/styles.css @@ -0,0 +1 @@ +body { padding-bottom: 70px; } diff --git a/webserver/favicon.ico b/webserver/favicon.ico deleted file mode 120000 index 6830c13..0000000 --- a/webserver/favicon.ico +++ /dev/null @@ -1 +0,0 @@ -../../sharpkey/webserver/favicon.ico \ No newline at end of file diff --git a/webserver/favicon.ico b/webserver/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..0babb7db78629851b5be89be7f7ef3b2d3a4ed5e GIT binary patch literal 4286 zcmeHLc~F#P7+Z%vQiMi0|5_2<4{CIEJP8O z<&sO~bW}u8Sgr&V1s2hTb$5XU1{c`O+o3+{;(~l-lQBjFr-~jJCF0Kcut*z~_pzE$g ztYbq%Q&UoM3U@IkmW#d^!(G0T)S4sBt!rs%$%fqYR{9g|+( zK5bQ1k0S&fVoU%V%-1V3>vsC3%ZXJ{(HHB%(UhE$)`%w)c56U0?vqKxa=r|sk*2h0 zu_ZY{jNNoRZoqa4{Jg7G5W-tgNW0kVQpAM%S^J?RGn9_g-i6 z_498QhlR_|UkFiIS}*5$#*Ui+YEl6{D_4Kd>$cn3>q0`rRq^qc8iLLR>yI4uq*ZIy zk%{Rz-u}VR5hM zt=M|l{8z7Chaueo`aF3ePnx%IF@=PNHDNrsga1H`x3;#nre@sAZ1D3B;4XxS`S_4f z3C*0nfY=;SdwQ(lmK4-Qhq! zzJ3~=PWP=4D@gd5pdQXk!Xx<{+1XP1`WtIvdop;~NZPphM@mXg}kV;hYy=KZ2Ex)4Kb#v)6MBhViNmi zI|RPJld!*y+VnZ@b5g%!=PpW2Os1go!L0UTVVfCv_8iZn(P+wqc<2XKS67#%q+R9M zJ`5c`iiR4QkmGJAijIjRi6nxWo107EH(ZG6Ov2|qg;E)E%0GZ^X58Z3+>g>WTYE}R z&$tbnRbbG0o?or5l?geZpQL!Il*h#x+Dmo45Q#IClz+%>|nvp<{CijERls zdGhlMaE8{?koLW1`<|GT!s+#TR)76F#4XG(C{$ZmE#u=hY}!IK3dLi=2eO|`CX;?K zV>Y4o>Mxp}Rn4<0ta zf3(2eQR3{l9XRC9v05~rJ)h7k8{sfR&&jx#)h;PrK_YgSPoRoL(7K>q94vYp~YhTm+r7KqP zXV!A*3aIHGpq^|tozMAl|D9wujaYBQS)d!~H?>$pR!)wT@4u;OSKB(SPb`+G(Sz9p z0rH&Oy!+^J64>Sn{@s=6#f8I+M-$eEaC()+K@MQOAM5b!Zl0d{fh+z?^Ev$a#{2ko z{*QM$?8cC`_B3yIgKL6t53+qgziWYx@7uk_(N!L; zR$I#cAGKoTT2_ZAyYlaCJJvf2C li { + position: relative; +} +.fa-li { + position: absolute; + left: -2.14285714em; + width: 2.14285714em; + top: 0.14285714em; + text-align: center; +} +.fa-li.fa-lg { + left: -1.85714286em; +} +.fa-border { + padding: .2em .25em .15em; + border: solid 0.08em #eeeeee; + border-radius: .1em; +} +.fa-pull-left { + float: left; +} +.fa-pull-right { + float: right; +} +.fa.fa-pull-left { + margin-right: .3em; +} +.fa.fa-pull-right { + margin-left: .3em; +} +/* Deprecated as of 4.4.0 */ +.pull-right { + float: right; +} +.pull-left { + float: left; +} +.fa.pull-left { + margin-right: .3em; +} +.fa.pull-right { + margin-left: .3em; +} +.fa-spin { + -webkit-animation: fa-spin 2s infinite linear; + animation: fa-spin 2s infinite linear; +} +.fa-pulse { + -webkit-animation: fa-spin 1s infinite steps(8); + animation: fa-spin 1s infinite steps(8); +} +@-webkit-keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +@keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +.fa-rotate-90 { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=1)"; + -webkit-transform: rotate(90deg); + -ms-transform: rotate(90deg); + transform: rotate(90deg); +} +.fa-rotate-180 { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2)"; + -webkit-transform: rotate(180deg); + -ms-transform: rotate(180deg); + transform: rotate(180deg); +} +.fa-rotate-270 { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=3)"; + -webkit-transform: rotate(270deg); + -ms-transform: rotate(270deg); + transform: rotate(270deg); +} +.fa-flip-horizontal { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)"; + -webkit-transform: scale(-1, 1); + -ms-transform: scale(-1, 1); + transform: scale(-1, 1); +} +.fa-flip-vertical { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"; + -webkit-transform: scale(1, -1); + -ms-transform: scale(1, -1); + transform: scale(1, -1); +} +:root .fa-rotate-90, +:root .fa-rotate-180, +:root .fa-rotate-270, +:root .fa-flip-horizontal, +:root .fa-flip-vertical { + filter: none; +} +.fa-stack { + position: relative; + display: inline-block; + width: 2em; + height: 2em; + line-height: 2em; + vertical-align: middle; +} +.fa-stack-1x, +.fa-stack-2x { + position: absolute; + left: 0; + width: 100%; + text-align: center; +} +.fa-stack-1x { + line-height: inherit; +} +.fa-stack-2x { + font-size: 2em; +} +.fa-inverse { + color: #ffffff; +} +/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen + readers do not read off random characters that represent icons */ +.fa-glass:before { + content: "\f000"; +} +.fa-music:before { + content: "\f001"; +} +.fa-search:before { + content: "\f002"; +} +.fa-envelope-o:before { + content: "\f003"; +} +.fa-heart:before { + content: "\f004"; +} +.fa-star:before { + content: "\f005"; +} +.fa-star-o:before { + content: "\f006"; +} +.fa-user:before { + content: "\f007"; +} +.fa-film:before { + content: "\f008"; +} +.fa-th-large:before { + content: "\f009"; +} +.fa-th:before { + content: "\f00a"; +} +.fa-th-list:before { + content: "\f00b"; +} +.fa-check:before { + content: "\f00c"; +} +.fa-remove:before, +.fa-close:before, +.fa-times:before { + content: "\f00d"; +} +.fa-search-plus:before { + content: "\f00e"; +} +.fa-search-minus:before { + content: "\f010"; +} +.fa-power-off:before { + content: "\f011"; +} +.fa-signal:before { + content: "\f012"; +} +.fa-gear:before, +.fa-cog:before { + content: "\f013"; +} +.fa-trash-o:before { + content: "\f014"; +} +.fa-home:before { + content: "\f015"; +} +.fa-file-o:before { + content: "\f016"; +} +.fa-clock-o:before { + content: "\f017"; +} +.fa-road:before { + content: "\f018"; +} +.fa-download:before { + content: "\f019"; +} +.fa-arrow-circle-o-down:before { + content: "\f01a"; +} +.fa-arrow-circle-o-up:before { + content: "\f01b"; +} +.fa-inbox:before { + content: "\f01c"; +} +.fa-play-circle-o:before { + content: "\f01d"; +} +.fa-rotate-right:before, +.fa-repeat:before { + content: "\f01e"; +} +.fa-refresh:before { + content: "\f021"; +} +.fa-list-alt:before { + content: "\f022"; +} +.fa-lock:before { + content: "\f023"; +} +.fa-flag:before { + content: "\f024"; +} +.fa-headphones:before { + content: "\f025"; +} +.fa-volume-off:before { + content: "\f026"; +} +.fa-volume-down:before { + content: "\f027"; +} +.fa-volume-up:before { + content: "\f028"; +} +.fa-qrcode:before { + content: "\f029"; +} +.fa-barcode:before { + content: "\f02a"; +} +.fa-tag:before { + content: "\f02b"; +} +.fa-tags:before { + content: "\f02c"; +} +.fa-book:before { + content: "\f02d"; +} +.fa-bookmark:before { + content: "\f02e"; +} +.fa-print:before { + content: "\f02f"; +} +.fa-camera:before { + content: "\f030"; +} +.fa-font:before { + content: "\f031"; +} +.fa-bold:before { + content: "\f032"; +} +.fa-italic:before { + content: "\f033"; +} +.fa-text-height:before { + content: "\f034"; +} +.fa-text-width:before { + content: "\f035"; +} +.fa-align-left:before { + content: "\f036"; +} +.fa-align-center:before { + content: "\f037"; +} +.fa-align-right:before { + content: "\f038"; +} +.fa-align-justify:before { + content: "\f039"; +} +.fa-list:before { + content: "\f03a"; +} +.fa-dedent:before, +.fa-outdent:before { + content: "\f03b"; +} +.fa-indent:before { + content: "\f03c"; +} +.fa-video-camera:before { + content: "\f03d"; +} +.fa-photo:before, +.fa-image:before, +.fa-picture-o:before { + content: "\f03e"; +} +.fa-pencil:before { + content: "\f040"; +} +.fa-map-marker:before { + content: "\f041"; +} +.fa-adjust:before { + content: "\f042"; +} +.fa-tint:before { + content: "\f043"; +} +.fa-edit:before, +.fa-pencil-square-o:before { + content: "\f044"; +} +.fa-share-square-o:before { + content: "\f045"; +} +.fa-check-square-o:before { + content: "\f046"; +} +.fa-arrows:before { + content: "\f047"; +} +.fa-step-backward:before { + content: "\f048"; +} +.fa-fast-backward:before { + content: "\f049"; +} +.fa-backward:before { + content: "\f04a"; +} +.fa-play:before { + content: "\f04b"; +} +.fa-pause:before { + content: "\f04c"; +} +.fa-stop:before { + content: "\f04d"; +} +.fa-forward:before { + content: "\f04e"; +} +.fa-fast-forward:before { + content: "\f050"; +} +.fa-step-forward:before { + content: "\f051"; +} +.fa-eject:before { + content: "\f052"; +} +.fa-chevron-left:before { + content: "\f053"; +} +.fa-chevron-right:before { + content: "\f054"; +} +.fa-plus-circle:before { + content: "\f055"; +} +.fa-minus-circle:before { + content: "\f056"; +} +.fa-times-circle:before { + content: "\f057"; +} +.fa-check-circle:before { + content: "\f058"; +} +.fa-question-circle:before { + content: "\f059"; +} +.fa-info-circle:before { + content: "\f05a"; +} +.fa-crosshairs:before { + content: "\f05b"; +} +.fa-times-circle-o:before { + content: "\f05c"; +} +.fa-check-circle-o:before { + content: "\f05d"; +} +.fa-ban:before { + content: "\f05e"; +} +.fa-arrow-left:before { + content: "\f060"; +} +.fa-arrow-right:before { + content: "\f061"; +} +.fa-arrow-up:before { + content: "\f062"; +} +.fa-arrow-down:before { + content: "\f063"; +} +.fa-mail-forward:before, +.fa-share:before { + content: "\f064"; +} +.fa-expand:before { + content: "\f065"; +} +.fa-compress:before { + content: "\f066"; +} +.fa-plus:before { + content: "\f067"; +} +.fa-minus:before { + content: "\f068"; +} +.fa-asterisk:before { + content: "\f069"; +} +.fa-exclamation-circle:before { + content: "\f06a"; +} +.fa-gift:before { + content: "\f06b"; +} +.fa-leaf:before { + content: "\f06c"; +} +.fa-fire:before { + content: "\f06d"; +} +.fa-eye:before { + content: "\f06e"; +} +.fa-eye-slash:before { + content: "\f070"; +} +.fa-warning:before, +.fa-exclamation-triangle:before { + content: "\f071"; +} +.fa-plane:before { + content: "\f072"; +} +.fa-calendar:before { + content: "\f073"; +} +.fa-random:before { + content: "\f074"; +} +.fa-comment:before { + content: "\f075"; +} +.fa-magnet:before { + content: "\f076"; +} +.fa-chevron-up:before { + content: "\f077"; +} +.fa-chevron-down:before { + content: "\f078"; +} +.fa-retweet:before { + content: "\f079"; +} +.fa-shopping-cart:before { + content: "\f07a"; +} +.fa-folder:before { + content: "\f07b"; +} +.fa-folder-open:before { + content: "\f07c"; +} +.fa-arrows-v:before { + content: "\f07d"; +} +.fa-arrows-h:before { + content: "\f07e"; +} +.fa-bar-chart-o:before, +.fa-bar-chart:before { + content: "\f080"; +} +.fa-twitter-square:before { + content: "\f081"; +} +.fa-facebook-square:before { + content: "\f082"; +} +.fa-camera-retro:before { + content: "\f083"; +} +.fa-key:before { + content: "\f084"; +} +.fa-gears:before, +.fa-cogs:before { + content: "\f085"; +} +.fa-comments:before { + content: "\f086"; +} +.fa-thumbs-o-up:before { + content: "\f087"; +} +.fa-thumbs-o-down:before { + content: "\f088"; +} +.fa-star-half:before { + content: "\f089"; +} +.fa-heart-o:before { + content: "\f08a"; +} +.fa-sign-out:before { + content: "\f08b"; +} +.fa-linkedin-square:before { + content: "\f08c"; +} +.fa-thumb-tack:before { + content: "\f08d"; +} +.fa-external-link:before { + content: "\f08e"; +} +.fa-sign-in:before { + content: "\f090"; +} +.fa-trophy:before { + content: "\f091"; +} +.fa-github-square:before { + content: "\f092"; +} +.fa-upload:before { + content: "\f093"; +} +.fa-lemon-o:before { + content: "\f094"; +} +.fa-phone:before { + content: "\f095"; +} +.fa-square-o:before { + content: "\f096"; +} +.fa-bookmark-o:before { + content: "\f097"; +} +.fa-phone-square:before { + content: "\f098"; +} +.fa-twitter:before { + content: "\f099"; +} +.fa-facebook-f:before, +.fa-facebook:before { + content: "\f09a"; +} +.fa-github:before { + content: "\f09b"; +} +.fa-unlock:before { + content: "\f09c"; +} +.fa-credit-card:before { + content: "\f09d"; +} +.fa-feed:before, +.fa-rss:before { + content: "\f09e"; +} +.fa-hdd-o:before { + content: "\f0a0"; +} +.fa-bullhorn:before { + content: "\f0a1"; +} +.fa-bell:before { + content: "\f0f3"; +} +.fa-certificate:before { + content: "\f0a3"; +} +.fa-hand-o-right:before { + content: "\f0a4"; +} +.fa-hand-o-left:before { + content: "\f0a5"; +} +.fa-hand-o-up:before { + content: "\f0a6"; +} +.fa-hand-o-down:before { + content: "\f0a7"; +} +.fa-arrow-circle-left:before { + content: "\f0a8"; +} +.fa-arrow-circle-right:before { + content: "\f0a9"; +} +.fa-arrow-circle-up:before { + content: "\f0aa"; +} +.fa-arrow-circle-down:before { + content: "\f0ab"; +} +.fa-globe:before { + content: "\f0ac"; +} +.fa-wrench:before { + content: "\f0ad"; +} +.fa-tasks:before { + content: "\f0ae"; +} +.fa-filter:before { + content: "\f0b0"; +} +.fa-briefcase:before { + content: "\f0b1"; +} +.fa-arrows-alt:before { + content: "\f0b2"; +} +.fa-group:before, +.fa-users:before { + content: "\f0c0"; +} +.fa-chain:before, +.fa-link:before { + content: "\f0c1"; +} +.fa-cloud:before { + content: "\f0c2"; +} +.fa-flask:before { + content: "\f0c3"; +} +.fa-cut:before, +.fa-scissors:before { + content: "\f0c4"; +} +.fa-copy:before, +.fa-files-o:before { + content: "\f0c5"; +} +.fa-paperclip:before { + content: "\f0c6"; +} +.fa-save:before, +.fa-floppy-o:before { + content: "\f0c7"; +} +.fa-square:before { + content: "\f0c8"; +} +.fa-navicon:before, +.fa-reorder:before, +.fa-bars:before { + content: "\f0c9"; +} +.fa-list-ul:before { + content: "\f0ca"; +} +.fa-list-ol:before { + content: "\f0cb"; +} +.fa-strikethrough:before { + content: "\f0cc"; +} +.fa-underline:before { + content: "\f0cd"; +} +.fa-table:before { + content: "\f0ce"; +} +.fa-magic:before { + content: "\f0d0"; +} +.fa-truck:before { + content: "\f0d1"; +} +.fa-pinterest:before { + content: "\f0d2"; +} +.fa-pinterest-square:before { + content: "\f0d3"; +} +.fa-google-plus-square:before { + content: "\f0d4"; +} +.fa-google-plus:before { + content: "\f0d5"; +} +.fa-money:before { + content: "\f0d6"; +} +.fa-caret-down:before { + content: "\f0d7"; +} +.fa-caret-up:before { + content: "\f0d8"; +} +.fa-caret-left:before { + content: "\f0d9"; +} +.fa-caret-right:before { + content: "\f0da"; +} +.fa-columns:before { + content: "\f0db"; +} +.fa-unsorted:before, +.fa-sort:before { + content: "\f0dc"; +} +.fa-sort-down:before, +.fa-sort-desc:before { + content: "\f0dd"; +} +.fa-sort-up:before, +.fa-sort-asc:before { + content: "\f0de"; +} +.fa-envelope:before { + content: "\f0e0"; +} +.fa-linkedin:before { + content: "\f0e1"; +} +.fa-rotate-left:before, +.fa-undo:before { + content: "\f0e2"; +} +.fa-legal:before, +.fa-gavel:before { + content: "\f0e3"; +} +.fa-dashboard:before, +.fa-tachometer:before { + content: "\f0e4"; +} +.fa-comment-o:before { + content: "\f0e5"; +} +.fa-comments-o:before { + content: "\f0e6"; +} +.fa-flash:before, +.fa-bolt:before { + content: "\f0e7"; +} +.fa-sitemap:before { + content: "\f0e8"; +} +.fa-umbrella:before { + content: "\f0e9"; +} +.fa-paste:before, +.fa-clipboard:before { + content: "\f0ea"; +} +.fa-lightbulb-o:before { + content: "\f0eb"; +} +.fa-exchange:before { + content: "\f0ec"; +} +.fa-cloud-download:before { + content: "\f0ed"; +} +.fa-cloud-upload:before { + content: "\f0ee"; +} +.fa-user-md:before { + content: "\f0f0"; +} +.fa-stethoscope:before { + content: "\f0f1"; +} +.fa-suitcase:before { + content: "\f0f2"; +} +.fa-bell-o:before { + content: "\f0a2"; +} +.fa-coffee:before { + content: "\f0f4"; +} +.fa-cutlery:before { + content: "\f0f5"; +} +.fa-file-text-o:before { + content: "\f0f6"; +} +.fa-building-o:before { + content: "\f0f7"; +} +.fa-hospital-o:before { + content: "\f0f8"; +} +.fa-ambulance:before { + content: "\f0f9"; +} +.fa-medkit:before { + content: "\f0fa"; +} +.fa-fighter-jet:before { + content: "\f0fb"; +} +.fa-beer:before { + content: "\f0fc"; +} +.fa-h-square:before { + content: "\f0fd"; +} +.fa-plus-square:before { + content: "\f0fe"; +} +.fa-angle-double-left:before { + content: "\f100"; +} +.fa-angle-double-right:before { + content: "\f101"; +} +.fa-angle-double-up:before { + content: "\f102"; +} +.fa-angle-double-down:before { + content: "\f103"; +} +.fa-angle-left:before { + content: "\f104"; +} +.fa-angle-right:before { + content: "\f105"; +} +.fa-angle-up:before { + content: "\f106"; +} +.fa-angle-down:before { + content: "\f107"; +} +.fa-desktop:before { + content: "\f108"; +} +.fa-laptop:before { + content: "\f109"; +} +.fa-tablet:before { + content: "\f10a"; +} +.fa-mobile-phone:before, +.fa-mobile:before { + content: "\f10b"; +} +.fa-circle-o:before { + content: "\f10c"; +} +.fa-quote-left:before { + content: "\f10d"; +} +.fa-quote-right:before { + content: "\f10e"; +} +.fa-spinner:before { + content: "\f110"; +} +.fa-circle:before { + content: "\f111"; +} +.fa-mail-reply:before, +.fa-reply:before { + content: "\f112"; +} +.fa-github-alt:before { + content: "\f113"; +} +.fa-folder-o:before { + content: "\f114"; +} +.fa-folder-open-o:before { + content: "\f115"; +} +.fa-smile-o:before { + content: "\f118"; +} +.fa-frown-o:before { + content: "\f119"; +} +.fa-meh-o:before { + content: "\f11a"; +} +.fa-gamepad:before { + content: "\f11b"; +} +.fa-keyboard-o:before { + content: "\f11c"; +} +.fa-flag-o:before { + content: "\f11d"; +} +.fa-flag-checkered:before { + content: "\f11e"; +} +.fa-terminal:before { + content: "\f120"; +} +.fa-code:before { + content: "\f121"; +} +.fa-mail-reply-all:before, +.fa-reply-all:before { + content: "\f122"; +} +.fa-star-half-empty:before, +.fa-star-half-full:before, +.fa-star-half-o:before { + content: "\f123"; +} +.fa-location-arrow:before { + content: "\f124"; +} +.fa-crop:before { + content: "\f125"; +} +.fa-code-fork:before { + content: "\f126"; +} +.fa-unlink:before, +.fa-chain-broken:before { + content: "\f127"; +} +.fa-question:before { + content: "\f128"; +} +.fa-info:before { + content: "\f129"; +} +.fa-exclamation:before { + content: "\f12a"; +} +.fa-superscript:before { + content: "\f12b"; +} +.fa-subscript:before { + content: "\f12c"; +} +.fa-eraser:before { + content: "\f12d"; +} +.fa-puzzle-piece:before { + content: "\f12e"; +} +.fa-microphone:before { + content: "\f130"; +} +.fa-microphone-slash:before { + content: "\f131"; +} +.fa-shield:before { + content: "\f132"; +} +.fa-calendar-o:before { + content: "\f133"; +} +.fa-fire-extinguisher:before { + content: "\f134"; +} +.fa-rocket:before { + content: "\f135"; +} +.fa-maxcdn:before { + content: "\f136"; +} +.fa-chevron-circle-left:before { + content: "\f137"; +} +.fa-chevron-circle-right:before { + content: "\f138"; +} +.fa-chevron-circle-up:before { + content: "\f139"; +} +.fa-chevron-circle-down:before { + content: "\f13a"; +} +.fa-html5:before { + content: "\f13b"; +} +.fa-css3:before { + content: "\f13c"; +} +.fa-anchor:before { + content: "\f13d"; +} +.fa-unlock-alt:before { + content: "\f13e"; +} +.fa-bullseye:before { + content: "\f140"; +} +.fa-ellipsis-h:before { + content: "\f141"; +} +.fa-ellipsis-v:before { + content: "\f142"; +} +.fa-rss-square:before { + content: "\f143"; +} +.fa-play-circle:before { + content: "\f144"; +} +.fa-ticket:before { + content: "\f145"; +} +.fa-minus-square:before { + content: "\f146"; +} +.fa-minus-square-o:before { + content: "\f147"; +} +.fa-level-up:before { + content: "\f148"; +} +.fa-level-down:before { + content: "\f149"; +} +.fa-check-square:before { + content: "\f14a"; +} +.fa-pencil-square:before { + content: "\f14b"; +} +.fa-external-link-square:before { + content: "\f14c"; +} +.fa-share-square:before { + content: "\f14d"; +} +.fa-compass:before { + content: "\f14e"; +} +.fa-toggle-down:before, +.fa-caret-square-o-down:before { + content: "\f150"; +} +.fa-toggle-up:before, +.fa-caret-square-o-up:before { + content: "\f151"; +} +.fa-toggle-right:before, +.fa-caret-square-o-right:before { + content: "\f152"; +} +.fa-euro:before, +.fa-eur:before { + content: "\f153"; +} +.fa-gbp:before { + content: "\f154"; +} +.fa-dollar:before, +.fa-usd:before { + content: "\f155"; +} +.fa-rupee:before, +.fa-inr:before { + content: "\f156"; +} +.fa-cny:before, +.fa-rmb:before, +.fa-yen:before, +.fa-jpy:before { + content: "\f157"; +} +.fa-ruble:before, +.fa-rouble:before, +.fa-rub:before { + content: "\f158"; +} +.fa-won:before, +.fa-krw:before { + content: "\f159"; +} +.fa-bitcoin:before, +.fa-btc:before { + content: "\f15a"; +} +.fa-file:before { + content: "\f15b"; +} +.fa-file-text:before { + content: "\f15c"; +} +.fa-sort-alpha-asc:before { + content: "\f15d"; +} +.fa-sort-alpha-desc:before { + content: "\f15e"; +} +.fa-sort-amount-asc:before { + content: "\f160"; +} +.fa-sort-amount-desc:before { + content: "\f161"; +} +.fa-sort-numeric-asc:before { + content: "\f162"; +} +.fa-sort-numeric-desc:before { + content: "\f163"; +} +.fa-thumbs-up:before { + content: "\f164"; +} +.fa-thumbs-down:before { + content: "\f165"; +} +.fa-youtube-square:before { + content: "\f166"; +} +.fa-youtube:before { + content: "\f167"; +} +.fa-xing:before { + content: "\f168"; +} +.fa-xing-square:before { + content: "\f169"; +} +.fa-youtube-play:before { + content: "\f16a"; +} +.fa-dropbox:before { + content: "\f16b"; +} +.fa-stack-overflow:before { + content: "\f16c"; +} +.fa-instagram:before { + content: "\f16d"; +} +.fa-flickr:before { + content: "\f16e"; +} +.fa-adn:before { + content: "\f170"; +} +.fa-bitbucket:before { + content: "\f171"; +} +.fa-bitbucket-square:before { + content: "\f172"; +} +.fa-tumblr:before { + content: "\f173"; +} +.fa-tumblr-square:before { + content: "\f174"; +} +.fa-long-arrow-down:before { + content: "\f175"; +} +.fa-long-arrow-up:before { + content: "\f176"; +} +.fa-long-arrow-left:before { + content: "\f177"; +} +.fa-long-arrow-right:before { + content: "\f178"; +} +.fa-apple:before { + content: "\f179"; +} +.fa-windows:before { + content: "\f17a"; +} +.fa-android:before { + content: "\f17b"; +} +.fa-linux:before { + content: "\f17c"; +} +.fa-dribbble:before { + content: "\f17d"; +} +.fa-skype:before { + content: "\f17e"; +} +.fa-foursquare:before { + content: "\f180"; +} +.fa-trello:before { + content: "\f181"; +} +.fa-female:before { + content: "\f182"; +} +.fa-male:before { + content: "\f183"; +} +.fa-gittip:before, +.fa-gratipay:before { + content: "\f184"; +} +.fa-sun-o:before { + content: "\f185"; +} +.fa-moon-o:before { + content: "\f186"; +} +.fa-archive:before { + content: "\f187"; +} +.fa-bug:before { + content: "\f188"; +} +.fa-vk:before { + content: "\f189"; +} +.fa-weibo:before { + content: "\f18a"; +} +.fa-renren:before { + content: "\f18b"; +} +.fa-pagelines:before { + content: "\f18c"; +} +.fa-stack-exchange:before { + content: "\f18d"; +} +.fa-arrow-circle-o-right:before { + content: "\f18e"; +} +.fa-arrow-circle-o-left:before { + content: "\f190"; +} +.fa-toggle-left:before, +.fa-caret-square-o-left:before { + content: "\f191"; +} +.fa-dot-circle-o:before { + content: "\f192"; +} +.fa-wheelchair:before { + content: "\f193"; +} +.fa-vimeo-square:before { + content: "\f194"; +} +.fa-turkish-lira:before, +.fa-try:before { + content: "\f195"; +} +.fa-plus-square-o:before { + content: "\f196"; +} +.fa-space-shuttle:before { + content: "\f197"; +} +.fa-slack:before { + content: "\f198"; +} +.fa-envelope-square:before { + content: "\f199"; +} +.fa-wordpress:before { + content: "\f19a"; +} +.fa-openid:before { + content: "\f19b"; +} +.fa-institution:before, +.fa-bank:before, +.fa-university:before { + content: "\f19c"; +} +.fa-mortar-board:before, +.fa-graduation-cap:before { + content: "\f19d"; +} +.fa-yahoo:before { + content: "\f19e"; +} +.fa-google:before { + content: "\f1a0"; +} +.fa-reddit:before { + content: "\f1a1"; +} +.fa-reddit-square:before { + content: "\f1a2"; +} +.fa-stumbleupon-circle:before { + content: "\f1a3"; +} +.fa-stumbleupon:before { + content: "\f1a4"; +} +.fa-delicious:before { + content: "\f1a5"; +} +.fa-digg:before { + content: "\f1a6"; +} +.fa-pied-piper-pp:before { + content: "\f1a7"; +} +.fa-pied-piper-alt:before { + content: "\f1a8"; +} +.fa-drupal:before { + content: "\f1a9"; +} +.fa-joomla:before { + content: "\f1aa"; +} +.fa-language:before { + content: "\f1ab"; +} +.fa-fax:before { + content: "\f1ac"; +} +.fa-building:before { + content: "\f1ad"; +} +.fa-child:before { + content: "\f1ae"; +} +.fa-paw:before { + content: "\f1b0"; +} +.fa-spoon:before { + content: "\f1b1"; +} +.fa-cube:before { + content: "\f1b2"; +} +.fa-cubes:before { + content: "\f1b3"; +} +.fa-behance:before { + content: "\f1b4"; +} +.fa-behance-square:before { + content: "\f1b5"; +} +.fa-steam:before { + content: "\f1b6"; +} +.fa-steam-square:before { + content: "\f1b7"; +} +.fa-recycle:before { + content: "\f1b8"; +} +.fa-automobile:before, +.fa-car:before { + content: "\f1b9"; +} +.fa-cab:before, +.fa-taxi:before { + content: "\f1ba"; +} +.fa-tree:before { + content: "\f1bb"; +} +.fa-spotify:before { + content: "\f1bc"; +} +.fa-deviantart:before { + content: "\f1bd"; +} +.fa-soundcloud:before { + content: "\f1be"; +} +.fa-database:before { + content: "\f1c0"; +} +.fa-file-pdf-o:before { + content: "\f1c1"; +} +.fa-file-word-o:before { + content: "\f1c2"; +} +.fa-file-excel-o:before { + content: "\f1c3"; +} +.fa-file-powerpoint-o:before { + content: "\f1c4"; +} +.fa-file-photo-o:before, +.fa-file-picture-o:before, +.fa-file-image-o:before { + content: "\f1c5"; +} +.fa-file-zip-o:before, +.fa-file-archive-o:before { + content: "\f1c6"; +} +.fa-file-sound-o:before, +.fa-file-audio-o:before { + content: "\f1c7"; +} +.fa-file-movie-o:before, +.fa-file-video-o:before { + content: "\f1c8"; +} +.fa-file-code-o:before { + content: "\f1c9"; +} +.fa-vine:before { + content: "\f1ca"; +} +.fa-codepen:before { + content: "\f1cb"; +} +.fa-jsfiddle:before { + content: "\f1cc"; +} +.fa-life-bouy:before, +.fa-life-buoy:before, +.fa-life-saver:before, +.fa-support:before, +.fa-life-ring:before { + content: "\f1cd"; +} +.fa-circle-o-notch:before { + content: "\f1ce"; +} +.fa-ra:before, +.fa-resistance:before, +.fa-rebel:before { + content: "\f1d0"; +} +.fa-ge:before, +.fa-empire:before { + content: "\f1d1"; +} +.fa-git-square:before { + content: "\f1d2"; +} +.fa-git:before { + content: "\f1d3"; +} +.fa-y-combinator-square:before, +.fa-yc-square:before, +.fa-hacker-news:before { + content: "\f1d4"; +} +.fa-tencent-weibo:before { + content: "\f1d5"; +} +.fa-qq:before { + content: "\f1d6"; +} +.fa-wechat:before, +.fa-weixin:before { + content: "\f1d7"; +} +.fa-send:before, +.fa-paper-plane:before { + content: "\f1d8"; +} +.fa-send-o:before, +.fa-paper-plane-o:before { + content: "\f1d9"; +} +.fa-history:before { + content: "\f1da"; +} +.fa-circle-thin:before { + content: "\f1db"; +} +.fa-header:before { + content: "\f1dc"; +} +.fa-paragraph:before { + content: "\f1dd"; +} +.fa-sliders:before { + content: "\f1de"; +} +.fa-share-alt:before { + content: "\f1e0"; +} +.fa-share-alt-square:before { + content: "\f1e1"; +} +.fa-bomb:before { + content: "\f1e2"; +} +.fa-soccer-ball-o:before, +.fa-futbol-o:before { + content: "\f1e3"; +} +.fa-tty:before { + content: "\f1e4"; +} +.fa-binoculars:before { + content: "\f1e5"; +} +.fa-plug:before { + content: "\f1e6"; +} +.fa-slideshare:before { + content: "\f1e7"; +} +.fa-twitch:before { + content: "\f1e8"; +} +.fa-yelp:before { + content: "\f1e9"; +} +.fa-newspaper-o:before { + content: "\f1ea"; +} +.fa-wifi:before { + content: "\f1eb"; +} +.fa-calculator:before { + content: "\f1ec"; +} +.fa-paypal:before { + content: "\f1ed"; +} +.fa-google-wallet:before { + content: "\f1ee"; +} +.fa-cc-visa:before { + content: "\f1f0"; +} +.fa-cc-mastercard:before { + content: "\f1f1"; +} +.fa-cc-discover:before { + content: "\f1f2"; +} +.fa-cc-amex:before { + content: "\f1f3"; +} +.fa-cc-paypal:before { + content: "\f1f4"; +} +.fa-cc-stripe:before { + content: "\f1f5"; +} +.fa-bell-slash:before { + content: "\f1f6"; +} +.fa-bell-slash-o:before { + content: "\f1f7"; +} +.fa-trash:before { + content: "\f1f8"; +} +.fa-copyright:before { + content: "\f1f9"; +} +.fa-at:before { + content: "\f1fa"; +} +.fa-eyedropper:before { + content: "\f1fb"; +} +.fa-paint-brush:before { + content: "\f1fc"; +} +.fa-birthday-cake:before { + content: "\f1fd"; +} +.fa-area-chart:before { + content: "\f1fe"; +} +.fa-pie-chart:before { + content: "\f200"; +} +.fa-line-chart:before { + content: "\f201"; +} +.fa-lastfm:before { + content: "\f202"; +} +.fa-lastfm-square:before { + content: "\f203"; +} +.fa-toggle-off:before { + content: "\f204"; +} +.fa-toggle-on:before { + content: "\f205"; +} +.fa-bicycle:before { + content: "\f206"; +} +.fa-bus:before { + content: "\f207"; +} +.fa-ioxhost:before { + content: "\f208"; +} +.fa-angellist:before { + content: "\f209"; +} +.fa-cc:before { + content: "\f20a"; +} +.fa-shekel:before, +.fa-sheqel:before, +.fa-ils:before { + content: "\f20b"; +} +.fa-meanpath:before { + content: "\f20c"; +} +.fa-buysellads:before { + content: "\f20d"; +} +.fa-connectdevelop:before { + content: "\f20e"; +} +.fa-dashcube:before { + content: "\f210"; +} +.fa-forumbee:before { + content: "\f211"; +} +.fa-leanpub:before { + content: "\f212"; +} +.fa-sellsy:before { + content: "\f213"; +} +.fa-shirtsinbulk:before { + content: "\f214"; +} +.fa-simplybuilt:before { + content: "\f215"; +} +.fa-skyatlas:before { + content: "\f216"; +} +.fa-cart-plus:before { + content: "\f217"; +} +.fa-cart-arrow-down:before { + content: "\f218"; +} +.fa-diamond:before { + content: "\f219"; +} +.fa-ship:before { + content: "\f21a"; +} +.fa-user-secret:before { + content: "\f21b"; +} +.fa-motorcycle:before { + content: "\f21c"; +} +.fa-street-view:before { + content: "\f21d"; +} +.fa-heartbeat:before { + content: "\f21e"; +} +.fa-venus:before { + content: "\f221"; +} +.fa-mars:before { + content: "\f222"; +} +.fa-mercury:before { + content: "\f223"; +} +.fa-intersex:before, +.fa-transgender:before { + content: "\f224"; +} +.fa-transgender-alt:before { + content: "\f225"; +} +.fa-venus-double:before { + content: "\f226"; +} +.fa-mars-double:before { + content: "\f227"; +} +.fa-venus-mars:before { + content: "\f228"; +} +.fa-mars-stroke:before { + content: "\f229"; +} +.fa-mars-stroke-v:before { + content: "\f22a"; +} +.fa-mars-stroke-h:before { + content: "\f22b"; +} +.fa-neuter:before { + content: "\f22c"; +} +.fa-genderless:before { + content: "\f22d"; +} +.fa-facebook-official:before { + content: "\f230"; +} +.fa-pinterest-p:before { + content: "\f231"; +} +.fa-whatsapp:before { + content: "\f232"; +} +.fa-server:before { + content: "\f233"; +} +.fa-user-plus:before { + content: "\f234"; +} +.fa-user-times:before { + content: "\f235"; +} +.fa-hotel:before, +.fa-bed:before { + content: "\f236"; +} +.fa-viacoin:before { + content: "\f237"; +} +.fa-train:before { + content: "\f238"; +} +.fa-subway:before { + content: "\f239"; +} +.fa-medium:before { + content: "\f23a"; +} +.fa-yc:before, +.fa-y-combinator:before { + content: "\f23b"; +} +.fa-optin-monster:before { + content: "\f23c"; +} +.fa-opencart:before { + content: "\f23d"; +} +.fa-expeditedssl:before { + content: "\f23e"; +} +.fa-battery-4:before, +.fa-battery:before, +.fa-battery-full:before { + content: "\f240"; +} +.fa-battery-3:before, +.fa-battery-three-quarters:before { + content: "\f241"; +} +.fa-battery-2:before, +.fa-battery-half:before { + content: "\f242"; +} +.fa-battery-1:before, +.fa-battery-quarter:before { + content: "\f243"; +} +.fa-battery-0:before, +.fa-battery-empty:before { + content: "\f244"; +} +.fa-mouse-pointer:before { + content: "\f245"; +} +.fa-i-cursor:before { + content: "\f246"; +} +.fa-object-group:before { + content: "\f247"; +} +.fa-object-ungroup:before { + content: "\f248"; +} +.fa-sticky-note:before { + content: "\f249"; +} +.fa-sticky-note-o:before { + content: "\f24a"; +} +.fa-cc-jcb:before { + content: "\f24b"; +} +.fa-cc-diners-club:before { + content: "\f24c"; +} +.fa-clone:before { + content: "\f24d"; +} +.fa-balance-scale:before { + content: "\f24e"; +} +.fa-hourglass-o:before { + content: "\f250"; +} +.fa-hourglass-1:before, +.fa-hourglass-start:before { + content: "\f251"; +} +.fa-hourglass-2:before, +.fa-hourglass-half:before { + content: "\f252"; +} +.fa-hourglass-3:before, +.fa-hourglass-end:before { + content: "\f253"; +} +.fa-hourglass:before { + content: "\f254"; +} +.fa-hand-grab-o:before, +.fa-hand-rock-o:before { + content: "\f255"; +} +.fa-hand-stop-o:before, +.fa-hand-paper-o:before { + content: "\f256"; +} +.fa-hand-scissors-o:before { + content: "\f257"; +} +.fa-hand-lizard-o:before { + content: "\f258"; +} +.fa-hand-spock-o:before { + content: "\f259"; +} +.fa-hand-pointer-o:before { + content: "\f25a"; +} +.fa-hand-peace-o:before { + content: "\f25b"; +} +.fa-trademark:before { + content: "\f25c"; +} +.fa-registered:before { + content: "\f25d"; +} +.fa-creative-commons:before { + content: "\f25e"; +} +.fa-gg:before { + content: "\f260"; +} +.fa-gg-circle:before { + content: "\f261"; +} +.fa-tripadvisor:before { + content: "\f262"; +} +.fa-odnoklassniki:before { + content: "\f263"; +} +.fa-odnoklassniki-square:before { + content: "\f264"; +} +.fa-get-pocket:before { + content: "\f265"; +} +.fa-wikipedia-w:before { + content: "\f266"; +} +.fa-safari:before { + content: "\f267"; +} +.fa-chrome:before { + content: "\f268"; +} +.fa-firefox:before { + content: "\f269"; +} +.fa-opera:before { + content: "\f26a"; +} +.fa-internet-explorer:before { + content: "\f26b"; +} +.fa-tv:before, +.fa-television:before { + content: "\f26c"; +} +.fa-contao:before { + content: "\f26d"; +} +.fa-500px:before { + content: "\f26e"; +} +.fa-amazon:before { + content: "\f270"; +} +.fa-calendar-plus-o:before { + content: "\f271"; +} +.fa-calendar-minus-o:before { + content: "\f272"; +} +.fa-calendar-times-o:before { + content: "\f273"; +} +.fa-calendar-check-o:before { + content: "\f274"; +} +.fa-industry:before { + content: "\f275"; +} +.fa-map-pin:before { + content: "\f276"; +} +.fa-map-signs:before { + content: "\f277"; +} +.fa-map-o:before { + content: "\f278"; +} +.fa-map:before { + content: "\f279"; +} +.fa-commenting:before { + content: "\f27a"; +} +.fa-commenting-o:before { + content: "\f27b"; +} +.fa-houzz:before { + content: "\f27c"; +} +.fa-vimeo:before { + content: "\f27d"; +} +.fa-black-tie:before { + content: "\f27e"; +} +.fa-fonticons:before { + content: "\f280"; +} +.fa-reddit-alien:before { + content: "\f281"; +} +.fa-edge:before { + content: "\f282"; +} +.fa-credit-card-alt:before { + content: "\f283"; +} +.fa-codiepie:before { + content: "\f284"; +} +.fa-modx:before { + content: "\f285"; +} +.fa-fort-awesome:before { + content: "\f286"; +} +.fa-usb:before { + content: "\f287"; +} +.fa-product-hunt:before { + content: "\f288"; +} +.fa-mixcloud:before { + content: "\f289"; +} +.fa-scribd:before { + content: "\f28a"; +} +.fa-pause-circle:before { + content: "\f28b"; +} +.fa-pause-circle-o:before { + content: "\f28c"; +} +.fa-stop-circle:before { + content: "\f28d"; +} +.fa-stop-circle-o:before { + content: "\f28e"; +} +.fa-shopping-bag:before { + content: "\f290"; +} +.fa-shopping-basket:before { + content: "\f291"; +} +.fa-hashtag:before { + content: "\f292"; +} +.fa-bluetooth:before { + content: "\f293"; +} +.fa-bluetooth-b:before { + content: "\f294"; +} +.fa-percent:before { + content: "\f295"; +} +.fa-gitlab:before { + content: "\f296"; +} +.fa-wpbeginner:before { + content: "\f297"; +} +.fa-wpforms:before { + content: "\f298"; +} +.fa-envira:before { + content: "\f299"; +} +.fa-universal-access:before { + content: "\f29a"; +} +.fa-wheelchair-alt:before { + content: "\f29b"; +} +.fa-question-circle-o:before { + content: "\f29c"; +} +.fa-blind:before { + content: "\f29d"; +} +.fa-audio-description:before { + content: "\f29e"; +} +.fa-volume-control-phone:before { + content: "\f2a0"; +} +.fa-braille:before { + content: "\f2a1"; +} +.fa-assistive-listening-systems:before { + content: "\f2a2"; +} +.fa-asl-interpreting:before, +.fa-american-sign-language-interpreting:before { + content: "\f2a3"; +} +.fa-deafness:before, +.fa-hard-of-hearing:before, +.fa-deaf:before { + content: "\f2a4"; +} +.fa-glide:before { + content: "\f2a5"; +} +.fa-glide-g:before { + content: "\f2a6"; +} +.fa-signing:before, +.fa-sign-language:before { + content: "\f2a7"; +} +.fa-low-vision:before { + content: "\f2a8"; +} +.fa-viadeo:before { + content: "\f2a9"; +} +.fa-viadeo-square:before { + content: "\f2aa"; +} +.fa-snapchat:before { + content: "\f2ab"; +} +.fa-snapchat-ghost:before { + content: "\f2ac"; +} +.fa-snapchat-square:before { + content: "\f2ad"; +} +.fa-pied-piper:before { + content: "\f2ae"; +} +.fa-first-order:before { + content: "\f2b0"; +} +.fa-yoast:before { + content: "\f2b1"; +} +.fa-themeisle:before { + content: "\f2b2"; +} +.fa-google-plus-circle:before, +.fa-google-plus-official:before { + content: "\f2b3"; +} +.fa-fa:before, +.fa-font-awesome:before { + content: "\f2b4"; +} +.fa-handshake-o:before { + content: "\f2b5"; +} +.fa-envelope-open:before { + content: "\f2b6"; +} +.fa-envelope-open-o:before { + content: "\f2b7"; +} +.fa-linode:before { + content: "\f2b8"; +} +.fa-address-book:before { + content: "\f2b9"; +} +.fa-address-book-o:before { + content: "\f2ba"; +} +.fa-vcard:before, +.fa-address-card:before { + content: "\f2bb"; +} +.fa-vcard-o:before, +.fa-address-card-o:before { + content: "\f2bc"; +} +.fa-user-circle:before { + content: "\f2bd"; +} +.fa-user-circle-o:before { + content: "\f2be"; +} +.fa-user-o:before { + content: "\f2c0"; +} +.fa-id-badge:before { + content: "\f2c1"; +} +.fa-drivers-license:before, +.fa-id-card:before { + content: "\f2c2"; +} +.fa-drivers-license-o:before, +.fa-id-card-o:before { + content: "\f2c3"; +} +.fa-quora:before { + content: "\f2c4"; +} +.fa-free-code-camp:before { + content: "\f2c5"; +} +.fa-telegram:before { + content: "\f2c6"; +} +.fa-thermometer-4:before, +.fa-thermometer:before, +.fa-thermometer-full:before { + content: "\f2c7"; +} +.fa-thermometer-3:before, +.fa-thermometer-three-quarters:before { + content: "\f2c8"; +} +.fa-thermometer-2:before, +.fa-thermometer-half:before { + content: "\f2c9"; +} +.fa-thermometer-1:before, +.fa-thermometer-quarter:before { + content: "\f2ca"; +} +.fa-thermometer-0:before, +.fa-thermometer-empty:before { + content: "\f2cb"; +} +.fa-shower:before { + content: "\f2cc"; +} +.fa-bathtub:before, +.fa-s15:before, +.fa-bath:before { + content: "\f2cd"; +} +.fa-podcast:before { + content: "\f2ce"; +} +.fa-window-maximize:before { + content: "\f2d0"; +} +.fa-window-minimize:before { + content: "\f2d1"; +} +.fa-window-restore:before { + content: "\f2d2"; +} +.fa-times-rectangle:before, +.fa-window-close:before { + content: "\f2d3"; +} +.fa-times-rectangle-o:before, +.fa-window-close-o:before { + content: "\f2d4"; +} +.fa-bandcamp:before { + content: "\f2d5"; +} +.fa-grav:before { + content: "\f2d6"; +} +.fa-etsy:before { + content: "\f2d7"; +} +.fa-imdb:before { + content: "\f2d8"; +} +.fa-ravelry:before { + content: "\f2d9"; +} +.fa-eercast:before { + content: "\f2da"; +} +.fa-microchip:before { + content: "\f2db"; +} +.fa-snowflake-o:before { + content: "\f2dc"; +} +.fa-superpowers:before { + content: "\f2dd"; +} +.fa-wpexplorer:before { + content: "\f2de"; +} +.fa-meetup:before { + content: "\f2e0"; +} +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} +.sr-only-focusable:active, +.sr-only-focusable:focus { + position: static; + width: auto; + height: auto; + margin: 0; + overflow: visible; + clip: auto; +} diff --git a/webserver/font-awesome/css/font-awesome.min.css b/webserver/font-awesome/css/font-awesome.min.css deleted file mode 120000 index 7a206f2..0000000 --- a/webserver/font-awesome/css/font-awesome.min.css +++ /dev/null @@ -1 +0,0 @@ -../../../../sharpkey/webserver/font-awesome/css/font-awesome.min.css \ No newline at end of file diff --git a/webserver/font-awesome/css/font-awesome.min.css b/webserver/font-awesome/css/font-awesome.min.css new file mode 100644 index 0000000..8912420 --- /dev/null +++ b/webserver/font-awesome/css/font-awesome.min.css @@ -0,0 +1,4 @@ +/*! + * Font Awesome 4.0.3 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.0.3');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.0.3') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff?v=4.0.3') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.0.3') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.0.3#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font-family:FontAwesome;font-style:normal;font-weight:normal;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.3333333333333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.2857142857142858em;text-align:center}.fa-ul{padding-left:0;margin-left:2.142857142857143em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.142857142857143em;width:2.142857142857143em;top:.14285714285714285em;text-align:center}.fa-li.fa-lg{left:-1.8571428571428572em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:spin 2s infinite linear;-moz-animation:spin 2s infinite linear;-o-animation:spin 2s infinite linear;animation:spin 2s infinite linear}@-moz-keyframes spin{0%{-moz-transform:rotate(0deg)}100%{-moz-transform:rotate(359deg)}}@-webkit-keyframes spin{0%{-webkit-transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg)}}@-o-keyframes spin{0%{-o-transform:rotate(0deg)}100%{-o-transform:rotate(359deg)}}@-ms-keyframes spin{0%{-ms-transform:rotate(0deg)}100%{-ms-transform:rotate(359deg)}}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}}.fa-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=0,mirror=1);-webkit-transform:scale(-1,1);-moz-transform:scale(-1,1);-ms-transform:scale(-1,1);-o-transform:scale(-1,1);transform:scale(-1,1)}.fa-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2,mirror=1);-webkit-transform:scale(1,-1);-moz-transform:scale(1,-1);-ms-transform:scale(1,-1);-o-transform:scale(1,-1);transform:scale(1,-1)}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-asc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-desc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-reply-all:before{content:"\f122"}.fa-mail-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"} diff --git a/webserver/font-awesome/css/font-awesome.min.css.orig b/webserver/font-awesome/css/font-awesome.min.css.orig deleted file mode 120000 index 26f3a76..0000000 --- a/webserver/font-awesome/css/font-awesome.min.css.orig +++ /dev/null @@ -1 +0,0 @@ -../../../../sharpkey/webserver/font-awesome/css/font-awesome.min.css.orig \ No newline at end of file diff --git a/webserver/font-awesome/css/font-awesome.min.css.orig b/webserver/font-awesome/css/font-awesome.min.css.orig new file mode 100644 index 0000000..8912420 --- /dev/null +++ b/webserver/font-awesome/css/font-awesome.min.css.orig @@ -0,0 +1,4 @@ +/*! + * Font Awesome 4.0.3 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.0.3');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.0.3') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff?v=4.0.3') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.0.3') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.0.3#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font-family:FontAwesome;font-style:normal;font-weight:normal;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.3333333333333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.2857142857142858em;text-align:center}.fa-ul{padding-left:0;margin-left:2.142857142857143em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.142857142857143em;width:2.142857142857143em;top:.14285714285714285em;text-align:center}.fa-li.fa-lg{left:-1.8571428571428572em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:spin 2s infinite linear;-moz-animation:spin 2s infinite linear;-o-animation:spin 2s infinite linear;animation:spin 2s infinite linear}@-moz-keyframes spin{0%{-moz-transform:rotate(0deg)}100%{-moz-transform:rotate(359deg)}}@-webkit-keyframes spin{0%{-webkit-transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg)}}@-o-keyframes spin{0%{-o-transform:rotate(0deg)}100%{-o-transform:rotate(359deg)}}@-ms-keyframes spin{0%{-ms-transform:rotate(0deg)}100%{-ms-transform:rotate(359deg)}}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}}.fa-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=0,mirror=1);-webkit-transform:scale(-1,1);-moz-transform:scale(-1,1);-ms-transform:scale(-1,1);-o-transform:scale(-1,1);transform:scale(-1,1)}.fa-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2,mirror=1);-webkit-transform:scale(1,-1);-moz-transform:scale(1,-1);-ms-transform:scale(1,-1);-o-transform:scale(1,-1);transform:scale(1,-1)}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-asc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-desc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-reply-all:before{content:"\f122"}.fa-mail-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"} diff --git a/webserver/font-awesome/font-awesome b/webserver/font-awesome/font-awesome deleted file mode 120000 index 4cea6b3..0000000 --- a/webserver/font-awesome/font-awesome +++ /dev/null @@ -1 +0,0 @@ -../../../sharpkey/webserver/font-awesome \ No newline at end of file diff --git a/webserver/font-awesome/fonts/fontawesome-webfont.woff b/webserver/font-awesome/fonts/fontawesome-webfont.woff deleted file mode 120000 index 3ecd29c..0000000 --- a/webserver/font-awesome/fonts/fontawesome-webfont.woff +++ /dev/null @@ -1 +0,0 @@ -../../../../sharpkey/webserver/font-awesome/fonts/fontawesome-webfont.woff \ No newline at end of file diff --git a/webserver/font-awesome/fonts/fontawesome-webfont.woff b/webserver/font-awesome/fonts/fontawesome-webfont.woff new file mode 100644 index 0000000000000000000000000000000000000000..400014a4b06eee3d0c0d54402a47ab2601b2862b GIT binary patch literal 98024 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 SdSQM28?+M|D(2r_;{O0|uQ74} literal 0 HcmV?d00001 diff --git a/webserver/font-awesome/fonts/fonts b/webserver/font-awesome/fonts/fonts deleted file mode 120000 index 7aa1ca6..0000000 --- a/webserver/font-awesome/fonts/fonts +++ /dev/null @@ -1 +0,0 @@ -../../../../sharpkey/webserver/font-awesome/fonts \ No newline at end of file diff --git a/webserver/images/images b/webserver/images/images deleted file mode 120000 index d6a4665..0000000 --- a/webserver/images/images +++ /dev/null @@ -1 +0,0 @@ -../../../sharpkey/webserver/images \ No newline at end of file diff --git a/webserver/index.html b/webserver/index.html deleted file mode 120000 index bb47430..0000000 --- a/webserver/index.html +++ /dev/null @@ -1 +0,0 @@ -../../sharpkey/webserver/index.html \ No newline at end of file diff --git a/webserver/index.html b/webserver/index.html new file mode 100644 index 0000000..50f3937 --- /dev/null +++ b/webserver/index.html @@ -0,0 +1,166 @@ + + + + + + + + + Dashboard - SharpKey Admin + + + + + + + + + + + + + + + + + + + + + diff --git a/webserver/js/140medley.min.js b/webserver/js/140medley.min.js deleted file mode 120000 index c81a78b..0000000 --- a/webserver/js/140medley.min.js +++ /dev/null @@ -1 +0,0 @@ -../../../sharpkey/webserver/js/140medley.min.js \ No newline at end of file diff --git a/webserver/js/140medley.min.js b/webserver/js/140medley.min.js new file mode 100644 index 0000000..d1495d1 --- /dev/null +++ b/webserver/js/140medley.min.js @@ -0,0 +1,2 @@ +var t=function(a,b){return function(c,d){return a.replace(/#{([^}]*)}/g,function(a,f){return Function("x","with(x)return "+f).call(c,d||b||{})})}},s=function(a,b){return b?{get:function(c){return a[c]&&b.parse(a[c])},set:function(c,d){a[c]=b.stringify(d)}}:{}}(this.localStorage||{},JSON),p=function(a,b,c,d){c=c||document;d=c[b="on"+b];a=c[b]=function(e){d=d&&d(e=e||c.event);return(a=a&&b(e))?b:d};c=this},m=function(a,b,c){b=document;c=b.createElement("p");c.innerHTML=a;for(a=b.createDocumentFragment();b= +c.firstChild;)a.appendChild(b);return a},$=function(a,b){a=a.match(/^(\W)?(.*)/);return(b||document)["getElement"+(a[1]?a[1]=="#"?"ById":"sByClassName":"sByTagName")](a[2])},j=function(a){for(a=0;a<4;a++)try{return a?new ActiveXObject([,"Msxml2","Msxml3","Microsoft"][a]+".XMLHTTP"):new XMLHttpRequest}catch(b){}}; diff --git a/webserver/js/bootstrap.min.js.gz b/webserver/js/bootstrap.min.js.gz deleted file mode 120000 index 399e6b3..0000000 --- a/webserver/js/bootstrap.min.js.gz +++ /dev/null @@ -1 +0,0 @@ -../../../sharpkey/webserver/js/bootstrap.min.js.gz \ No newline at end of file diff --git a/webserver/js/bootstrap.min.js.gz b/webserver/js/bootstrap.min.js.gz new file mode 100644 index 0000000000000000000000000000000000000000..99c5170f4e6abef397f96d1dc4f5c28926486a61 GIT binary patch literal 10921 zcmV;aDpu7WiwFoJm1<%D17dG)baQlaVQ?;La{%o<{Z}J7a=-Ie2#%!f9b=f?Tyo*o zGhrAuM=}d!V6(Z$aY=e zmA1cHUsGSHH$_pE)jXZ458w26`d_QdJM~ZFv{LDGr2gq3`BvF4PCU+{XK%XD;DY| zy;IYoQj0PJ7$6+?Bkl--r*(6V=!;BfCavc!k{ZYdxE~^x(r_f*qzs9nn zN-K@@gZc7@=_D&l!xhAJ=?oAWrk8-`B)wI|T%}j@49cnk5x$)RNv2mlRTkswHl1e( zW|RX7^UFo$&6hzBMlCDAjHOexv3IHtPdC(?z0FC@azxAKb)x__ujwXJ32c& zI912*)&B9(+rzWN<0JU>o!UG4qx#pwqqjYk!E8Z4*~i%&M*wK%IFs3krtLJ#JQT(S zqg2kaVLr}>Ftq8_BE8Dg6|B7Z6h@?G+59Fik)kE8Is`MxZ}KW77Oan}|CC%wi+kK+5Qr>tCHK`BeYJaK~E*$7k`np z;U6?`_-3N&aZr(}79Lnt#G6)O%Z6U=d7Q|F9USZw#z*45Ht4P$GfMhw(l4vKNp_A6 zF4Q{kiIw!Orh@fbE#^~T80c{~0#&!a*A4F1%>UnOrqV2PIVrCC=_LDjlMaXJEiuio zEVn#StTy}OX+OJJOn|8VGz}bYbViFgtsKb~L)h;!s+LYrf^HI@pj@OXn;rbK+lQI! zZ1&5=Y*x&xzSu)xcs`2!HE%cyhf;T%J20YeCPc#Z|Ng)JSB=;TOu3=yKjGHwO z9?*I4K26P@x9I0B`gx1i(sb|L0qn^8@AhD;etYoU;StSF+vbZ6QD^}XaDX5PlJiko zrCa*LV1riK#zmql?%_`mBT~45ATz=R{{}JINru2EKPNo{xJMk`A5Mxg>l*Pyu==xk zQ59%faH$Vj3h_-^B-FPS*tV$5CfTq8LJqiAKdq{HC!vv4=^Vs}+44yUo9b>WYXERP z&&El&UdR6Vw|{H@?$5K?Bpqg*?fzGt*Mr}j|M^el#aEl%ZP>4qE|LJLH6wn@vwx8L zGmUNr3uao;;4FR)XPAi$rYgg&^=&pz7n7x^@E-U};>}X}Ld1%d(RY zVF9t%dSZ8V4Q|lsN(2pbvt1#THv6>CBdLLG{mZhiAxfTEhp@2z}tv(n!OF%OEKUjBPLZ;}>A$G9DR2ixdb z0~G>@(*j_QK}uGkqNv$ZPy|R8SMekUZ}-mjw)RdAW8JedXzU^(q3tT6Io?WVxs=&a z0sY0)LQn~+$M|o!-Lqo&tQbBkhFU_r`QhyB_(+}n@CL~FAO+E`0KI#>p~V%bf3!pO zw5YYN1QOkwx|FtqOzwhIteeoD(9b?ruw{0Bx$o(2`her!5AV)S9c&|;g9_GXGbz#$ z5atYE?W#oo>i7Fe(9j7rFs)C4UcTa4;uooWj#%+bfGLs9wSR?nyKJJgGr^A57Jf*n z5Wa&Zglf%f!8%t0CFD%Hu5Km{nBLd0K5cq>Pb@Wp{NEQU!SN+R{lm(j=AfGZO1cg} zVm~J2_Be-rFFHm?vmabv8(3-)vR2(9B>U-T#M;=X%c?nORwJh>aV4oij*Ql~N3~J7;@;j< zJ0S}>PN<5jt4S8!j(rW?qc!h}B`CsdIWd&MhTew2bP@`2lbTNOi92fG$2qJC!#PfW z1hi+ciC<^KpMe{DYTa)o^n4h~hb-xewoX;P29cH}Qf*=^GTw>A*}j!S(jj+r00hAsMK^Lx-NI0$ht-d z0_AHuYjOD#n1TsfCj51|lB|31wPa>?LemM;uBUQ~UYR&rQ`V&F7?iUK?$2X4ZB)sK zQS&vXOKXkJSEk=+@_1BVu1*g^sH-T{mi9|DYK=vkO5LtgYn+5q?Ma6}54gfb(rff~ zTvk)wH2dtq=n7*W%EStXU$DGw3oNuUsZWJG16aWQb}WUdB;r~4lK$UeIxj$JnLJ$8 zH!Yua{bybOS=VPVwZHfN_=nSjcMnzb&Ca>6*VH3)e7zUe)qGGYM!+*5@XH4Mrf-(= zmaCe?3@8bsj^!kz(eYzxcKjOa+(=)>3UT7(WT zSpX`XALvqtQ}Iu@ngBv5z$)W~xT0IObUqG>eQR8m{&Wfyr0YkUh>(5`tN$ryoy78e8@eC01LUIL7FbtaFm)vp7kae60k6X?&8Nm>w-XQ9Z9K{M=Jh@(2mb z!ftabiyUf3bEY>z{1doKgF45KE;_DmUhV@uapxWQBy^50P$!2b_5@nf*2!xD7R4kR z^P(Un+_}fbCblNpYMjqYZU%OdXyt^H7EQvE!p1dk3>A4>0Pcq#fs~$)mMKiS=LX-4 zPMxcbZo-RF<7DT}s1I7SxN~Cf=#@y0_NLLEK!E%LXyVD@=#0$9T}JeL=c4B(eOJu? zy>w5}bVPhdacL{my4oDpDy;4=3I$1{z_uPO%b$m)KkbwOc`k8qyW%;Og&Wa@bCLz% zQ2u#eoJ%r9{@$ZCO>4thbCb)C$cnSxE?6_^w_9@5E(YZ=$0gL2MZhKfVmt;N>BoEo zQXlLEu#CX=oM+=nacin-Wy3jT4KsD){nX1Dg7Zi|Ao9*OGTlfWbnfA2_$GQI@&AYyH(8W6^{~!w3F;N7ue*C*6~? zPh^h5xeQ1jUD@$@iess#!dp@`DcPiWNuQpziy!Dwj{20X#!%Gq4~4i4=uY%c^FX#whp?0{aJt9HCVC2vfQ67_BQF!HiM}9v>m`YI@86? zB^bHZv)%DWV}08^BpoWkKQ5X);6evwu%SI*7w>(IUjk?NzU8ZcX;*NF3zxWszKT;^ zuR@U+H`fU4sA-u8AskXG07IJ1*U&lREbLn>PrQp$w$*E;izz%S zGajfWe9G(Mw%x(!&%Oz-ieQybLstvw0I;W4; z%C1Y#Z5J1v9ob2^@LxJKX|K$xc)VL2#|A7VCLX5wRD72>Ch5Xztqqv&Eb$5e`%feM zFT8H+F^7^pxG}@$_#s40qkl17xuOe)0=Fvp8}VhCtO4xAu}B95!$j>Um6a6 z+gx1cDAJ%6o&iVwJ>UFxC&mF@_vbqo=WaL`bvf)L*>!k+uw0TgMjbvxlwBJq_7@Xn zk6WV6b1Vuh$(%3s544B0utw|JUM3hC?oB<#@?U99x`l5SClGX$#a8FTMaYvc?`4Fw zE9x^Y*ro;85`c`r&)XIosx7pIq1J-ZF}d*Xj5$sBDg2+tl(ZhFnCAOM{huMHwfR5m zI`bp^pUWMWPEqh2mqz7ykmE9Xd9A^>XoeT~7A?&JhIpWF(Li6xx9HennQzfx^GUu% zgIiPJ?ZDLd1gE0*DYo7C2&!O=;v#y+}Tr$*fCe)0Tr8+y)3jx(3Z$P&WQqBQD%Y#g}t7 zj^@Q|gqrq)osFjD^F7RGZ{xGKQEzf@-yfg6J^u0Nv9Z}0ON0vxZzA^13fnSYO^yz& zM$&ynPwbf%Hk-#={UPSB^tMEOK4OaeUDtq3deY@*brRD_yP_M}f}Q?nw*lTy@G52B z+{EmuU3)1rV(M+W%?RYx3CG8Z>;4?Bwr65+>**hgS!RtY3P1cln=VQ(`G#rlZ$wEk zt%}9)nyk|V*Pl&HX8zF09VmR+^z5$gOw$jS>3j?RW8*Mo{0lG)baPW&qEx!2{~^nz zj!Q1HA%%w$rTD%kEazgHRB8mIyaM~gLfh;l-;VMRP&dW2FE!@c*nS0AmQ(A;JQk73HufPzbd|J9Jl2OaLJFF)76y7*PPwS%@#q_!_lFg~U zg*N!dgwiOTV`dNidzk_!N-ya;Eoi5`WA6Mr=qi*90y>z36irw-S&O)KIAT@_uOhGX*&dZ?V9YvPp zy=MDDfYN$c6RO%Cd7ZRAQ>CGbmxP&m+TsE;`rn=7yM{m@hdAh}hAnIcxA%g~(O{vfCw5=^DR&&=)AF}|Ir2Oj!s_3ht1RFRo zofKD`LHRb#E838#i1N(QrRRKVAwBvsVGRkMg8FqiqJsR<5bLqCBNNoE1pK~He$`^L zJTGEPcIc?9fvL8#Oj>^ep(JjeM5K~;qCXzP62}S3r|rn<$&G|SwJ^QQF@UUseZ>Tz z!RvuHcY|Ef4S`atMHX54O1u!n7p=r~K7>vqbIC;?w^jpl(ac<(O1MfvJDiU|^#!fF zL`@x*b6%k$iQq9`q=hk!fN&EK_0B~SSAgK8CrV7FG?xuVQphDzi|h*FD)9Zs=H(Jf zAZf`EpGsou_9#lTC=4GNrOfD6P4~XFA-kAaUR{0CJg2$s8F|Bw^4?>ydw0vxTmFs6 zAwM)=XPoUVvaPOQx2a?Saos4ED<33>Szx>|^}4dG9&79zr`~WHdBBI>7|tgYeJfGS z7vF0gngwhDH1vKM>(@LQ6U*5X=Ifc}H=J#DnUGWb7b@dPg900}xOK~W%HXKjvz<0c zT#eS6$9!U}!TBZY4z?U4d(6Nvv`67-rcjRJYT#PqGK}lYD+n$tqgY@K7jx89jS=EG z^;LZ8wC58{>tQ0~*J~BZL>6K$09KYs^<~$|0vRnE3I(CeT6%Q@IxJIRqr?kUR;kuI zzV%Of9ml-(XUjreT_Lo6ELG{e=NXi3Xq1>Hky&5fp%4w0ePlKsr-=ODoSaY?d6lW| zns9xy)8>=kx}Y^`-SjuL?S_|GPc3yxe1_O%xvjJYF8h9(&l9z5gqb{*Vg2z5;+5;( z8vIwwr-0_A$t#SebgWr$tiw)n|4iq5rn%&!2fWk45LY?JG2FD|5Qi~%^gA7lPh(`W z@X)Lp*lbHE2w8;zyW&Ll5y=!*OPjDPZ$dpr*%%sr7_wa@id~z#YTX_dBN{fES}o1J zz-DuUIjx%-3x<%fvO_L-&RRuI$|j$Ki@`BLB?+Nf4>}S_1{%)pLWiVE?4N$Zq0K=9mW$G72yb@+qu( z9zY+em3LV9FKG#r@!dSVdCs%&eDwBw^rrW|v*Y7;XNM=BlxD$bb!WURS3i!URFV8o z9I1)43gT-vr3lF2YsJaig0GY?Sw9|A_e-cqx@h!-Ve)+%07%ao+D50*W2~Rau@#$R z4o7|1)>E{>A{=}LnB%~EHi~K22C2dZl>xVr0t*zgOr)*S3R6fry{9`Fy0W@|F;r5TG;(ibGrr1del4TpV3;ft7 zSjTf>3!Bg&vJQycq|*hdcz%LUQpXNC&7446=UWGLP^48ypOj^{0K_prveOKL)2t(i z%gJJ%tToSvMd<}-M30yiLbm4Fk9UJ;Ke8JdP&oJ6(yno?1!7$*4CFr>Y*N`4&jH=4 z*L0MkC`mx?3l1p8`NuO92c4Sm^OC7+MLd2$p)IaZYPJGAw&MxC09Hh_4OyVnF&X16 z(TrtLi+nPeV$aHQ#MzI&+(%`yn8h~>e-7hbt819=K2NW9Er0X>5*TBVH=e0L-^ zfzsX-qVSh{`*q~#Sf5A1j+Wmj^*A|QyzkQV_&K}l;ah8q;0liE>l`ZJ8Ih@Do;IV; z`m1p&JZLs(0s9q{mNEn;T$8uQE?ev(R!LLa0b%suY=Y=~EK>z}m;M^7V_%a{n+Hw9d` zz7R(#K0?$Ox2^}9DA-d?=rog&1myo`GD6|w=!nw8r=vb?dqO%E^d_PN-b_fsrho()vCy3k+9!n)S!cV}7*|NnErz|?{WbQok!7Q;j#fl_8(lknK3R^6Q>^#}>dKVLhA^5SD+U?i%T zGwQ7PIm2j@?LU>T@%L-^?{*$kI>FXZ$u?j&pam)&*S3Dwn+i7$3_hh74BE!gmDPd54sUXQttE=zHdN(jB-%%(e?pVvjFYe%kgdLCNP)GU}no& zNc5IF65JeT#;bMdW(w4kw>dr9_$*SG)PqV|H77a}t|QMa(qRQEA0Pga=Qa<|1Z$N! zJ5sH(Oqx_9Rgn3=;bBgD@5ZZL%&$&jl=do(is30G&EhdPUEJ++X05Ygh794kK3$Oq zm~pZb)IdjEYX}5w63&0yCo5aG+qHL&Ky>P_g4tX@c5WZok8T(A!(XEvy#t0;D-LK! z!X;W$pys%8bc!xT4Wz1y8#>Vfz67&!rqKGrzd2PS{_+@WOAzywKQDZSFiWsYCyC@g zzjIU9>wL>&mMy>YZ(+1e|6aS%tkO9@gkCWM#FHo4UI536Xot z>-<}v6_#{vMzf@I#}b}^4PyS}lJ*X&V-l2$?xlo-y=q7&+M{FQM`!vQ_Sj?^ zik@gD&)rdaq)$23@3#NZs+xHT1s#hbXoUh_WwP#tH4!JM>LI9aPVj#QN-CF!v|r!aAS{^yhYE4B5tt>$yajxUEp`rE>l z)jzcoVFFQ2GDpw$9-c~ccraU9^t^}(WBr47FfX+AmqxIG{=gLTrF)P0qn4FY!|6un zsZl!1-lc4&@rhKkJ$wJeJH((P8hmdAcRF36rz!nfb$+`8wC%h^O@%&fJAhr7w2i;n z=u!-4n_X?~&PMB+w}$A>K&L%oQ#I~hD_6{o5AR4R(asX1P2#~2Wx0g^l6StshuSrX;*PW^Q4`d^;|yh0ZG)_)jQA zS@=)ZjUSAL?%AEvx8HibV-@E_eP*4EZ;wsHhK&~_e6x8;D59Q`Wmh1)Ktwl)GrzRo>|)(;YwPLVLKMm!K0GHf_HX;7ghq%rIjb8aK2_p&^=qZQRK~!Swsj9$ zv+K$lfn3b{@3#6nunF-CY`30~7dNSE{cZ)@?GyKF4BFQ5dXWEMKh==&JD2@#e-*dl zzuJC^e&r;zu2Zx4nm05oT_&l>dlou*v{=>9+ohGB7J8OgpL+InOW(O{zUy2r8T*Qr z`5cI4jeEn`!$_f+aFTmB{f%$i29KKP(3zH%ng;4yLtmzv{AC^mA;lrohNw!LuJR$B z_zOwM8v#$VVs57otos&cMqfP=}+_ z)U5(&Nyrd~r5w2G%iyic-^<{}FYJo*#0)+5o$wmar=1b%`#jW@KA%ILs^ui6GCKIA zLpf|mpXf-NKebQ*OwxIA*F0m)M#1&K08_0`zHK<`6p`~bdhmhCvLg3DOib^Z@_t96 zK)%e~4-*R*zx}bom5${%I$XG+ywjnH`q{G3o2YC5O2(O)exI;5{ebc7Z-UBf3}E^7H?JbVwshb1h;j_hW6~ZSpB?O4E;#s) zvxB3zr()_>Vt|31UaQ)Kv7U|7Yg=@qaJ;y%fhmtyT`{^fWCg=#b$~0oYg-+*GTOuW zZp`yh2E08f_2ZJHTi2ufW8Jy|ERDdb&~$Z)T-{F`brLJ@FzfK(cWP8*CFX`ohqQxT z^JDfO@{}awf9=9rsqX$u&B|;sDwK=KzRlD%#u>sUGAax}+7Rt|C+t0RTt??(08FD@ z_MIu$GO;vz^xU@AF$P`(?>x$Ag^MeIr9FM4K$9|D`$5PH_>dr#bfbD=ID^F*4jYgSCU}1m29yr9grX59GgVWzlo(5;p`H!$G z@1-M5&p?r@UGmj(lq^M1M^pDgO{W)7Rw`~R>aespuuk>bbwB;aFrG&9Ut1`ZMB)!U zkl3wL+p)A^>>Rc}5^wMQ85n^wAQNAp8fBj@bd&Xn_Y30*K$Jd`#v6|cA3_2aSFco> zh#}Kas2pyy>ZKDjzpV>E0fh@qJWaM`h+#klqv8}DR}aNP#4?6vAw37euqag zlnoHPN#i;B+#_Rk-ApE^!Slt5R-0tP8T=Y~L_~wtY`XpD-~8z|%m}l|f4T@Zt$cJg ztwiB}SeKZYFI)R1=jU8_F3EL)>0Rky7o9+P`6JPZFF^S+0L`J!PS?C;QY*GM3Ba`D zw@B5+1r1!B$JvV`{j!+L`&VgCU3MuyFO4ikw%Jmb_#fA%QEk>|NSb&0vqj0*Fufdd ze9Xi=A*#HzRqi7z#w`LPubUQuyJTTmv1N+PA(D|>jCugfzZl^I!{bq}ma)Hx&BIa) z_$K`*uZ(yQwxUR1%li~=wUJ=qIjw|N!9}9Bst$aq)9H4 zINPvte<*Y(jJ}wgUgAwme7{A!niy~t^v-`+I=g2VCwcfjRudM2pEVvfp3sopIA}tZ z_`$C}#XKd>>@gCeBIVdrfT5byBbNe$*h+!z$hr zN$@4v2OUOBURc*z2(_g{1_?eO5=~>bX&%-F6b2iN4ZWM)&RLAG&sO-+ji<9|Y@SY6 zQsT=cjY}^dkuWa3d_Jdt_5nTnfJ}JL-kS$_f6NwlmR`nA9JZjopOjtUZjn1kGi-~?0R%#qwkuJ6N%%yL)`q?<1?upD#e+<(-4{X#b)W$>r zp;3V9uiIkI)V!caBw*(3Q5DtSMe9JNN!P^d+p95pe#6WhAWV~{$=HCIca^*Ui_@bEd#$r$ zeFOImD8SZ}Mn#4nP8%N3+GYNDa7t4aS#`L;>zi2&{saCY(Z$Vk? zr`g9!PHCv@#|tx+?$A_Hi!teOle(V<}uBz!GHJTJ8RTT(Szw_hqlPI=IUGG;9;Z!|Z? z-XC(Y%diJN#N3O;ET@+)_%+xZjUn@y-}Ti3XNCOnEVjK!9@?x;#iL1rpP|FR`((=Vt4Q?W%_C?ubWoZg(i%xacYW<1C*v=6jFiFQ=g}A+R^z zXp=8%Ri)!`{_){<6*YS{tey?4XTyrc%HDV19sb9oUb5Z8rL=mmb3+DHg3@}Lys6J6 z2hNpb9J>AyH;V)z`n)rHiiSL~ISqIlk$|iKt-)yl^ds;MSsU+)VitM6bV-h9>WY#^ zyKjr>EXU`0>j%kR&+hD)r||v3>A@MaNeCv(M@&Sc|I*iTUH!&$<{cldh(m51cyzN9 zIUJV~qNiSVSL$}fi;2No7#vvzUeHez8YwD1>0QDH!u8@XNO1ZX=q#N+49tIL{6&aA%>@B!K5P76 LzA7`J*W&;HjU', { + class: s.tableClass + ((s.first_row) ? ' wh' : ''), + html: defaultTableContent + }), + addRowCallBack = null, + defaultth = ' ', + colnumber, + rownumber, + reset, + is_validated = true; + + // Increment for IDs + i = i + 1; + + // Build cell + function buildCell(content, type) { + content = (content === 0) ? "0" : (content || ''); + // Custom type + if (type && 'text' !== type){ + var field = s.field_templates[type]; + return '' + field.setValue(field.html, content)[0].outerHTML + ''; + } + // Default + return ''; + } + + // Build row + function buildRow(data, len) { + + var rowcontent = '', b; + + data = data || ''; + + if (!s.row_template) { + // Without row template + for (b = 0; b < (len || data.length); b += 1) { + rowcontent += buildCell(data[b]); + } + } else { + // With row template + for (b = 0; b < s.row_template.length; b += 1) { + // For each field in the row + rowcontent += buildCell(data[b], s.row_template[b]); + } + } + + return $('', { + html: rowcontent + ' / ' + }); + + } + + // Check button status (enable/disabled) + function checkButtons() { + if (colnumber < 2) { + $table.find('.delcol').addClass('disabled'); + } + if (rownumber < 2) { + $table.find('.delrow').addClass('disabled'); + } + if (s.maxRows && rownumber === s.maxRows) { + $table.find('.addrow').addClass('disabled'); + } + } + + // Extension method to change or set the table header. This method should be called before fillTableData. + function setTableHeader(header) { + s.headerCols = header; + } + + // Extension method to change or set the table column types. This method should be called before fillTableData. + function setTableType(types) { + s.row_template = types; + } + + // Fill table with data + function fillTableData(data) { + + var a, crow = Math.min(s.maxRows, data.length); + + // Clear table + $table.html(defaultTableContent); + + // If headers or row_template are set + if (s.headerCols || s.row_template) { + + // Fixed columns + var col = s.headerCols || s.row_template; + + // Table headers + for (a = 0; a < col.length; a += 1) { + var col_title = s.headerCols[a] || ''; + $table.find('thead tr').append('' + col_title + ''); + } + $table.find('thead tr').append(''); + + // Table content + for (a = 0; a < crow; a += 1) { + // For each row in data + buildRow(data[a], col.length).appendTo($table.find('tbody')); + } + + } else if ( data[0] ) { + + // Variable columns + for (a = 0; a < data[0].length; a += 1) { + $table.find('thead tr').append(defaultth); + } + + for (a = 0; a < crow; a += 1) { + buildRow(data[a]).appendTo($table.find('tbody')); + } + + } + + // Append missing th + //$table.find('thead tr').append(''); + + // Count rows and columns + colnumber = $table.find('thead th').length - 1; + rownumber = $table.find('tbody tr').length; + + checkButtons(); + } + + // Export data + function exportData() { + var row = 0, data = [], value; + + is_validated = true; + + $table.find('tbody tr').each(function () { + + row += 1; + data[row] = []; + + $(this).find('td:not(:last-child)').each(function (i, v) { + if ( s.row_template && 'text' !== s.row_template[i] ){ + var field = s.field_templates[s.row_template[i]], + el = $(this).find($(field.html).prop('tagName')); + + value = field.getValue(el); + if ( !s.validate_field(i, value, s.row_template[i], el) ){ + is_validated = false; + } + data[row].push(value); + } else { + value = $(this).find('input[type="text"]').val(); + if ( !s.validate_field(i, value, 'text', v) ){ + is_validated = false; + } + data[row].push(value); + } + }); + + }); + + // Remove undefined + data.splice(0, 1); + + return data; + } + + // Fill the table with data from textarea or given properties + if ($el.is('textarea')) { + + try { + reset = JSON.parse($el.val()); + } catch (e) { + reset = s.data; + } + + $el.after($table); + + // If inside a form set the textarea content on submit + if ($table.parents('form').length > 0) { + $table.parents('form').submit(function () { + $el.val(JSON.stringify(exportData())); + }); + } + + } else { + reset = (JSON.parse(s.jsonData) || s.data); + $el.append($table); + } + + fillTableData(reset); + + // Add column + $table.on('click', '.addcol', function () { + + var colid = parseInt($(this).closest('tr').children().index($(this).parent('th')), 10); + + colnumber += 1; + + $table.find('thead tr').find('th:eq(' + colid + ')').after(defaultth); + + $table.find('tbody tr').each(function () { + $(this).find('td:eq(' + colid + ')').after(buildCell()); + }); + + $table.find('.delcol').removeClass('disabled'); + + return false; + }); + + // Remove column + $table.on('click', '.delcol', function () { + + if ($(this).hasClass('disabled')) { + return false; + } + + var colid = parseInt($(this).closest('tr').children().index($(this).parent('th')), 10); + + colnumber -= 1; + + checkButtons(); + + $(this).parent('th').remove(); + + $table.find('tbody tr').each(function () { + $(this).find('td:eq(' + colid + ')').remove(); + }); + + return false; + }); + + // Add row + $table.on('click', '.addrow', function () { + + if ($(this).hasClass('disabled')) { + return false; + } + + rownumber += 1; + + $(this).closest('tr').after(buildRow(0, colnumber)); + + $table.find('.delrow').removeClass('disabled'); + + checkButtons(); + + // Addition, raise callback after row has been added. + if(addRowCallBack) + { + addRowCallBack(rownumber, $(this).closest('tr').next('tr'), $table); + } + + return false; + }); + + // Delete row + $table.on('click', '.delrow', function () { + + if ($(this).hasClass('disabled')) { + return false; + } + + rownumber -= 1; + + checkButtons(); + + $(this).closest('tr').remove(); + + $table.find('.addrow').removeClass('disabled'); + + return false; + }); + + // Select all content on click + $table.on('click', 'input', function () { + $(this).select(); + }); + + // Return functions + return { + // Get an array of data + getData: function () { + return exportData(); + }, + // Get the JSON rappresentation of data + getJsonData: function () { + return JSON.stringify(exportData()); + }, + // Load an array of data + loadData: function (data) { + reset = data; + fillTableData(data); + }, + // Load a JSON rappresentation of data + loadJsonData: function (data) { + reset = JSON.parse(data); + fillTableData(JSON.parse(data)); + }, + // Extension method to change or set the table header. This method should be called before fillTableData. + setHeaders: function (headers) { + setTableHeader(headers); + }, + // Extension method to change or set the table column types. This method should be called before fillTableData. + setTypes: function (types) { + setTableType(types); + }, + // Reset data to the first instance + reset: function () { + fillTableData(reset); + }, + isValidated: function () { + return is_validated; + }, + addRowCallBack: function (func) { + addRowCallBack = func; + } + }; + }; + +})(jQuery, this, 0); diff --git a/webserver/js/jquery.edittable.min.js b/webserver/js/jquery.edittable.min.js deleted file mode 120000 index 0c2173e..0000000 --- a/webserver/js/jquery.edittable.min.js +++ /dev/null @@ -1 +0,0 @@ -../../../sharpkey/webserver/js/jquery.edittable.min.js \ No newline at end of file diff --git a/webserver/js/jquery.edittable.min.js b/webserver/js/jquery.edittable.min.js new file mode 100644 index 0000000..c2527ec --- /dev/null +++ b/webserver/js/jquery.edittable.min.js @@ -0,0 +1,2 @@ +/*! editTable v0.2.0 by Alessandro Benoit */ +(function(e,t,n){"use strict";e.fn.editTable=function(t){function p(e,t){e=e===0?"0":e||"";if(t&&"text"!==t){var n=r.field_templates[t];return""+n.setValue(n.html,e)[0].outerHTML+""}return''}function d(t,n){var i="",s;t=t||"";if(!r.row_template){for(s=0;s<(n||t.length);s+=1){i+=p(t[s])}}else{for(s=0;s",{html:i+'+ -'})}function v(){if(f<2){u.find(".delcol").addClass("disabled")}if(l<2){u.find(".delrow").addClass("disabled")}if(r.maxRows&&l===r.maxRows){u.find(".addrow").addClass("disabled")}}function m(e){var t,n=Math.min(r.maxRows,e.length);u.html(o);if(r.headerCols||r.row_template){var i=r.headerCols||r.row_template;for(t=0;t"+s+"")}for(t=0;t");f=u.find("thead th").length-1;l=u.find("tbody tr").length;v()}function g(){var t=0,n=[],i;h=true;u.find("tbody tr").each(function(){t+=1;n[t]=[];e(this).find("td:not(:last-child)").each(function(s,o){if(r.row_template&&"text"!==r.row_template[s]){var u=r.field_templates[r.row_template[s]],a=e(this).find(e(u.html).prop("tagName"));i=u.getValue(a);if(!r.validate_field(s,i,r.row_template[s],a)){h=false}n[t].push(i)}else{i=e(this).find('input[type="text"]').val();if(!r.validate_field(s,i,"text",o)){h=false}n[t].push(i)}})});n.splice(0,1);return n}var r=e.extend({data:[[""]],tableClass:"inputtable",jsonData:false,headerCols:false,maxRows:999,first_row:true,row_template:false,field_templates:false,validate_field:function(e,t,n,r){return true}},t),s=e(this),o="",u=e("",{"class":r.tableClass+(r.first_row?" wh":""),html:o}),a='',f,l,c,h=true;n=n+1;if(s.is("textarea")){try{c=JSON.parse(s.val())}catch(y){c=r.data}s.after(u);if(u.parents("form").length>0){u.parents("form").submit(function(){s.val(JSON.stringify(g()))})}}else{c=JSON.parse(r.jsonData)||r.data;s.append(u)}m(c);u.on("click",".addcol",function(){var t=parseInt(e(this).closest("tr").children().index(e(this).parent("th")),10);f+=1;u.find("thead tr").find("th:eq("+t+")").after(a);u.find("tbody tr").each(function(){e(this).find("td:eq("+t+")").after(p())});u.find(".delcol").removeClass("disabled");return false});u.on("click",".delcol",function(){if(e(this).hasClass("disabled")){return false}var t=parseInt(e(this).closest("tr").children().index(e(this).parent("th")),10);f-=1;v();e(this).parent("th").remove();u.find("tbody tr").each(function(){e(this).find("td:eq("+t+")").remove()});return false});u.on("click",".addrow",function(){if(e(this).hasClass("disabled")){return false}l+=1;e(this).closest("tr").after(d(0,f));u.find(".delrow").removeClass("disabled");v();return false});u.on("click",".delrow",function(){if(e(this).hasClass("disabled")){return false}l-=1;v();e(this).closest("tr").remove();u.find(".addrow").removeClass("disabled");return false});u.on("click","input",function(){e(this).select()});return{getData:function(){return g()},getJsonData:function(){return JSON.stringify(g())},loadData:function(e){m(e)},loadJsonData:function(e){m(JSON.parse(e))},reset:function(){m(c)},isValidated:function(){return h}}}})(jQuery,this,0) \ No newline at end of file diff --git a/webserver/js/jquery.min.js.gz b/webserver/js/jquery.min.js.gz deleted file mode 120000 index 53701da..0000000 --- a/webserver/js/jquery.min.js.gz +++ /dev/null @@ -1 +0,0 @@ -../../../sharpkey/webserver/js/jquery.min.js.gz \ No newline at end of file diff --git a/webserver/js/jquery.min.js.gz b/webserver/js/jquery.min.js.gz new file mode 100644 index 0000000000000000000000000000000000000000..ccc6d9369dc63e00a977e7231338a121a6846d27 GIT binary patch literal 33465 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 z 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 keymap file, please wait...

"); + req.send(); +} + +// Listener for the Download button click. Once clicked commence download via ajax function. +// +document.getElementById('keymapDownload').onclick = function keymapFileDownload(e) +{ + downloadFile(); +} + + +// Listener for an event change on the INPUT file tag 'keymapUpload'. Once pressed and a filename available, this method +// commences transmission via a POST method to the SharpKey server. +document.getElementById('keymapUpload').onchange = function keymapFileUpload(e) +{ + var fileInput = document.getElementById("keymapUpload").files; + + // Reset the last status. Used to track state change. + lastStatus = 0; + + // Client side checks, no point initiating an upload if the file isnt valid! + if (fileInput.length == 0) + { + alert("No file selected!"); + + // Sane size check, a keymap entry is 6-12 bytes long, so a 1000 rows should be the max size. + } else if (fileInput[0].size > 12*1024) + { + alert("File size must be less than 12KB!"); + } else + { + var xhttp; + if(window.XMLHttpRequest) + { + xhttp = new XMLHttpRequest(); + } else + { + xhttp = new ActiveXObject("Microsoft.XMLHTTP"); + } + + // Install listeners to handle state change. + xhttp.onreadystatechange = function() + { + if (xhttp.readyState == 4) + { + lastStatus = xhttp.status; + if (xhttp.status == 200) + { + showMessage(5000, 'keymapMsg', "

Upload complete. Please press Reboot to activate new key map.

"); + } else if (xhttp.status == 500) + { + showMessage(5000, 'keymapMsg', "

Error: " + xhttp.responseText + " - Press Reboot

"); + } + else if (xhttp.status == 0) + { + showMessage(5000, 'keymapMsg', "

Error: Server closed the connection abrubtly, status unknown! - Please retry or press Reboot

"); + } else + { + showMessage(5000, 'keymapMsg', "

Error: " + xhttp.responseText + " - Press Reboot

"); + } + } + }; + showMessage(10000, 'keymapMsg', "

Uploading keymap file, please wait...

"); + xhttp.open("POST", "/keymap", true); + xhttp.send(fileInput[0]); + } +} + + +// Method to enable the correct side-bar menu for the underlying host interface. +function enableIfConfig() +{ + // Disable keymap if no host is connected to the SharpKey. KeyInterface is the base class which exists when + // no host was detected to invoke a host specific sub-class. + if(activeInterface === "KeyInterface ") + { + document.getElementById("keyMapAvailable").style.display = 'none'; + } + // Mouse interface active? + else if(activeInterface === "Mouse ") + { + document.getElementById("keyMapAvailable").style.display = 'none'; + document.getElementById("mouseCfgAvailable").style.display = 'compact'; + } else + { + document.getElementById("keyMapAvailable").style.display = 'compact'; + + // Secondary interface available? + if(secondaryInterface == "Mouse ") + { + document.getElementById("mouseCfgAvailable").style.display = 'compact'; + } else + { + document.getElementById("mouseCfgAvailable").style.display = 'none'; + } + } +} + +// Method to validate a hex input field. Basically call hexConvert and it will return a +// valid number, 0 if invalid or 255 if out of range. +function validateHexInput(event) +{ + event.target.value=hexConvert(event.target.value, false); + return true; +}; + +// Use variables to remember last popover and value as the popover mechanism, probably due to race states, isnt uniform. +// Also use the shown/hidden events and toggle as hide doesnt trigger reliably. +var $currentPopover = null; +var $currentPopoverVal = -1; +var $currentClass = null; +var $currentSelectCount = 0; +var $currentTimerId = null; +var $currentDataSetModified = false; + +function updateButtons() +{ + if($currentSelectCount == 2) + { + $('#keymapSwap').removeAttr('disabled'); + } else + { + $('#keymapSwap').attr('disabled', 'disabled'); + } + if($currentSelectCount > 0) + { + $('#keymapDelete').removeAttr('disabled'); + } else + { + $('#keymapDelete').attr('disabled', 'disabled'); + } + if($currentDataSetModified == true) + { + $('#keymapSave').removeAttr('disabled'); + $('#keymapReload').removeAttr('disabled'); + } else + { + $('#keymapSave').attr('disabled', 'disabled'); + $('#keymapReload').attr('disabled', 'disabled'); + } +} + +function addPopover(field, tableRow) +{ + + // Popover auto-hide timer alarm. + const popoverAlarm = { + + setup: function(timerId, timeout) { + if (timerId != null) { + timerId = this.cancel(timerId); + } + + timerId = setTimeout(function() { + + if($currentPopover) { + $currentPopover.popover('toggle'); + } + }, timeout); + + return(timerId); + }, + + cancel: function(timerId) { + clearTimeout(timerId); + timerId = null; + return(timerId); + }, + }; + + // Setup popover menus on each custom field to aid with conversion of a feature into a hex number. + $(field).popover({ + html: true, + title: function () { + + return $('#popover-' + field.className).find('.head').html(); + }, + content: function () { + return $('#popover-' + field.className).find('.content').html(); + } + }) + .on("show.bs.popover", function(e) + { + var className = this.getAttribute('class'); + var $target = $(e.target); + + // Detect a class change, user has gone from one column to another, need to close out current + // popover ready for initialisation of new. + if($currentClass != null && $currentClass != className) + { + if($currentPopover) + { + if($currentTimerId != 0) + { + $currentTimerId = popoverAlarm.cancel($currentTimerId); + } + + if ($currentPopover && ($currentPopover.get(0) != $target.get(0))) { + $currentPopover.popover('toggle'); + } + $currentPopover = null; + } + } + $currentClass = className; + + if($currentPopover) + { + $currentPopoverVal = 0; + $('#popover-' + field.className).find('input').each( function( index ) { + + if($(this).attr("checked") === "checked") + { + $currentPopoverVal += parseInt(this.getAttribute('data-value'), 10); + } + }); + } else + { + $currentPopoverVal = -1; + } + + // Go through all the checkboxes and setup the initial value. + $('#popover-' + field.className).find('input').each( function( index ) { + if((e.target.value & this.getAttribute('data-value')) == this.getAttribute('data-value')) + { + $(this).attr('checked', true); + } else + { + $(this).attr('checked', false); + } + }); + }) + .on("shown.bs.popover", function(e) + { + var $target = $(e.target); + + if ($currentPopover && ($currentPopover.get(0) != $target.get(0))) { + $currentPopover.popover('toggle'); + } + $currentPopover = $target; + + // Setup an alarm to remove a visible popover if not clicked closed. + $currentTimerId = popoverAlarm.setup($currentTimerId, 5000); + }) + .on("hidden.bs.popover", function(e) + { + var $target = $(e.target); + + if ($currentPopover && ($currentPopover.get(0) == $target.get(0))) { + if($currentPopoverVal == -1) + { + $currentPopoverVal = 0; + $('#popover-' + field.className).find('input').each( function( index ) { + + if($(this).attr("checked") === "checked") + { + $currentPopoverVal += parseInt(this.getAttribute('data-value'), 10); + } + }); + } + + if(e.target.value != hexConvert($currentPopoverVal, false)) + { + e.target.value = hexConvert($currentPopoverVal, false); + $currentDataSetModified = true; + updateButtons(); + } + $currentPopover = null; + // Cancel the alarm on the hidden popover. + $currentTimerId = popoverAlarm.cancel($currentTimerId); + } + $currentPopoverVal = -1; + }) + + .on("hide.bs.popover", function(e) + { + // $currentTimerId = popoverAlarm.cancel($currentTimerId); + }) + .on("click", function(e) + { + console.log('click: ' + e.target.value); + }) + .on("change", function(e) + { + if(e.target.className === 'checkbox') + { + if($(e.target).attr('checked') == 'checked') + { + $currentSelectCount += 1; + //$(e.target).attr("checked", true); + } + else if($(e.target).attr('checked') != 'checked') + { + $currentSelectCount -= 1; + //$(e.target).attr("checked", false); + } + } else + { + if(e.target.value != hexConvert($currentPopoverVal, false)) + { + e.target.value = hexConvert(e.target.value, false); + $currentDataSetModified = true; + } + } + updateButtons(); + }) + + .blur(function () { + }); + // End popover definition + + // Only attach popover handlers to classes in the first row, subsequent rows will use the same popover. + // + if(tableRow == 1) + { + var search = '#popover-' + field.className; + $(search + ' input').each(function () { + + $('body').on('click', '#' + this.id, function(e) { + + $currentTimerId = popoverAlarm.setup($currentTimerId, 5000); + + $(search + ' input').each(function() { + if(this.id === e.target.id) + { + $(this).attr("checked", e.target.checked ? true : false); + } + }); + }); + }); + } +} + +// Method to convert a string/numeric into a hex number and range check it to be within and 8bit unsigned range. +function hexConvert(inVal, invert) +{ + if(isNaN(inVal) == true && isNaN('0x' + inVal) == false) + { + inVal = parseInt(inVal, 16); + } + else if(isNaN(inVal) == false && typeof inVal == 'string' && inVal.length > 0) + { + if(inVal.toUpperCase().indexOf("0X") == 0) + { + inVal = parseInt(inVal, 16); + } else + { + inVal = parseInt(inVal, 10); + } + } + else if(isNaN(inVal) == true || inVal.length == 0) + { + inVal = 0; + } + if(inVal < 0) + { + inVal = 0; + } + else if(inVal > 255) + { + inVal = 255; + } + if(invert == true) + { + inVal = (~inVal) & 0xFF; + } + return('0x' + (inVal).toString(16).toUpperCase()); +} + +// Method to swap 2 tables rows. This is necessary as the keymap table has top down priority in mapping. +function swapKeyMapData() +{ + // Locals. + var firstRow = null; + var secondRow = null; + + // Get row numbers of the rows to be swapped. + var rowcnt = 1; + $('.inputtable tr .checkbox').each(function() { + if(firstRow == null && this.checked == true) + { + firstRow = rowcnt; + } else + if(firstRow != null && secondRow == null && this.checked == true) + { + secondRow = rowcnt; + } + rowcnt++; + }); + + // If rows were matched (should be due to count) then perform swap. + if(firstRow != null && secondRow != null) + { + $('.inputtable > tbody > tr:nth-child(' + firstRow + ')').replaceWith($('.inputtable > tbody > tr:nth-child(' + secondRow + ')').after($('.inputtable > tbody > tr:nth-child(' + firstRow + ')').clone(true))); + } + + // Update the modified state and update button state. + $currentDataSetModified = true; + updateButtons(); + + return true; +} + +// Method to delete 1 or multiple rows. An individual row can be deleted by the +/- buttons but multiple requires selection and then running through the table, deleting the selected rows. +function deleteKeyMapData() +{ + // Locals. + var toDeleteRows = []; + + // Get row numbers of the rows to be swapped. + var rowcnt = 1; + $('.inputtable tr .checkbox').each(function() { + if(this.checked == true) + { + toDeleteRows.push(rowcnt); + } + rowcnt++; + }); + + // Iterate through the array of rows to delete and actually perform the deletion. + // We work in reverse order, ie. deleting the last row first due to the index changing if we delete first to last. + for(var idx = toDeleteRows.length; idx > 0; idx--) + { + $('.inputtable > tbody > tr:nth-child(' + toDeleteRows[idx-1] + ')').remove(); + } + + // Reset the select counter and update button state. + $currentSelectCount = 0; + $currentDataSetModified = true; + updateButtons(); + + return true; +} + +// Method to save the current keymap table data to the interface. +function saveKeyMapData() +{ + var data = keymapTable.getData(); + + // Reset the last status. Used to track state change. + lastStatus = 0; + + // Client side checks, no point initiating an upload if the file isnt valid! + if(data.length == 0) + { + alert("No data to save!!"); + + } else + { + var xhttp; + if(window.XMLHttpRequest) + { + xhttp = new XMLHttpRequest(); + } else + { + xhttp = new ActiveXObject("Microsoft.XMLHTTP"); + } + + // Install listeners to handle state change. + xhttp.onreadystatechange = function() + { + if (xhttp.readyState == 4) + { + lastStatus = xhttp.status; + if (xhttp.status == 200) + { + showMessage(5000, 'keymapEditorMsg', "

Upload complete. Please press Reboot to activate new key map.

"); + + // Reset the data modified flag and update button state. + $currentDataSetModified = false; + updateButtons(); + } else if (xhttp.status == 500) + { + showMessage(5000, 'keymapEditorMsg', "

Error: " + xhttp.responseText + " - Press Reboot

"); + } + else if (xhttp.status == 0) + { + showMessage(5000, 'keymapEditorMsg', "

Error: Server closed the connection abrubtly, status unknown! - Please retry Save

"); + } else + { + showMessage(5000, 'keymapEditorMsg', "

Error: " + xhttp.responseText + " - Press Reboot

"); + } + } + }; + showMessage(5000, 'keymapEditorMsg', "

Uploading keymap data, please wait...

"); + xhttp.open("POST", "/keymap/table", true); + xhttp.send(JSON.stringify(data)); + } + + return true; +} + +// Method to reload the initial table data set. +function reloadKeyMapData() +{ + // Request reload of the initial data. + keymapTable.reset(); + + // Re-install the listeners as the old dataset was deleted. + setupTableListeners(); + + // Complete message. + showMessage(5000, 'keymapEditorMsg', "

Reload complete, data reset to initial values.

"); + + // Reset the select counter and update button state. + $currentSelectCount = 0; + $currentDataSetModified = false; + updateButtons(); + return true; +} + +// Callback function triggered after a row has been added to the edittable. Use the event to add popover listeners to the new fields. +// +function tableRowAdded(row, rowHandle, tableHandle) +{ + $(rowHandle).children().children().each(function() { + if(this.className !== "") + { + addPopover(this, 0); + } + }); +} + +// Method to install listeners on the table data for popover aids. +// +function setupTableListeners() +{ + // Setup a popover on each input field where a modal exists. + $('.inputtable >tbody > tr').each(function (tridx) { + + // We seperate out the Row from the input as we want to watch every input but only setup 1 popover per input type. + $(this).find('input').each(function (inpidx) { + if(this.className !== "") + { + addPopover(this, tridx); + } + }); + }); + // + // Setup a callback on new rows added to table. + keymapTable.addRowCallBack(tableRowAdded); +} + +$(document).ready(function() { + + // Load up the table headers, types and data. This is done with JQuery fetch as it is easier. The SharpKey is synchronous + // but fetch order can vary so we nest the requests to ensure all headers and types are in place before the data. + fetch('/data/keymap/table/headers') + .then((response) => { + return(response.json()); + }) + .then((headers) => { + keymapTable.setHeaders(headers); + + fetch('/data/keymap/table/types') + .then((response) => { + return(response.json()); + }) + .then((types) => { + keymapTable.setTypes(types); + + fetch('/data/keymap/table/data') + .then((response) => { + return(response.json()); + }) + .then((data) => { + keymapTable.loadData(data); + setupTableListeners(); + }); + }); + }); + + // Disable buttons, enabled when user action requires them. + $('#keymapSwap').attr('disabled', 'disabled'); + $('#keymapDelete').attr('disabled', 'disabled'); + $('#keymapSave').attr('disabled', 'disabled'); + $('#keymapReload').attr('disabled', 'disabled'); + + // Add listeners to the buttons. + $('#keymapSwap').on('click', function() { swapKeyMapData(); }); + $('#keymapDelete').on('click', function() { deleteKeyMapData(); }); + $('#keymapSave').on('click', function() { saveKeyMapData(); }); + $('#keymapReload').on('click', function() { reloadKeyMapData(); }); + + // Setup the menu options according to underlying interface. + enableIfConfig(); +}); diff --git a/webserver/js/mouse.js b/webserver/js/mouse.js deleted file mode 120000 index b04dd1c..0000000 --- a/webserver/js/mouse.js +++ /dev/null @@ -1 +0,0 @@ -../../../sharpkey/webserver/js/mouse.js \ No newline at end of file diff --git a/webserver/js/mouse.js b/webserver/js/mouse.js new file mode 100644 index 0000000..8c9536e --- /dev/null +++ b/webserver/js/mouse.js @@ -0,0 +1,141 @@ +// Method to display a message in a message field. The existing html is saved and replaced +// with the new html. After a timeout period the original html is restored. +// +$origMessage = null; +$origId = null; +$msgTimerId = null; +function showMessage(timeout, id, message) +{ + // Is this a new message whilst one is active? + if($origMessage !== null) + { + // Cancel timer and restore original message. + clearTimeout($msgTimerId); + $('#' + $origId).html($origMessage); + } + + // Store original message and Id so that on timer expiry it can be replaced.. + $origMessage = $('#' + id).html(); + $origId = id; + + // Change HTML and set timer to restore it. + $('#' + id).html(message); + $msgTimerId = setTimeout(function(msgFieldId) + { + $('#' + msgFieldId).html($origMessage); + $origMessage = null; + }, timeout, id); +} + +// Method to enable the correct side-bar menu for the underlying host interface. +function enableIfConfig() +{ + // Disable keymap if no host is connected to the SharpKey. KeyInterface is the base class which exists when + // no host was detected to invoke a host specific sub-class. + if(activeInterface === "KeyInterface ") + { + document.getElementById("keyMapAvailable").style.display = 'none'; + } + // Mouse interface active? + else if(activeInterface === "Mouse ") + { + document.getElementById("keyMapAvailable").style.display = 'none'; + document.getElementById("mouseCfgAvailable").style.display = 'compact'; + } else + { + document.getElementById("keyMapAvailable").style.display = 'compact'; + + // Secondary interface available? + if(secondaryInterface == "Mouse ") + { + document.getElementById("mouseCfgAvailable").style.display = 'compact'; + } else + { + document.getElementById("mouseCfgAvailable").style.display = 'none'; + } + } +} + +// Method to convert a string/numeric into a hex number and range check it to be within and 8bit unsigned range. +function hexConvert(inVal, invert) +{ + if(isNaN(inVal) == true && isNaN('0x' + inVal) == false) + { + inVal = parseInt(inVal, 16); + } + else if(isNaN(inVal) == false && typeof inVal == 'string' && inVal.length > 0) + { + if(inVal.toUpperCase().indexOf("0X") == 0) + { + inVal = parseInt(inVal, 16); + } else + { + inVal = parseInt(inVal, 10); + } + } + else if(isNaN(inVal) == true || inVal.length == 0) + { + inVal = 0; + } + if(inVal < 0) + { + inVal = 0; + } + else if(inVal > 255) + { + inVal = 255; + } + if(invert == true) + { + inVal = (~inVal) & 0xFF; + } + return('0x' + (inVal).toString(16).toUpperCase()); +} + +$(document).ready(function() { + + // Setup the menu options according to underlying interface. + enableIfConfig(); + + // AJAX code to post in a controlled manner so that we can receive back an error/success message to the commit. + $("#mouseHostCfgSave").submit( function(e) + { + var form = $(this); + var actionUrl = form.attr('action'); + + // Prevent default submit action, we want to manually submit and be able to receive a response for errors/success. + e.preventDefault(); + + $.ajax( + { + type: "POST", + url: actionUrl, + data: form.serialize(), // serializes the form's elements. + success: function(data) + { + // Show the message then revert back to original text. + showMessage(10000, "mouseHostCfgMsg", data); + } + }); + }); + $("#mousePS2CfgSave").submit( function(e) + { + var form = $(this); + var actionUrl = form.attr('action'); + + // Prevent default submit action, we want to manually submit and be able to receive a response for errors/success. + e.preventDefault(); + + $.ajax( + { + type: "POST", + url: actionUrl, + data: form.serialize(), // serializes the form's elements. + success: function(data) + { + // Show the message then revert back to original text. + showMessage(10000, "mousePS2CfgMsg", data); + } + }); + }); +}); diff --git a/webserver/js/ota.js b/webserver/js/ota.js deleted file mode 120000 index 7086b86..0000000 --- a/webserver/js/ota.js +++ /dev/null @@ -1 +0,0 @@ -../../../sharpkey/webserver/js/ota.js \ No newline at end of file diff --git a/webserver/js/ota.js b/webserver/js/ota.js new file mode 100644 index 0000000..522fc25 --- /dev/null +++ b/webserver/js/ota.js @@ -0,0 +1,300 @@ +var lastStatus = 0; + +// Method to display a message in a message field. The existing html is saved and replaced +// with the new html. After a timeout period the original html is restored. +// +$origMessage = null; +$origId = null; +$msgTimerId = null; +function showMessage(timeout, id, message) +{ + // Is this a new message whilst one is active? + if($origMessage !== null) + { + // Cancel timer and restore original message. + clearTimeout($msgTimerId); + $('#' + $origId).html($origMessage); + } + + // Store original message and Id so that on timer expiry it can be replaced.. + $origMessage = $('#' + id).html(); + $origId = id; + + // Change HTML and set timer to restore it. + $('#' + id).html(message); + $msgTimerId = setTimeout(function(msgFieldId) + { + $('#' + msgFieldId).html($origMessage); + $origMessage = null; + }, timeout, id); +} + +// Clear the file input array. +function clearFileInput(ctrl) +{ + try { + ctrl.value = null; + } catch(ex) { } + if (ctrl.value) { + ctrl.parentNode.replaceChild(ctrl.cloneNode(true), ctrl); + } +} + +// Firmware handlers. +document.getElementById('firmwareUpload').onchange = function getFirmwareFileName(e) +{ + var default_path = document.getElementById("firmwareUpload").files[0].name; + + // Put the name of the file into the table cell. + document.getElementById('firmwareName').innerHTML = "=>" + default_path; + document.getElementById('firmwareUpgrade').value = document.getElementById('firmwareUpload').files[0].name; + + // Disable select and enable upgrade/cancel. + document.getElementById('firmwareUploadLabel').style.display = 'none'; + document.getElementById('firmwareUpgrade').disabled = false; + document.getElementById('firmwareUpgrade').style.display = 'block'; + document.getElementById('firmwareCancel').disabled = false; + document.getElementById('firmwareCancel').style.display = 'block'; + document.getElementById('firmwareMsg').innerHTML = "Press Upgrade to upload and flash the firmware into the SharpKey or Cancel to cancel and re-select file."; +} +document.getElementById('firmwareCancel').onclick = function cancelFirmwareUpload(e) +{ + var default_path = document.getElementById("firmwareUpload").files[0].name; + + // Reset the selected filename. + document.getElementById('firmwareName').innerHTML = ""; + document.getElementById('firmwareUpgrade').value = []; + clearFileInput(document.getElementById('firmwareUpload')); + + // Enable select and disable upgrade/cancel. + document.getElementById('firmwareUploadLabel').style.display = 'block'; + document.getElementById('firmwareUpgrade').disabled = true; + document.getElementById('firmwareUpgrade').style.display = 'none'; + document.getElementById('firmwareCancel').disabled = true; + document.getElementById('firmwareCancel').style.display = 'none'; + document.getElementById('firmwareMsg').innerHTML = "Select a firmware image file with which to upgrade the SharpKey Operating System."; +} + +// Firmware upgrade handler. +function firmwareUpdateProgress(e) +{ + if (e.lengthComputable) + { + var percentage = Math.round((e.loaded/e.total)*100); + document.getElementById('firmwareProgressBar').style.width = percentage + '%'; + } + else + { + document.getElementById('firmwareProgressBar').innerHTML = "Unable to compute progress information since the total size is unknown"; + } +} +document.getElementById('firmwareUpgrade').onclick = function firmwareUpload() +{ + var fileInput = document.getElementById("firmwareUpload").files; + + // Reset the last status. Used to track state change. + lastStatus = 0; + + // Client side checks, no point initiating a firmware update if the file isnt valid! + if (fileInput.length == 0) + { + alert("No file selected!"); + } else if (fileInput[0].size > 1664*1024) + { + alert("File size must be less than 1.6MB!"); + } else + { + document.getElementById("firmwareUpload").disabled = true; + document.getElementById("firmwareUploadLabel").style.display = 'none'; + document.getElementById("firmwareUpgrade").disabled = true; + document.getElementById("firmwareUpgrade").style.display = 'none'; + document.getElementById("firmwareCancel").style.display = 'none'; + document.getElementById("firmwareProgress").style.display = 'block'; + + var xhttp; + if(window.XMLHttpRequest) + { + xhttp = new XMLHttpRequest(); + } else + { + xhttp = new ActiveXObject("Microsoft.XMLHTTP"); + } + + // Install listeners to + xhttp.upload.addEventListener("progress", firmwareUpdateProgress, false); + xhttp.onreadystatechange = function() + { + if (xhttp.readyState == 4) + { + lastStatus = xhttp.status; + if (xhttp.status == 200) + { + document.getElementById('firmwareMsg').innerHTML = "

Transfer complete, firmware successfully flashed onto the SharpKey. Please press Reboot to activate.

"; + document.getElementById("firmwareProgress").style.display = 'none'; + document.getElementById('firmwareName').style.display = 'none'; + } else if (xhttp.status == 500) + { + document.getElementById('firmwareMsg').innerHTML = "

Error: " + xhttp.responseText + " - Press Reboot

"; + } + else if (xhttp.status == 0) + { + document.getElementById('firmwareMsg').innerHTML = "

Error: Server closed the connection abrubtly, flash status unknown! - Press Reboot

"; + } else + { + document.getElementById('firmwareMsg').innerHTML = "

Error: " + xhttp.responseText + " - Press Reboot

"; + } + } + }; + document.getElementById('firmwareMsg').innerHTML = "

Uploading and flashing the new firmware, please wait...

"; + xhttp.open("POST", "/ota/firmware", true); + xhttp.send(fileInput[0]); + } +} + +// Filepack handlers. +document.getElementById('filepackUpload').onchange = function getFilepackFileName(e) +{ + var default_path = document.getElementById("filepackUpload").files[0].name; + + // Put the name of the file into the table cell. + document.getElementById('filepackName').innerHTML = "=>" + default_path; + document.getElementById('filepackUpgrade').value = document.getElementById('filepackUpload').files[0].name; + + // Disable select and enable upgrade/cancel. + document.getElementById('filepackUploadLabel').style.display = 'none'; + document.getElementById('filepackUpgrade').disabled = false; + document.getElementById('filepackUpgrade').style.display = 'block'; + document.getElementById('filepackCancel').disabled = false; + document.getElementById('filepackCancel').style.display = 'block'; + document.getElementById('filepackWarning').style.display = 'block'; + document.getElementById('filepackMsg').innerHTML = "Press Upgrade to upload and flash the filepack onto the SharpKey filesystem or Cancel to cancel and re-select file."; +} +document.getElementById('filepackCancel').onclick = function cancelFilepackUpload(e) +{ + var default_path = document.getElementById("filepackUpload").files[0].name; + + // Reset the selected filename. + document.getElementById('filepackName').innerHTML = ""; + clearFileInput(document.getElementById('filepackUpload')); + + // Enable select and disable upgrade/cancel. + document.getElementById('filepackUploadLabel').style.display = 'block'; + document.getElementById('filepackUpgrade').disabled = true; + document.getElementById('filepackUpgrade').style.display = 'none'; + document.getElementById('filepackCancel').disabled = true; + document.getElementById('filepackCancel').style.display = 'none'; + document.getElementById('filepackWarning').style.display = 'none'; + document.getElementById('filepackMsg').innerHTML = "Select a filepack image file with which to upgrade the SharpKey filesystem."; +} + +// Filepack upgrade handler. +function filepackUpdateProgress(e) +{ + if (e.lengthComputable) + { + var percentage = Math.round((e.loaded/e.total)*100); + document.getElementById('filepackProgressBar').style.width = percentage + '%'; + } + else + { + document.getElementById('filepackProgressBar').innerHTML = "Unable to compute progress information since the total size is unknown"; + } +} +document.getElementById('filepackUpgrade').onclick = function filepackUpload() +{ + var fileInput = document.getElementById("filepackUpload").files; + + // Reset the last status. Used to track state change. + lastStatus = 0; + + // Client side checks, no point initiating a filepack update if the file isnt valid! + if (fileInput.length == 0) + { + alert("No file selected!"); + } else if (fileInput[0].size > 640*1024) + { + alert("File size must be less than 640K!"); + } else + { + document.getElementById("filepackUpload").disabled = true; + document.getElementById("filepackUploadLabel").style.display = 'none'; + document.getElementById("filepackUpgrade").disabled = true; + document.getElementById("filepackUpgrade").style.display = 'none'; + document.getElementById("filepackCancel").style.display = 'none'; + document.getElementById("filepackProgress").style.display = 'block'; + + var xhttp; + if(window.XMLHttpRequest) + { + xhttp = new XMLHttpRequest(); + } else + { + xhttp = new ActiveXObject("Microsoft.XMLHTTP"); + } + + // Install listeners to + xhttp.upload.addEventListener("progress", filepackUpdateProgress, false); + xhttp.onreadystatechange = function() + { + if (xhttp.readyState == 4) + { + lastStatus = xhttp.status; + if (xhttp.status == 200) + { + document.getElementById('filepackMsg').innerHTML = "

Transfer complete, filepack successfully flashed onto SharpKey filesystem. Please press Reboot to activate.

"; + document.getElementById("filepackProgress").style.display = 'none'; + document.getElementById('filepackName').style.display = 'none'; + } else if (xhttp.status == 500) + { + document.getElementById('filepackMsg').innerHTML = "

Error: " + xhttp.responseText + " - Press Reboot

"; + } + else if (xhttp.status == 0) + { + document.getElementById('filepackMsg').innerHTML = "

Error: Server closed the connection abrubtly, flash status unknown! - Press Reboot

"; + } else + { + document.getElementById('filepackMsg').innerHTML = "

Error: " + xhttp.responseText + " - Press Reboot

"; + } + } + }; + document.getElementById('filepackMsg').innerHTML = "

Uploading and flashing the new filepack, please wait...

"; + xhttp.open("POST", "/ota/filepack", true); + xhttp.send(fileInput[0]); + } +} + + +// Method to enable the correct side-bar menu for the underlying host interface. +function enableIfConfig() +{ + // Disable keymap if no host is connected to the SharpKey. KeyInterface is the base class which exists when + // no host was detected to invoke a host specific sub-class. + if(activeInterface === "KeyInterface ") + { + document.getElementById("keyMapAvailable").style.display = 'none'; + } + // Mouse interface active? + else if(activeInterface === "Mouse ") + { + document.getElementById("keyMapAvailable").style.display = 'none'; + document.getElementById("mouseCfgAvailable").style.display = 'compact'; + } else + { + document.getElementById("keyMapAvailable").style.display = 'compact'; + + // Secondary interface available? + if(secondaryInterface == "Mouse ") + { + document.getElementById("mouseCfgAvailable").style.display = 'compact'; + } else + { + document.getElementById("mouseCfgAvailable").style.display = 'none'; + } + } +} + +// On document load, setup the items viewable on the page according to set values. +document.addEventListener("DOMContentLoaded", function setPageDefaults() +{ + enableIfConfig(); +}); diff --git a/webserver/js/wifimanager.js b/webserver/js/wifimanager.js deleted file mode 120000 index 16fdd44..0000000 --- a/webserver/js/wifimanager.js +++ /dev/null @@ -1 +0,0 @@ -../../../sharpkey/webserver/js/wifimanager.js \ No newline at end of file diff --git a/webserver/js/wifimanager.js b/webserver/js/wifimanager.js new file mode 100644 index 0000000..7927ed3 --- /dev/null +++ b/webserver/js/wifimanager.js @@ -0,0 +1,178 @@ +// Method to display a message in a message field. The existing html is saved and replaced +// with the new html. After a timeout period the original html is restored. +// +$origMessage = null; +$origId = null; +$msgTimerId = null; +function showMessage(timeout, id, message) +{ + // Is this a new message whilst one is active? + if($origMessage !== null) + { + // Cancel timer and restore original message. + clearTimeout($msgTimerId); + $('#' + $origId).html($origMessage); + } + + // Store original message and Id so that on timer expiry it can be replaced.. + $origMessage = $('#' + id).html(); + $origId = id; + + // Change HTML and set timer to restore it. + $('#' + id).html(message); + $msgTimerId = setTimeout(function(msgFieldId) + { + $('#' + msgFieldId).html($origMessage); + $origMessage = null; + }, timeout, id); +} + + +function showWiFiAPInput() +{ + document.getElementById('inputWiFiClient').style.display = 'none'; + document.getElementById('inputWiFiAP').style.display = 'block'; + document.getElementById("errorMsgAP").innerHTML = ""; +} + +function showWiFiClientInput() +{ + document.getElementById('inputWiFiClient').style.display = 'block'; + document.getElementById('inputWiFiAP').style.display = 'none'; + document.getElementById("errorMsgClient").innerHTML = ""; +} + +function hideFixedIPInput() +{ + document.getElementById('rowClientIP').style.display = 'none'; + document.getElementById('rowClientNETMASK').style.display = 'none'; + document.getElementById('rowClientGATEWAY').style.display = 'none'; + document.getElementById('dhcpInput').style.display = 'block'; + document.getElementById("errorMsgClient").innerHTML = ""; +} + +function showFixedIPInput() +{ + document.getElementById('rowClientIP').style.display = 'table-row'; + document.getElementById('rowClientNETMASK').style.display = 'table-row'; + document.getElementById('rowClientGATEWAY').style.display = 'table-row'; + document.getElementById('dhcpInput').style.display = 'none'; + document.getElementById("errorMsgClient").innerHTML = ""; +} + +function showIPConfig() +{ + if(document.getElementById("wifiCfg0checked")) + { + document.getElementById("wifiCfg0checked").style.display = 'compact'; + document.getElementById("wifiCfg").style.display = 'none'; + + if(document.getElementById("wifiCfg3checked")) + { + document.getElementById("wifiCfg3checked").style.display = 'compact'; + document.getElementById("wifiCfg3").style.display = 'none'; + } else + { + document.getElementById("wifiCfg3checked").style.display = 'none'; + document.getElementById("wifiCfg3").style.display = 'compact'; + } + + if(document.getElementById("wifiCfg1checked")) + { + document.getElementById("wifiCfg1checked").style.display = 'compact'; + } else + { + document.getElementById("wifiCfg1").style.display = 'none'; + } + } else + { + document.getElementById("wifiCfg0").style.display = 'none'; + document.getElementById("wifiCfgchecked").style.display = 'compact'; + } +} + + +// Method to enable the correct side-bar menu for the underlying host interface. +function enableIfConfig() +{ + // Disable keymap if no host is connected to the SharpKey. KeyInterface is the base class which exists when + // no host was detected to invoke a host specific sub-class. + if(activeInterface === "KeyInterface ") + { + document.getElementById("keyMapAvailable").style.display = 'none'; + } + // Mouse interface active? + else if(activeInterface === "Mouse ") + { + document.getElementById("keyMapAvailable").style.display = 'none'; + document.getElementById("mouseCfgAvailable").style.display = 'compact'; + } else + { + document.getElementById("keyMapAvailable").style.display = 'compact'; + + // Secondary interface available? + if(secondaryInterface == "Mouse ") + { + document.getElementById("mouseCfgAvailable").style.display = 'compact'; + } else + { + document.getElementById("mouseCfgAvailable").style.display = 'none'; + } + } +} + +// On document load, setup the items viewable on the page according to set values. +document.addEventListener("DOMContentLoaded", function setPageDefaults() +{ + document.getElementById('wifiModeAccessPoint').onclick = showWiFiAPInput; + document.getElementById('wifiModeClient').onclick = showWiFiClientInput; + document.getElementById('dhcpModeEnabled').onclick = hideFixedIPInput; + document.getElementById('dhcpModeDisabled').onclick = showFixedIPInput; + + // Setup AP/Client display. + if(document.getElementById('wifiModeClient').checked) + { + showWiFiClientInput(); + } else + { + showWiFiAPInput(); + } + if(document.getElementById('dhcpModeEnabled').checked) + { + hideFixedIPInput(); + } else + { + showFixedIPInput(); + } + + // AJAX code to post in a controlled manner so that we can receive back an error/success message to the commit. + $("#wifiman").submit( function(e) + { + var form = $(this); + var actionUrl = form.attr('action'); + + // Prevent default submit action, we want to manually submit and be able to receive a response for errors/success. + e.preventDefault(); + + // Clear message window before making a POST, allows for a new message if one is provided or blank if it isnt. + document.getElementById("errorMsgClient").innerHTML = ""; + document.getElementById("errorMsgAP").innerHTML = ""; + + $.ajax( + { + type: "POST", + url: actionUrl, + data: form.serialize(), // serializes the form's elements. + success: function(data) + { + // JQuery not rendering HTML correcly so revert to DOM. + document.getElementById("errorMsgClient").innerHTML = data; + document.getElementById("errorMsgAP").innerHTML = data; + //form.find('#errorMsg').html(data); + } + }); + }); + + showIPConfig(); + enableIfConfig(); +}); diff --git a/webserver/keymap.html b/webserver/keymap.html deleted file mode 120000 index eba4287..0000000 --- a/webserver/keymap.html +++ /dev/null @@ -1 +0,0 @@ -../../sharpkey/webserver/keymap.html \ No newline at end of file diff --git a/webserver/keymap.html b/webserver/keymap.html new file mode 100644 index 0000000..3abf816 --- /dev/null +++ b/webserver/keymap.html @@ -0,0 +1,239 @@ + + + + + + + + + Dashboard - SharpKey Admin + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+

%SK_CURRENTIF%KeyMap

+ +
+ +

Configure the keyboard mapping of the interface from PS/2 to %SK_CURRENTIF% via the table below. The priority is top down and a copy can be saved or uploaded to/from your disk for backup or offline editting.

+
+
+
+ + +
+
+
+
+

KeyMap Editor

+
+
+
+%SK_KEYMAPPOPOVER% + +
+
+
+

+

Directly edit the table, click to add a row, to delete, select () 2 rows to Swap or select multiple rows for multi-row Delete.
Press Save to commit changes or Reload to discard changes and reload active i/f map.

+
+
+ -
+ + + + + + + + +
+ + + + + + + +
+ + + + + + + + +
+
+
+
+

Direct KeyMap Access

+
+
+
+
+

The KeyMap for the %SK_CURRENTIF% interface can be directly manipulated via the buttons below. You can download the file for backup or to modify locally then upload to restore or implement changes.

+

Select Upload file (to SharpKey) or Download (to your PC) via the buttons below.

+ + + + + + + +
+ + + + +
+
+
+
+
+
+
+ + + + + + + + + + + diff --git a/webserver/mouse.html b/webserver/mouse.html deleted file mode 120000 index 6b4aa00..0000000 --- a/webserver/mouse.html +++ /dev/null @@ -1 +0,0 @@ -../../sharpkey/webserver/mouse.html \ No newline at end of file diff --git a/webserver/mouse.html b/webserver/mouse.html new file mode 100644 index 0000000..dc2322e --- /dev/null +++ b/webserver/mouse.html @@ -0,0 +1,167 @@ + + + + + + + + + Dashboard - SharpKey Admin + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+

Mouse Config

+ +
+ +

Configure the Mouse settings in the panel below. Some of the mouse settings can be configured by the mouse wheel, please refer to the documentation for details.

+
+
+
+ + +
+
+
+
+

Mouse Host Configuration

+
+
+
+

Host Scaling

+
+ %SK_MOUSEHOSTSCALING% +
+
+
+

Setup the host side mouse interface parameters. Commit changes by pressing Save.

+
+
+
+ + + + + + +
+ +
+
+
+
+
+
+
+ +
+
+
+
+

Mouse Configuration

+
+
+
+

Mouse Scaling

+
+ %SK_MOUSEPS2SCALING% +
+

Mouse Resolution

+
+ %SK_MOUSEPS2RESOLUTION% +
+

Mouse Sampling Rate

+
+ %SK_MOUSEPS2SAMPLERATE% +
+
+
+

Setup the mouse parameters which are PS/2 settings, Bluetooth mice will use these parameters via internal mapping.
Commit changes by pressing Save.

+
+
+
+ + + + + + +
+ +
+
+ +
+
+
+
+ +
+ + + + + + + + + diff --git a/webserver/ota.html b/webserver/ota.html deleted file mode 120000 index a5f4c13..0000000 --- a/webserver/ota.html +++ /dev/null @@ -1 +0,0 @@ -../../sharpkey/webserver/ota.html \ No newline at end of file diff --git a/webserver/ota.html b/webserver/ota.html new file mode 100644 index 0000000..5e81850 --- /dev/null +++ b/webserver/ota.html @@ -0,0 +1,238 @@ + + + + + + + + + Dashboard - SharpKey Admin + + + + + + + + + + + + +
+ + + + +
+ +
+
+

Over The Air Update

+ +
+ +

Welcome to the SharpKey OTA Update page.

This page allows you to check current firmware/filepack version information and upgrade them as necessary. Please read the documentation if you need help.

+
+
+
+ +
+
+ +
+
+
+
+

Version Information

+
+
+
+
+ %SK_PRODNAME% v%SK_PRODVERSION% © P.D. Smart, 2018-22

+ Modules
+ %SK_MODULES% + File Pack
+ %SK_FILEPACK% +
+
+
+
+
+
+ +
+
+
+
+

ESP32 Partitions

+
+
+
+
+ + + + + + + + + + + + + + + %SK_PARTITIONS% + +
Partition NameTypeSub-TypeAddressSizeVersionTimestampActive (Running)
+
+
+
+
+
+
+ +
+
+
+
+

Firmware Upload

+
+
+
+
+

Firmware is a binary file containing the latest operating system for the SharpKey interface.

+
+

Select a firmware image file with which to upgrade the SharpKey Operating System.

+
+ + + + + + + + + + +
+ + +
+
+ + + + +
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+

File Pack Upload

+
+
+
+
+

A filepack is a SharpKey filesystem stored in a binary image file which contains static and template files for the webserver interface.

+ +
+

Select a filepack image file with which to upgrade the SharpKey filesystem.

+
+ + + + + + + + + + +
+ + +
+
+ + + + +
+
+
+
+
+
+
+
+
+
+
+ +
+ +
+ + + + + + + + diff --git a/webserver/ptable.bin b/webserver/ptable.bin deleted file mode 120000 index 9b9323d..0000000 --- a/webserver/ptable.bin +++ /dev/null @@ -1 +0,0 @@ -../../sharpkey/webserver/ptable.bin \ No newline at end of file diff --git a/webserver/ptable.bin b/webserver/ptable.bin new file mode 100644 index 0000000..eb5c099 --- /dev/null +++ b/webserver/ptable.bin @@ -0,0 +1 @@ +hello philip˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙and another test here˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ \ No newline at end of file diff --git a/webserver/version.txt b/webserver/version.txt deleted file mode 120000 index 73e7e0d..0000000 --- a/webserver/version.txt +++ /dev/null @@ -1 +0,0 @@ -../../sharpkey/webserver/version.txt \ No newline at end of file diff --git a/webserver/version.txt b/webserver/version.txt new file mode 100644 index 0000000..1010473 --- /dev/null +++ b/webserver/version.txt @@ -0,0 +1 @@ +1.02 diff --git a/webserver/wifimanager.html b/webserver/wifimanager.html deleted file mode 120000 index 923d18b..0000000 --- a/webserver/wifimanager.html +++ /dev/null @@ -1 +0,0 @@ -../../sharpkey/webserver/wifimanager.html \ No newline at end of file diff --git a/webserver/wifimanager.html b/webserver/wifimanager.html new file mode 100644 index 0000000..bf4c8e0 --- /dev/null +++ b/webserver/wifimanager.html @@ -0,0 +1,312 @@ + + + + + + + + + Dashboard - SharpKey Admin + + + + + + + + + + + + + +
+ + + + +
+ +
+
+

WiFi Manager

+ +
+ +

Welcome to the SharpKey WiFi Manager page.

This page allows you to configure the SharpKey WiFi Access Point (you connect to the SharpKey) or + Client (SharpKey connects to your network) mode.

+
+
+
+ +
+
+ +
+
+
+
+

WiFi Configuration

+
+
+
+
+ +
+ + + + + + + + + + + + + + +
SSID:%SK_APSSID%
Password:%SK_APPWD%
IP (AP):%SK_CURRENTIP%NETMASK:%SK_CURRENTNM%GATEWAY:%SK_CURRENTGW%
+
+
+ + + + + + + + + + + + + + + + + + + +
SSID:%SK_CLIENTSSID%
DHCP:Enabled
IP (assigned):%SK_CURRENTIP%NETMASK:%SK_CURRENTNM%GATEWAY:%SK_CURRENTGW%
IP (fixed):%SK_CURRENTIP%NETMASK:%SK_CURRENTNM%GATEWAY:%SK_CURRENTGW%
+
+
+
+
+
+
+
+ +
+
+
+
+

Configure WiFi

+
+
+
+
+
+ + + + + + + + +
WiFi Mode: +
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
Name of Wifi router to use. +
Password of WiFi router.
DHCP Mode: +
+ + +
+
+
+ + +
+
Static IP address to assign to SharpKey
Netmask for Static IP
Gateway or router IP Address (if needed).
+
+
+

DHCP Mode enabled.
The SharpKey will use the credentials specified above (SSID/Password) to connect to the router and obtain + an IP address, Netmask and Gateway. Use your router admin page to determine the IP address allocated and connect to the SharpKey via http://<assigned IP>

+
+
+
+ %SK_ERRMSG% +
+
+
+ + + + + + +
+
+
+
+ + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Name of this WiFi Access Point. +
Password required by this AP to authenticate clients.
IP address assigned to this Access Point. ie. 192.168.4.1
Netmask of this AP network.
Gateway this AP will assign to a client. Normally same as IP.
+
+
+
+ %SK_ERRMSG% +
+
+
+ + + + + + +
+
+
+
+
+
+
+
+
+
+ +
+ +
+ + + + + + + +
+ + + + +
+ +
+
+

Status

+ +
+ +

Welcome to the SharpKey embedded browser based configuration App which enables keyboard map changes, mouse configuration, Over The Air firmware updates and more.
No help text is available as NVRAM is at a premium, so please read the documentation if you need help.

+
+
+
+ +
+
+
+
+

WiFi Configuration

+
+
+
+
+
+ + + + + + + + + + + + + + +
SSID:%SK_APSSID%
Password:%SK_APPWD%
IP (AP):%SK_CURRENTIP%NETMASK:%SK_CURRENTNM%GATEWAY:%SK_CURRENTGW%
+
+
+ + + + + + + + + + + + + + + + + + + +
SSID:%SK_CLIENTSSID%
DHCP:Enabled
IP (assigned):%SK_CURRENTIP%NETMASK:%SK_CURRENTNM%GATEWAY:%SK_CURRENTGW%
IP (fixed):%SK_CURRENTIP%NETMASK:%SK_CURRENTNM%GATEWAY:%SK_CURRENTGW%
+
+
+
+
+
+
+
+ +
+
+
+
+

Version Information

+
+
+
+
+ %SK_PRODNAME% v%SK_PRODVERSION% © P.D. Smart, 2018-22

+ Modules
+ %SK_MODULES% + File Pack
+ %SK_FILEPACK% +
+
+
+
+
+
+ + +
+ +