diff --git a/CMakeLists.txt b/CMakeLists.txt index 1fa54af..5286b35 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,3 +9,17 @@ project(main) # Create the default filesystem with files loaded from the webserver directory. littlefs_create_partition_image(filesys webfs) + +idf_build_get_property(build_dir BUILD_DIR) +idf_build_get_property(elf_name EXECUTABLE_NAME GENERATOR_EXPRESSION) + +# Post-build: version management, filepack creation, and release packaging. +add_custom_command (OUTPUT ${CMAKE_SOURCE_DIR}/updated + DEPENDS "${build_dir}/.bin_timestamp" + COMMAND bash -c "${CMAKE_SOURCE_DIR}/backup_version.sh" + COMMAND bash -c "${CMAKE_SOURCE_DIR}/update_version.sh" + COMMAND bash -c "${CMAKE_SOURCE_DIR}/make_filepack.sh" + COMMAND bash -c "${CMAKE_SOURCE_DIR}/make_release.sh" +) + +add_custom_target(version ALL DEPENDS ${CMAKE_SOURCE_DIR}/updated) diff --git a/backup_version.sh b/backup_version.sh new file mode 100755 index 0000000..a47163e --- /dev/null +++ b/backup_version.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +PRJDIR=$(dirname ${PWD}) +ROOTDIR=/srv/dvlp/Projects +OLDVERSION=$(cat ${PRJDIR}/version.txt) +NEWVERSION=$(perl -e "$(echo "print $(cat ${PRJDIR}/version.txt)+0.01")") + +echo "Backing up version (${OLDVERSION}) to ${PRJDIR}/versions/sharpkey_${OLDVERSION}_$(date +'%y%m%d%H%M').tar.gz..." +cd ${PRJDIR} +tar -czf ${PRJDIR}/versions/sharpkey_${OLDVERSION}_$(date +'%y%m%d%H%M').tar.gz --exclude=build \ + backup_version.sh build_webfs.sh CMakeLists.txt license.txt \ + main make_filepack.sh make_release.sh \ + sharpkey_partition_table.csv sharpkey_version.txt \ + sdkconfig update_version.sh version.txt webfs +if [[ $? != 0 ]]; then + echo "Backup failure!" + cd ${PRJDIR} + exit 1 +fi diff --git a/build_webfs.sh b/build_webfs.sh index c2c2e82..5104a4a 100755 --- a/build_webfs.sh +++ b/build_webfs.sh @@ -4,21 +4,22 @@ SRCDIR=`pwd`/webserver WEBFSDIR=`pwd`/webfs echo "Building into:$WEBFSDIR from $SRCDIR..." -mkdir -p ${WEBFSDIR}/css -mkdir -p ${WEBFSDIR}/js -mkdir -p ${WEBFSDIR}/font-awesome -mkdir -p ${WEBFSDIR}/font-awesome/css -mkdir -p ${WEBFSDIR}/font-awesome/fonts -mkdir -p ${WEBFSDIR}/images +mkdir -p webfs/css +mkdir -p webfs/js +mkdir -p webfs/font-awesome +mkdir -p webfs/font-awesome/css +mkdir -p webfs/font-awesome/fonts +mkdir -p webfs/images (cd ${SRCDIR}/; cp favicon.ico ${WEBFSDIR}/ -cp version.txt ${WEBFSDIR}/ +cp webfs_version.txt ${WEBFSDIR}/ cp index.html ${WEBFSDIR}/ cp keymap.html ${WEBFSDIR}/keymap.html cp mouse.html ${WEBFSDIR}/mouse.html cp ota.html ${WEBFSDIR}/ota.html cp wifimanager.html ${WEBFSDIR}/wifimanager.html +cp hostconfig.html ${WEBFSDIR}/hostconfig.html (cd ${SRCDIR}/css; diff --git a/filepack_version.txt b/filepack_version.txt new file mode 100644 index 0000000..11a84ad --- /dev/null +++ b/filepack_version.txt @@ -0,0 +1 @@ +1.04 diff --git a/main/HID.cpp b/main/HID.cpp index ecb72a5..e4a34fc 100644 --- a/main/HID.cpp +++ b/main/HID.cpp @@ -886,30 +886,48 @@ void HID::init(const char *className, enum HID_DEVICE_TYPES deviceType) 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) + // PS/2 keyboards need 300-750ms after power-on to complete their BAT (Basic Assurance Test). + // Retry detection several times to avoid a race condition where the ESP32 boots faster than + // the keyboard and incorrectly falls back to Bluetooth. { - // 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); + bool ps2Found = false; + for(int retry = 0; retry < 10 && !ps2Found; retry++) + { + if(checkPS2Keyboard() == true) + { + ps2Found = true; + } else + { + ESP_LOGW(INITTAG, "PS2 keyboard not detected, retry %d/10...", retry + 1); + vTaskDelay(100); + } + } - // 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); + // If no PS/2 keyboard detected after retries then default to Bluetooth. + if(!ps2Found) + { + // 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; - hidCtrl.deviceType = HID_DEVICE_TYPE_BLUETOOTH; - hidCtrl.hidDevice = HID_DEVICE_BLUETOOTH; - } else - { - hidCtrl.deviceType = HID_DEVICE_TYPE_KEYBOARD; - hidCtrl.hidDevice = HID_DEVICE_PS2_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; } diff --git a/main/SharpKey.cpp b/main/SharpKey.cpp index 281fb55..2f94cc4 100644 --- a/main/SharpKey.cpp +++ b/main/SharpKey.cpp @@ -128,6 +128,9 @@ struct SharpKeyConfig { struct { uint8_t bootMode; // Flag to indicate the mode SharpKey should boot into. // 0 = Interface, 1 = WiFi (configured), 2 = WiFi (default). + uint32_t hostMode; // Host machine override. 0 = Auto (detect from hardware), + // 2500 = MZ-2500, 2800 = MZ-2800, 1 = X1, 68000 = X68000, + // 5600 = MZ-5600/6500, 9801 = PC-9801, 2 = Mouse. } params; } sharpKeyConfig; @@ -485,8 +488,15 @@ void startWiFi(NVS &nvs, LED *led, bool defaultMode, uint32_t ifMode) // 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) +uint32_t getHostType(bool eFuseInvalid, t_EFUSE sharpkeyEfuses, uint32_t hostMode) { + // If a host mode has been set via the web interface (stored in NVS), use it directly. + if(hostMode != 0) + { + ESP_LOGW(MAINTAG, "Host mode set via config: %d", hostMode); + return(hostMode); + } + // Locals. // uint32_t RTSNI_MASK = (1 << (CONFIG_HOST_RTSNI - 32)); @@ -905,6 +915,7 @@ void setup(NVS &nvs) { ESP_LOGW(SETUPTAG, "SharpKey configuration set to default, no valid config found in NVS."); sharpKeyConfig.params.bootMode = 0; + sharpKeyConfig.params.hostMode = 0; // Persist the data for next time. if(nvs.persistData(SHARPKEY_NAME, &sharpKeyConfig, sizeof(struct SharpKeyConfig)) == false) @@ -919,7 +930,7 @@ void setup(NVS &nvs) } // Get the host type SharpKey is connected with. - ifMode = getHostType(eFuseInvalid, sharpkeyEfuses); + ifMode = getHostType(eFuseInvalid, sharpkeyEfuses, sharpKeyConfig.params.hostMode); // If bootMode is for Wifi, start it. This has to be seperate due to a conflict with Bluetooth and WiFi which shares the same antenna. // Code is written to allow co-existence but it doesnt work so well in this project. diff --git a/main/WiFi.cpp b/main/WiFi.cpp index cf54535..7ff5e7a 100644 --- a/main/WiFi.cpp +++ b/main/WiFi.cpp @@ -57,6 +57,8 @@ #include #include #include +#include +#include #include #include "freertos/FreeRTOS.h" #include "freertos/task.h" @@ -78,6 +80,16 @@ #include "esp_littlefs.h" #include "WiFi.h" +// External reference to the SharpKey global configuration stored in NVS. +struct SharpKeyConfig { + struct { + uint8_t bootMode; + uint32_t hostMode; + } params; +}; +extern struct SharpKeyConfig sharpKeyConfig; +#define SHARPKEY_NAME "SharpKey" + // FreeRTOS event group to signal when we are connected static EventGroupHandle_t s_wifi_event_group; @@ -558,6 +570,16 @@ esp_err_t WiFi::sendMouseRadioChoice(httpd_req_t *req, const char *option) // esp_err_t WiFi::expandVarsAndSend(httpd_req_t *req, std::string str) { + // Fast path: template variables are always prefixed "%SK_". + // A line with no "%SK_" cannot contain any template variable, so skip the + // expensive pairs build (which includes flash partition enumeration via + // esp_partition_find / esp_ota_get_partition_description) and send directly. + if(str.find("%SK_") == std::string::npos) + { + str += "\n"; + return httpd_resp_send_chunk(req, str.c_str(), str.length()); + } + // Locals. // bool largeMacroDetected = false; @@ -590,6 +612,14 @@ esp_err_t WiFi::expandVarsAndSend(httpd_req_t *req, std::string str) 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_HOSTMODE_AUTO%"; keyValue.value = (sharpKeyConfig.params.hostMode == 0 ? "checked" : ""); pairs.push_back(keyValue); + keyValue.name = "%SK_HOSTMODE_MZ2500%"; keyValue.value = (sharpKeyConfig.params.hostMode == 2500 ? "checked" : ""); pairs.push_back(keyValue); + keyValue.name = "%SK_HOSTMODE_MZ2800%"; keyValue.value = (sharpKeyConfig.params.hostMode == 2800 ? "checked" : ""); pairs.push_back(keyValue); + keyValue.name = "%SK_HOSTMODE_X1%"; keyValue.value = (sharpKeyConfig.params.hostMode == 1 ? "checked" : ""); pairs.push_back(keyValue); + keyValue.name = "%SK_HOSTMODE_X68000%"; keyValue.value = (sharpKeyConfig.params.hostMode == 68000 ? "checked" : ""); pairs.push_back(keyValue); + keyValue.name = "%SK_HOSTMODE_MZ5600%"; keyValue.value = (sharpKeyConfig.params.hostMode == 5600 ? "checked" : ""); pairs.push_back(keyValue); + keyValue.name = "%SK_HOSTMODE_PC9801%"; keyValue.value = (sharpKeyConfig.params.hostMode == 9801 ? "checked" : ""); pairs.push_back(keyValue); + keyValue.name = "%SK_HOSTMODE_MOUSE%"; keyValue.value = (sharpKeyConfig.params.hostMode == 2 ? "checked" : ""); 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); @@ -778,39 +808,54 @@ esp_err_t WiFi::expandAndSendFile(httpd_req_t *req, const char *basePath, std::s 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. + // Read the entire file in one filesystem operation — dramatically faster than + // getline() per line (each getline triggers a separate fread, ~2-3ms per call; + // a 300-line file would cost 600-900ms in filesystem I/O alone). inFile.open(fqfn.c_str()); - while(result == ESP_OK && std::getline(inFile, line)) + if(!inFile.is_open()) { - // 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); + httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "File does not exist"); + return(ESP_FAIL); + } - // Respond with 500 Internal Server Error. - httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to send file"); - break; + std::string contents((std::istreambuf_iterator(inFile)), std::istreambuf_iterator()); + inFile.close(); + + if(contents.find("%SK_") == std::string::npos) + { + // No template variables — send entire file as a single HTTP chunk. + result = httpd_resp_send_chunk(req, contents.c_str(), contents.length()); + if(result == ESP_OK) + result = httpd_resp_send_chunk(req, NULL, 0); + } + else + { + // Template variables present — process line by line from the in-memory + // string (no filesystem I/O per line). + std::istringstream stream(contents); + while(result == ESP_OK && std::getline(stream, line)) + { + if((result = expandVarsAndSend(req, line)) != ESP_OK) + { + httpd_resp_sendstr_chunk(req, NULL); + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to send file"); + break; + } + } + + if(result == ESP_OK) + { + result = httpd_resp_send_chunk(req, NULL, 0); } } - // 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); } @@ -997,65 +1042,47 @@ esp_err_t WiFi::defaultFileHandler(httpd_req_t *req) struct stat file_stat; char *buf; int bufLen; + esp_err_t result = ESP_OK; std::string gzipFile = ""; std::string disposition = ""; - + + // Per-request local variables — the shared wifiCtrl.session struct must NOT be + // used here because multiple requests are handled concurrently by esp_http_server. + // Using the shared session caused a race condition where parallel requests + // overwrote each other's filePath/fileName/gzip fields, leaving some responses + // stuck forever in "pending" state. + std::string localFilePath; + std::string localFileName; + bool localGzip = false; + // 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; - } + // Close connection after each file response. The server is single-threaded — + // while sending a large file (e.g. 98KB font), all other requests are blocked. + // With keep-alive, the browser queues subsequent requests on existing connections + // that may be stalled behind a large transfer, leaving them "pending" forever. + // With Connection:close, the browser opens fresh connections for each resource. + httpd_resp_set_hdr(req, "Connection", "close"); + // 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); + localGzip = (strstr(buf, "gzip") != 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) + // Look for a filename in the URI and construct the file path returning both. + if(pThis->getPathFromURI(localFilePath, localFileName, pThis->wifiCtrl.run.basePath, req->uri) == ESP_FAIL) { - 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 = "/"; + localFileName = "/"; } else { httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Filename invalid"); @@ -1063,18 +1090,17 @@ esp_err_t WiFi::defaultFileHandler(httpd_req_t *req) } } - // 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) + // See if the provided name matches a static handler, such as root. + if(localFileName.compare("/") == 0 || localFileName.compare("index.html") == 0 || localFileName.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) + // Is this a macro to specify keymap file? + if(localFileName.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()); + localFileName = std::regex_replace(localFileName, std::regex("keymap"), pThis->keyIf->getKeyMapFileName()); + localFilePath = std::regex_replace(localFilePath, 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) { @@ -1084,23 +1110,17 @@ esp_err_t WiFi::defaultFileHandler(httpd_req_t *req) } // Get details of the file, throw error 404 - File Not Found on error. - if(stat(pThis->wifiCtrl.session.filePath.c_str(), &file_stat) == -1) + if(stat(localFilePath.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"; + if(localGzip) + gzipFile = localFilePath + ".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) + if(localGzip == 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"); @@ -1108,67 +1128,57 @@ esp_err_t WiFi::defaultFileHandler(httpd_req_t *req) } } - // 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) + // If the file is HTML, JS or CSS then process externally as we need to substitute embedded variables. + // Guard around gzipFile.size: gzipped html/js/css cannot be parsed, served 'as is'. + if((pThis->isFileExt(localFileName, ".html") || (pThis->isFileExt(localFileName, ".js") && !pThis->isFileExt(localFileName, ".min.js")) || (pThis->isFileExt(localFileName, ".css") && !pThis->isFileExt(localFileName, ".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); + result = pThis->expandAndSendFile(req, pThis->wifiCtrl.run.basePath, localFileName); } 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"); + fd = fopen(gzipFile.size() > 0 ? gzipFile.c_str() : localFilePath.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); + ESP_LOGI(WIFITAG, "Sending %sfile : %s (%ld bytes)...", gzipFile.size() > 0 ? "gzip " : " ", localFileName.c_str(), file_stat.st_size); + pThis->setContentTypeFromFileType(req, localFileName); + + // Use smart pointer to prevent leaks on error paths. + std::unique_ptr chunk(new (std::nothrow) char[MAX_CHUNK_SIZE]); + if(chunk == nullptr) + { + ESP_LOGE(WIFITAG, "Memory exhausted in defaultFileHandler — closing socket"); + fclose(fd); + int sockFd = httpd_req_to_sockfd(req); + if(sockFd != -1) close(sockFd); + return(ESP_FAIL); + } - // 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) + chunksize = fread(chunk.get(), 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) + if(httpd_resp_send_chunk(req, chunk.get(), 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); - } + result = ESP_FAIL; + httpd_resp_send_chunk(req, NULL, 0); + int sockFd = httpd_req_to_sockfd(req); + if(sockFd != -1) close(sockFd); + } } + } while(chunksize != 0 && result == ESP_OK); - // 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; + if(result == ESP_OK) + httpd_resp_send_chunk(req, NULL, 0); + return result; } // Handler to send data sets. The handler is triggered on the /data URI and subpaths define the data to be sent. @@ -2088,6 +2098,46 @@ esp_err_t WiFi::mouseDataPOSTHandler(httpd_req_t *req, std::vector pai return(dataError == false ? ESP_OK : ESP_FAIL); } +// /data/hostconfig POST handler. Process host machine selection and store in NVS. +// +esp_err_t WiFi::hostConfigDataPOSTHandler(httpd_req_t *req, std::vector pairs, std::string& resp) +{ + // Locals. + // + bool dataError = false; + uint32_t newHostMode = 0; + + resp = ""; + for(auto pair : pairs) + { + if(pair.name.compare("hostmode") == 0) + { + newHostMode = (uint32_t)atoi(pair.value.c_str()); + + // Validate the value. + if(newHostMode != 0 && newHostMode != 2500 && newHostMode != 2800 && newHostMode != 1 && + newHostMode != 68000 && newHostMode != 5600 && newHostMode != 9801 && newHostMode != 2) + { + resp.append("Invalid host mode value: " + pair.value); + dataError = true; + } + } + } + + if(!dataError) + { + sharpKeyConfig.params.hostMode = newHostMode; + if(nvs->persistData(SHARPKEY_NAME, &sharpKeyConfig, sizeof(struct SharpKeyConfig)) == false || + nvs->commitData() == false) + { + resp.append("Save config to NVS RAM failed, retry, if 2nd attempt fails, power cycle the interface."); + dataError = true; + } + } + + 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) @@ -2132,6 +2182,14 @@ esp_err_t WiFi::defaultDataPOSTHandler(httpd_req_t *req) resp = "Data values accepted. Press 'Reboot' to restart interface with new values."; } } + if(uriStr.compare("hostconfig") == 0) + { + if((ret = pThis->hostConfigDataPOSTHandler(req, pairs, resp)) == ESP_OK) + { + pThis->wifiCtrl.run.rebootButton = true; + resp = "Host configuration saved. Press 'Reboot' to restart interface with new host selection."; + } + } } else { resp = "

No values in POST, check browser!

"; @@ -2193,10 +2251,19 @@ bool WiFi::startWebserver(void) httpd_config_t config = HTTPD_DEFAULT_CONFIG(); // Tweak default settings. - config.stack_size = 10240; + // Pages request multiple files simultaneously (CSS, JS, fonts, images). + // The default 7 sockets causes LRU purge to kill active transfers, leaving + // resources stuck in "pending" state forever. 20 sockets accommodates + // parallel requests with spare slots. Aggressive recv_wait_timeout recycles + // idle keep-alive connections; send_wait_timeout prevents stale connections + // from blocking fresh ones. + config.stack_size = 16384; config.uri_match_fn = httpd_uri_match_wildcard; config.lru_purge_enable = true; - config.max_uri_handlers = 12; + config.max_uri_handlers = 14; + config.max_open_sockets = 7; + config.recv_wait_timeout = 1; + config.send_wait_timeout = 15; // Setup the required paths and descriptors then register them with the server. const httpd_uri_t dataPOST = { diff --git a/main/include/WiFi.h b/main/include/WiFi.h index 9fdb2cc..30fe52b 100644 --- a/main/include/WiFi.h +++ b/main/include/WiFi.h @@ -262,6 +262,7 @@ 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); + esp_err_t hostConfigDataPOSTHandler(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); diff --git a/make_filepack.sh b/make_filepack.sh new file mode 100755 index 0000000..7c1d9e8 --- /dev/null +++ b/make_filepack.sh @@ -0,0 +1,98 @@ +#!/bin/bash + +# PRJDIR hardcoded as CMake may not be in correct directory. +ROOTDIR=/srv/dvlp/Projects +PRJDIR=${PWD} +if [[ "${PWD}" != "${ROOTDIR}/SharpKey" ]]; then + PRJDIR=$(dirname ${PWD}) +fi +#if [[ "${PRJDIR}" != "${ROOTDIR}/SharpKey" ]] && [[ "${PRJDIR}" != "/project" ]]; then +# echo "Wrong run directory (${PRJDIR})! Should be /SharpKey" +# exit -1 +#fi +SRCDIR=${PRJDIR}/webserver +WEBFSDIR=${PRJDIR}/webfs + +OLDWEBFSVERSION=$(cat ${SRCDIR}/webfs_version.txt) +NEWWEBFSVERSION=$(perl -e "$(echo "print $(cat ${SRCDIR}/webfs_version.txt)+0.01")") +OLDFILEPACKVERSION=$(cat ${PRJDIR}/filepack_version.txt) +NEWFILEPACKVERSION=$(perl -e "$(echo "print $(cat ${PRJDIR}/filepack_version.txt)+0.01")") + +ISNEWER=$(find ${SRCDIR} -newer ${SRCDIR}/webfs_version.txt) +if [[ ${ISNEWER} != "" ]]; then + + echo "Building into:$WEBFSDIR from $SRCDIR..." + + mkdir -p ${WEBFSDIR}/ + rm -fr ${WEBFSDIR}/* + mkdir -p ${WEBFSDIR}/css + mkdir -p ${WEBFSDIR}/js + mkdir -p ${WEBFSDIR}/font-awesome + mkdir -p ${WEBFSDIR}/font-awesome/css + mkdir -p ${WEBFSDIR}/font-awesome/fonts + mkdir -p ${WEBFSDIR}/images + + echo ${NEWWEBFSVERSION} > ${SRCDIR}/webfs_version.txt + echo "Old WebFS Version:${OLDWEBFSVERSION} -> New Version:${NEWWEBFSVERSION}" + + (cd ${SRCDIR}/; + cp favicon.ico ${WEBFSDIR}/ + cp webfs_version.txt ${WEBFSDIR}/ + cp index.html ${WEBFSDIR}/ + cp keymap.html ${WEBFSDIR}/keymap.html + cp mouse.html ${WEBFSDIR}/mouse.html + cp ota.html ${WEBFSDIR}/ota.html + cp wifimanager.html ${WEBFSDIR}/wifimanager.html + cp hostconfig.html ${WEBFSDIR}/hostconfig.html + + + (cd ${SRCDIR}/css; + cp bootstrap.min.css.gz ${WEBFSDIR}/css/ + gzip -c jquery.edittable.min.css > ${WEBFSDIR}/css/jquery.edittable.min.css.gz + gzip -c sb-admin.css > ${WEBFSDIR}/css/sb-admin.css.gz + gzip -c sharpkey.css > ${WEBFSDIR}/css/sharpkey.css.gz + gzip -c style.css > ${WEBFSDIR}/css/style.css.gz + gzip -c styles.css > ${WEBFSDIR}/css/styles.css.gz + ) + + (cd ${SRCDIR}/font-awesome + ) + + (cd ${SRCDIR}/font-awesome/css + gzip -c font-awesome.css > ${WEBFSDIR}/font-awesome/css/font-awesome.min.css.gz + ) + + (cd ${SRCDIR}/font-awesome/fonts + gzip -c fontawesome-webfont.woff > ${WEBFSDIR}/font-awesome/fonts/fontawesome-webfont.woff.gz + ) + + (cd ${SRCDIR}/images; + ) + + (cd ${SRCDIR}/js; + cp 140medley.min.js ${WEBFSDIR}/js/ + cp bootstrap.min.js.gz ${WEBFSDIR}/js/ + gzip -c index.js > ${WEBFSDIR}/js/index.js.gz + gzip -c jquery.edittable.js > ${WEBFSDIR}/js/jquery.edittable.js.gz + gzip -c jquery.edittable.min.js > ${WEBFSDIR}/js/jquery.edittable.min.j.gz + cp jquery.min.js.gz ${WEBFSDIR}/js/ + gzip -c keymap.js > ${WEBFSDIR}/js/keymap.js.gz + gzip -c mouse.js > ${WEBFSDIR}/js/mouse.js.gz + gzip -c ota.js > ${WEBFSDIR}/js/ota.js.gz + gzip -c wifimanager.js > ${WEBFSDIR}/js/wifimanager.js.gz + ) + + ) + + echo ${NEWFILEPACKVERSION} > ${PRJDIR}/filepack_version.txt + echo "Old Filepack Version:${OLDFILEPACKVERSION} -> New Version:${NEWFILEPACKVERSION}" + + echo "Building FilePack v${NEWFILEPACKVERSION}..." + cd ${PRJDIR} + rm -f filepack*tar + mv filepack*gz archive/ 2>/dev/null + tar -cvf release/filepack_sharpkey_${NEWFILEPACKVERSION}.tar sharpkey_version.txt filepack_version.txt webfs/ + gzip release/filepack_sharpkey_${NEWFILEPACKVERSION}.tar +else + echo "No FilePack change, current version:${OLDFILEPACKVERSION}" +fi diff --git a/make_release.sh b/make_release.sh new file mode 100755 index 0000000..67794cd --- /dev/null +++ b/make_release.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +ROOTDIR=/dvlp/Projects +PRJDIR=${PWD} +if [[ "${PWD}" = "/srv${ROOTDIR}/SharpKey" ]]; then + ROOTDIR=/srv/dvlp/Projects +fi +if [[ "${PWD}" != "${ROOTDIR}/SharpKey" ]]; then + PRJDIR=$(dirname ${PWD}) +fi +if [[ "${PRJDIR}" != "${ROOTDIR}/SharpKey" ]] && [[ "${PRJDIR}" != "/project" ]]; then + echo "Wrong run directory (${PRJDIR})! Should be /SharpKey" + exit -1 +fi +RELEASEDIR=${PRJDIR}/release +VERSION=$(perl -e "$(echo "print $(cat ${PRJDIR}/version.txt)-0.00")") + +mkdir -p ${RELEASEDIR} +cp ${PRJDIR}/build/main.bin ${RELEASEDIR}/sharpkey_fw_v${VERSION}.bin +#cp ${PRJDIR}/filepack*gz ${RELEASEDIR}/ diff --git a/sdkconfig b/sdkconfig index 97213f8..eda9b54 100644 --- a/sdkconfig +++ b/sdkconfig @@ -131,8 +131,8 @@ CONFIG_PARTITION_TABLE_MD5=y # # SharpKey Configuration # -CONFIG_SHARPKEY=y -# CONFIG_MZ25KEY_MZ2500 is not set +# CONFIG_SHARPKEY is not set +CONFIG_MZ25KEY_MZ2500=y # CONFIG_MZ25KEY_MZ2800 is not set CONFIG_DISABLE_FEATURE_SECURITY=y # CONFIG_ENABLE_FEATURE_SECURITY is not set diff --git a/sharpkey_version.txt b/sharpkey_version.txt new file mode 100644 index 0000000..993f095 --- /dev/null +++ b/sharpkey_version.txt @@ -0,0 +1 @@ +1.05 diff --git a/update_version.sh b/update_version.sh new file mode 100755 index 0000000..e33c4e1 --- /dev/null +++ b/update_version.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +PRJDIR=$(dirname ${PWD}) +OLDVERSION=$(cat ${PRJDIR}/version.txt) +NEWVERSION=$(perl -e "$(echo "print $(cat ${PRJDIR}/version.txt)+0.01")") + +# NB: SharpKey project version is updated manually in the file ${PRJDIR}/sharpkey_version.txt +ISNEWER=$(find ${PRJDIR}/main -newer ${PRJDIR}/version.txt) +if [[ ${ISNEWER} != "" ]]; then + echo ${NEWVERSION} > ${PRJDIR}/version.txt + echo "" + echo "Version:${OLDVERSION} -> Next Version:${NEWVERSION}" +else + echo "" + echo "Build Version:${OLDVERSION}" +fi diff --git a/version.txt b/version.txt index 993f095..16e5283 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.05 +1.07 diff --git a/webserver/css/sb-admin.css b/webserver/css/sb-admin.css index f371f07..4ad7541 100644 --- a/webserver/css/sb-admin.css +++ b/webserver/css/sb-admin.css @@ -1,8 +1,8 @@ -/* +/* Author: Start Bootstrap - http://startbootstrap.com 'SB Admin' HTML Template by Start Bootstrap -All Start Bootstrap themes are licensed under Apache 2.0. +All Start Bootstrap themes are licensed under Apache 2.0. For more info and more free Bootstrap 3 HTML themes, visit http://startbootstrap.com! */ @@ -12,6 +12,23 @@ For more info and more free Bootstrap 3 HTML themes, visit http://startbootstrap body { margin-top: 50px; + background: + /* Subtle diagonal pinstripe pattern */ + repeating-linear-gradient( + 135deg, + transparent, + transparent 10px, + rgba(0,0,0,0.015) 10px, + rgba(0,0,0,0.015) 11px + ), + /* Soft radial vignette - lighter centre, slightly darker edges */ + radial-gradient( + ellipse at center, + #f5f6f8 0%, + #e8eaef 60%, + #dcdfe6 100% + ); + background-attachment: fixed; } #wrapper { @@ -23,6 +40,12 @@ body { padding: 5px 15px; } +/* Ensure all panel content can scroll horizontally on small screens */ +.panel-body { + overflow-x: auto; + -webkit-overflow-scrolling: touch; +} + /* Nav Messages */ .messages-dropdown .dropdown-menu .message-preview .avatar, @@ -86,21 +109,40 @@ table.tablesorter thead tr th:hover { /* Edit Below to Customize Widths > 768px */ @media (min-width:768px) { + /* Centered layout: sidebar(225) + content(1210) = 1435px total */ + + /* Navbar: dark background only within the centred area, page background outside */ + .navbar-inverse { + background-color: transparent; + border: none; + } + .nav-centered { + max-width: 1435px; + margin: 0 auto; + position: relative; + background-color: #222222; + } + /* Wrappers */ #wrapper { padding-left: 225px; + max-width: 1435px; + margin: 0 auto; } #page-wrapper { padding: 15px 25px; + max-width: 1210px; + background: #fff; + min-height: calc(100vh - 50px); } - /* Side Nav */ + /* Side Nav - fixed but tracks the centred layout */ .side-nav { - margin-left: -225px; - left: 225px; + margin-left: 0; + left: max(0px, calc(50% - 717.5px)); width: 225px; position: fixed; top: 50px; diff --git a/webserver/css/sharpkey.css b/webserver/css/sharpkey.css index 9bd439e..3114429 100644 --- a/webserver/css/sharpkey.css +++ b/webserver/css/sharpkey.css @@ -262,5 +262,80 @@ input[type=radio] .radio-mouse, input.radio .radio-mouse { float: left; clear: none; - margin: 2px 0 0 2px; + margin: 2px 0 0 2px; +} + +/* Sidebar navigation styling */ +.side-nav > li > a { + margin-left: 20px; + padding-left: 15px; +} + +.side-nav .collapse { + padding-left: 0; + list-style: none; +} + +.side-nav .collapse li > a { + margin-left: 40px; + font-size: 0.95em; +} + +.side-nav .collapse .collapse li > a { + padding-left: 55px; + font-size: 0.92em; +} + +.side-nav .collapse a > i { + margin-right: 8px; + width: 18px; + text-align: center; +} + +.side-nav li > a.dropdown-toggle { + font-weight: 500; +} + +.side-nav > li > a:hover, +.side-nav > li > a:focus, +.side-nav .collapse li > a:hover, +.side-nav .collapse li > a:focus { + background-color: rgba(255, 200, 50, 0.15) !important; + color: #ffd700 !important; +} + +.side-nav li.active > a { + background-color: #262c32; + color: white !important; +} + +.side-nav a.active { + background-color: #322f11; + color: white !important; + font-weight: bold; +} + +.side-nav .dropdown-toggle { + padding-right: 25px; + position: relative; +} + +.side-nav .dropdown-toggle .caret { + position: absolute; + right: 40px; + top: 50%; + margin-top: -2px; + float: none; +} + +.side-nav a:focus:not(:focus-visible), +.side-nav .dropdown-toggle:focus:not(:focus-visible) { + outline: none !important; + box-shadow: none !important; +} + +.side-nav a:focus-visible, +.side-nav .dropdown-toggle:focus-visible { + outline: 2px solid #1abc9c; + outline-offset: 1px; } diff --git a/webserver/hostconfig.html b/webserver/hostconfig.html new file mode 100644 index 0000000..d8da285 --- /dev/null +++ b/webserver/hostconfig.html @@ -0,0 +1,168 @@ + + + + + + + + + Dashboard - SharpKey Admin + + + + + + + + + + + + +
+ + + + +
+ +
+
+

Host Configuration

+ +
+ +

Select the host machine the adapter is connected to. Choose Auto for automatic hardware detection at power-on, or select a specific host to override detection. A reboot is required after saving.

+
+
+
+ +
+
+
+
+

Host Machine Selection

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


+
+ + %SK_ERRMSG% +
+
+
+
+
+ +
+ +
+ + + + + + + + + diff --git a/webserver/index.html b/webserver/index.html index 50f3937..84a53df 100644 --- a/webserver/index.html +++ b/webserver/index.html @@ -23,6 +23,7 @@
@@ -85,15 +98,15 @@ - + - + - - - + + +
SSID:%SK_APSSID%SSID:%SK_APSSID%
Password:%SK_APPWD%Password:%SK_APPWD%
IP (AP):%SK_CURRENTIP%NETMASK:%SK_CURRENTNM%GATEWAY:%SK_CURRENTGW%IP (AP):%SK_CURRENTIP%NETMASK:%SK_CURRENTNM%GATEWAY:%SK_CURRENTGW%
@@ -102,20 +115,20 @@ - + - + - - - + + + - - - + + +
SSID:%SK_CLIENTSSID%SSID:%SK_CLIENTSSID%
DHCP:EnabledDHCP:Enabled
IP (assigned):%SK_CURRENTIP%NETMASK:%SK_CURRENTNM%GATEWAY:%SK_CURRENTGW%IP (assigned):%SK_CURRENTIP%NETMASK:%SK_CURRENTNM%GATEWAY:%SK_CURRENTGW%
IP (fixed):%SK_CURRENTIP%NETMASK:%SK_CURRENTNM%GATEWAY:%SK_CURRENTGW%IP (fixed):%SK_CURRENTIP%NETMASK:%SK_CURRENTNM%GATEWAY:%SK_CURRENTGW%
diff --git a/webserver/js/index.js b/webserver/js/index.js index 778eeab..997788c 100644 --- a/webserver/js/index.js +++ b/webserver/js/index.js @@ -1,18 +1,24 @@ -function showIPConfig() +function showIPConfig() { + var el; + if(document.getElementById("wifiCfg0checked")) { document.getElementById("wifiCfg0checked").style.display = 'compact'; - document.getElementById("wifiCfg").style.display = 'none'; + el = document.getElementById("wifiCfg"); + if(el) el.style.display = 'none'; if(document.getElementById("wifiCfg3checked")) { document.getElementById("wifiCfg3checked").style.display = 'compact'; - document.getElementById("wifiCfg3").style.display = 'none'; + el = document.getElementById("wifiCfg3"); + if(el) el.style.display = 'none'; } else { - document.getElementById("wifiCfg3checked").style.display = 'none'; - document.getElementById("wifiCfg3").style.display = 'compact'; + el = document.getElementById("wifiCfg3checked"); + if(el) el.style.display = 'none'; + el = document.getElementById("wifiCfg3"); + if(el) el.style.display = 'compact'; } if(document.getElementById("wifiCfg1checked")) @@ -20,12 +26,15 @@ function showIPConfig() document.getElementById("wifiCfg1checked").style.display = 'compact'; } else { - document.getElementById("wifiCfg1").style.display = 'none'; + el = document.getElementById("wifiCfg1"); + if(el) el.style.display = 'none'; } } else { - document.getElementById("wifiCfg0").style.display = 'none'; - document.getElementById("wifiCfgchecked").style.display = 'compact'; + el = document.getElementById("wifiCfg0"); + if(el) el.style.display = 'none'; + el = document.getElementById("wifiCfgchecked"); + if(el) el.style.display = 'compact'; } } @@ -37,6 +46,7 @@ function enableIfConfig() if(activeInterface === "KeyInterface ") { document.getElementById("keyMapAvailable").style.display = 'none'; + document.getElementById("mouseCfgAvailable").style.display = 'none'; } // Mouse interface active? else if(activeInterface === "Mouse ") @@ -61,6 +71,15 @@ function enableIfConfig() // On document load, setup the items viewable on the page according to set values. document.addEventListener("DOMContentLoaded", function setPageDefaults() { - showIPConfig(); enableIfConfig(); + showIPConfig(); +}); + +// jQuery dropdown toggle handlers for sidebar submenu animations. +$(document).ready(function(){ + $('.side-nav .dropdown-toggle').on('click', function(e) { + e.preventDefault(); + var $target = $($(this).data('target')); + $target.collapse('toggle'); + }); }); diff --git a/webserver/keymap.html b/webserver/keymap.html index 3abf816..7786e1d 100644 --- a/webserver/keymap.html +++ b/webserver/keymap.html @@ -25,6 +25,7 @@
diff --git a/webserver/mouse.html b/webserver/mouse.html index dc2322e..4fe80f6 100644 --- a/webserver/mouse.html +++ b/webserver/mouse.html @@ -25,6 +25,7 @@
diff --git a/webserver/ota.html b/webserver/ota.html index 5e81850..90a03fc 100644 --- a/webserver/ota.html +++ b/webserver/ota.html @@ -23,6 +23,7 @@
diff --git a/webserver/version.txt b/webserver/version.txt deleted file mode 100644 index 1010473..0000000 --- a/webserver/version.txt +++ /dev/null @@ -1 +0,0 @@ -1.02 diff --git a/webserver/webfs_version.txt b/webserver/webfs_version.txt new file mode 100644 index 0000000..11a84ad --- /dev/null +++ b/webserver/webfs_version.txt @@ -0,0 +1 @@ +1.04 diff --git a/webserver/wifimanager.html b/webserver/wifimanager.html index bf4c8e0..f602ff8 100644 --- a/webserver/wifimanager.html +++ b/webserver/wifimanager.html @@ -24,6 +24,7 @@