2940 lines
121 KiB
C++
2940 lines
121 KiB
C++
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// 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 <philip.smart@net2net.org>
|
|
//
|
|
// 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 <http://www.gnu.org/licenses/>.
|
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// This is an optional compile time module, only compile if configured.
|
|
#include "sdkconfig.h"
|
|
#if defined(CONFIG_IF_WIFI_ENABLED)
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <fstream>
|
|
#include <iostream>
|
|
#include <sstream>
|
|
#include <iomanip>
|
|
#include <regex>
|
|
#include <filesystem>
|
|
#include <vector>
|
|
#include <algorithm>
|
|
#include <memory>
|
|
#include <unistd.h>
|
|
#include <map>
|
|
#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 <sys/param.h>
|
|
#include "esp_tls_crypto.h"
|
|
#include <esp_http_server.h>
|
|
#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;
|
|
|
|
// Template to convert a given type into a std::string.
|
|
//
|
|
template<typename Type> 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<int>(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<int>(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<std::string> 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(",\"<i class=\\\"fa fa-check\\\"></i>\"]");
|
|
} 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<std::string> 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<std::string> 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: '<input type=\"text\" class=\"" + typeList[idx].substr(startPos+11, std::string::npos) + "\" data-placement=\"left\"/>', \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<uint32_t> 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<std::string> headerList;
|
|
std::vector<std::string> typeList;
|
|
std::vector<std::pair<std::string, int>> 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 = "<div class=\"popover-markup\" id=\"popover-" + jsClass + "\">\n" +
|
|
" <div class=\"head hide\">" + headerList[idx] + "</div>\n" +
|
|
" <div class=\"content hide\">\n" +
|
|
" <form role=\"form\">\n" +
|
|
" <div class=\"row\">\n" +
|
|
" <div class=\"col-xs-6 col-md-8\">\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(" <div class=\"checkbox\">\n")
|
|
.append(" <label>\n")
|
|
.append(" <input id=\"").append(jsClass).append("_").append(iter->first).append("\" type=\"checkbox\" data-value=\"").append(to_str(iter->second, 0, 10)).append("\"/> ").append(iter->first).append("\n")
|
|
.append(" </label>\n")
|
|
.append(" </div>\n");
|
|
}
|
|
}
|
|
else if(typeList[idx].find("custom_rd") != std::string::npos)
|
|
{
|
|
for(auto iter = std::begin(selectList); iter != std::end(selectList); iter++)
|
|
{
|
|
jsArray.append(" <div class=\"radio\">\n")
|
|
.append(" <label>\n")
|
|
.append(" <input id=\"").append(jsClass).append("_").append(iter->first).append("\" type=\"radio\" name=\"").append(jsClass).append("\" data-value=\"").append(to_str(iter->second, 0, 10)).append("\"/> ").append(iter->first).append("\n")
|
|
.append(" </label>\n")
|
|
.append(" </div>\n");
|
|
}
|
|
}
|
|
|
|
// Finish off by closing up the opened DIV blocks.
|
|
jsArray.append(" </div>\n")
|
|
.append(" </div>\n")
|
|
.append(" </form>\n")
|
|
.append(" </div>\n")
|
|
.append("</div>\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<std::string> typeList;
|
|
std::vector<std::pair<std::string, int>> 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("<div class=\"form-check form-check-inline\">\n");
|
|
for(auto iter = std::begin(selectList); iter != std::end(selectList); iter++)
|
|
{
|
|
// Skip current value item.
|
|
if(iter == std::begin(selectList)) continue;
|
|
|
|
typeStr.append(" <input class=\"form-check-input radio-mouse\" id=\"").append(typeList[idx]).append("_").append(iter->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(" <label class=\"form-check-label radio-mouse\" for=\"").append(typeList[idx]).append("_").append(iter->first).append("\">").append(iter->first).append("</label>\n");
|
|
}
|
|
typeStr.append("</div><br>");
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
{
|
|
// 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;
|
|
int startPos;
|
|
t_kvPair keyValue;
|
|
esp_err_t result = ESP_OK;
|
|
std::vector<t_kvPair> 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_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);
|
|
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 << "<table class=\"table table-borderless table-sm\"><tbody><tr>";
|
|
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 << "</tr>";
|
|
if(idx < wifiCtrl.run.versionList->elements) { list << "<tr>"; }
|
|
}
|
|
list << "<td><span style=\"color: blue;\">" << wifiCtrl.run.versionList->item[idx]->object << "</span> <i>(v" << to_str(wifiCtrl.run.versionList->item[idx]->version, 2, 10) << ") </i></td> ";
|
|
}
|
|
list << "</tr></tbody?></table>";
|
|
keyValue.value = list.str();
|
|
} else { keyValue.value = "Unknown"; }; pairs.push_back(keyValue);
|
|
keyValue.name = "%SK_FILEPACK%"; {
|
|
std::ostringstream list;
|
|
list << "<table class=\"table table-borderless table-sm\"><tbody><tr>";
|
|
list << "<td><span style=\"color: blue;\">FilePack</span> <i>(v" << to_str(getVersionNumber("FilePack"), 2, 10) << ")</i></td> ";
|
|
list << "</tr></tbody?></table>";
|
|
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 << "<tr>"
|
|
<< "<td>" << part->label << "</td>"
|
|
<< "<td>" << esp32PartitionType(part->type) << "</td>"
|
|
<< "<td>" << esp32PartitionSubType(part->subtype) << "</td>"
|
|
<< "<td>" << to_str(part->address, 0, 16) << "</td>"
|
|
<< "<td>" << to_str(part->size, 0, 16) << "</td>"
|
|
<< "<td>" << (err == ESP_OK ? appDesc.version : part->subtype == ESP_PARTITION_SUBTYPE_DATA_SPIFFS ? to_str(getVersionNumber("FilePack"), 2, 10) : "") << "</td>"
|
|
<< "<td>" << (err == ESP_OK ? appDesc.date : "") << " "
|
|
<< (err == ESP_OK ? appDesc.time : "") << "</td>"
|
|
<< "<td>" << (runPart->address == part->address ? "Yes" : "") << "</td>"
|
|
<< "</tr>";
|
|
}
|
|
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 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());
|
|
if(!inFile.is_open())
|
|
{
|
|
httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "File does not exist");
|
|
return(ESP_FAIL);
|
|
}
|
|
|
|
std::string contents((std::istreambuf_iterator<char>(inFile)), std::istreambuf_iterator<char>());
|
|
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);
|
|
}
|
|
}
|
|
|
|
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;
|
|
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;
|
|
|
|
// 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];
|
|
if(httpd_req_get_hdr_value_str(req, "Accept-Encoding", buf, bufLen) == ESP_OK)
|
|
{
|
|
localGzip = (strstr(buf, "gzip") != NULL ? true : false);
|
|
}
|
|
delete buf;
|
|
}
|
|
|
|
// 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)
|
|
{
|
|
if(strlen(req->uri) == 1 && req->uri[0] == '/')
|
|
{
|
|
localFileName = "/";
|
|
} 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(localFileName.compare("/") == 0 || localFileName.compare("index.html") == 0 || localFileName.compare("index.htm") == 0)
|
|
{
|
|
return pThis->expandAndSendFile(req, pThis->wifiCtrl.run.basePath, "index.html");
|
|
}
|
|
|
|
// Is this a macro to specify keymap file?
|
|
if(localFileName.compare("keymap") == 0)
|
|
{
|
|
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)
|
|
{
|
|
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(localFilePath.c_str(), &file_stat) == -1)
|
|
{
|
|
if(localGzip)
|
|
gzipFile = localFilePath + ".gz";
|
|
|
|
if(localGzip == true && stat(gzipFile.c_str(), &file_stat) == -1)
|
|
{
|
|
httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "File does not exist");
|
|
return(ESP_FAIL);
|
|
}
|
|
|
|
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 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)
|
|
{
|
|
result = pThis->expandAndSendFile(req, pThis->wifiCtrl.run.basePath, localFileName);
|
|
} else
|
|
{
|
|
fd = fopen(gzipFile.size() > 0 ? gzipFile.c_str() : localFilePath.c_str(), "r");
|
|
if(!fd)
|
|
{
|
|
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 " : " ", localFileName.c_str(), file_stat.st_size);
|
|
pThis->setContentTypeFromFileType(req, localFileName);
|
|
|
|
// Use smart pointer to prevent leaks on error paths.
|
|
std::unique_ptr<char[]> 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);
|
|
}
|
|
|
|
size_t chunksize;
|
|
do {
|
|
chunksize = fread(chunk.get(), 1, MAX_CHUNK_SIZE, fd);
|
|
if(chunksize > 0)
|
|
{
|
|
if(httpd_resp_send_chunk(req, chunk.get(), chunksize) != ESP_OK)
|
|
{
|
|
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);
|
|
|
|
fclose(fd);
|
|
ESP_LOGI(WIFITAG, "File sending complete");
|
|
}
|
|
|
|
// Respond with an empty chunk to signal HTTP response completion.
|
|
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.
|
|
//
|
|
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<uint32_t> 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<t_kvPair> *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<std::string> 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<t_kvPair> 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<t_kvPair> 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/hostconfig POST handler. Process host machine selection and store in NVS.
|
|
//
|
|
esp_err_t WiFi::hostConfigDataPOSTHandler(httpd_req_t *req, std::vector<t_kvPair> 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)
|
|
{
|
|
// Locals.
|
|
//
|
|
std::vector<t_kvPair> 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.";
|
|
}
|
|
}
|
|
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 = "<p>No values in POST, check browser!</p>";
|
|
}
|
|
|
|
// Add in an error message if one has been generated.
|
|
pThis->wifiCtrl.run.errorMsg = "<font size=\"2\" face=\"verdana\" color=\"";
|
|
pThis->wifiCtrl.run.errorMsg += (ret == ESP_OK ? "green" : "red");
|
|
pThis->wifiCtrl.run.errorMsg += "\">" + resp + "</font>";
|
|
|
|
// 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 = "<head> <meta http-equiv=\"refresh\" content=\"10; URL=http://";
|
|
resp += pThis->wifiConfig.clientParams.ip;
|
|
resp += "/\" /> </head><body><font size=\"5\" face=\"verdana\" color=\"red\"/>Rebooting... </font><font size=\"5\" face=\"verdana\" color=\"black\"/>Please wait.</font></body>";
|
|
} else
|
|
{
|
|
resp = "<head> </head><body><font size=\"5\" face=\"verdana\" color=\"red\"/>Rebooting... </font><br><font size=\"4\" face=\"verdana\" color=\"black\"/><p>Please look in your router admin panel for the assigned IP address and enter http://<router assigned ip address> into browser to continue.</p></font></body>";
|
|
}
|
|
|
|
// 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.
|
|
// 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 = 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 = {
|
|
.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
|